diff --git a/.devcontainer.json b/.devcontainer.json index c40157671ce76..6003d00e23dac 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -1,8 +1,8 @@ { "name": "Dev Container Definition - AWS CDK", - "image": "jsii/superchain", + "image": "jsii/superchain:1-buster-slim", "postCreateCommand": "yarn build --skip-test --no-bail --skip-prereqs --skip-compat", "extensions": [ "dbaeumer.vscode-eslint@2.1.5" ] -} \ No newline at end of file +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7b64323c2a278..96c1345fa6c78 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,3 +11,10 @@ updates: labels: - "auto-approve" open-pull-requests-limit: 5 + - package-ecosystem: "pip" + directory: "/packages/@aws-cdk/lambda-layer-awscli" + schedule: + interval: "weekly" + labels: + - "auto-approve" + open-pull-requests-limit: 5 diff --git a/.github/workflows/issue-label-assign.yml b/.github/workflows/issue-label-assign.yml index 5ed1048d1bd4d..97e0de9be71ad 100644 --- a/.github/workflows/issue-label-assign.yml +++ b/.github/workflows/issue-label-assign.yml @@ -8,7 +8,7 @@ on: types: [opened, edited] jobs: - test: + issue-triage-manager: permissions: issues: write pull-requests: write @@ -17,16 +17,57 @@ jobs: - uses: aws-github-ops/aws-issue-triage-manager@main with: github-token: "${{ secrets.GITHUB_TOKEN }}" + target: "issues" excluded-expressions: "[CDK CLI Version|TypeScript|Java|Python]" area-is-keyword: true + included-labels: "[needs-triage]" + excluded-labels: "[p1|p2|p0|effort-small|effort-medium|effort-large|guidance]" + default-area: ${{ env.OSDS_DEVS }} + parameters: ${{ env.AREA_PARAMS }} + guidance-triage-manager: + permissions: + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: aws-github-ops/aws-issue-triage-manager@main + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + target: "issues" + excluded-expressions: "[CDK CLI Version|TypeScript|Java|Python]" + area-is-keyword: true + included-labels: "[guidance]" + default-area: ${{ env.OSDS_DEVS }} parameters: > + [{"area":"guidance","keywords":["guidance"]}] + pr-triage-manager: + permissions: + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: aws-github-ops/aws-issue-triage-manager@main + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + target: "pull-requests" + area-is-keyword: true + excluded-labels: "[contribution/core]" + parameters: ${{ env.AREA_PARAMS }} + +env: + OSDS_DEVS: > + { + "assignees":["NGL321","peterwoodworth","ryparker"] + } + + AREA_PARAMS: > [ {"area":"package/tools","keywords":["cli","command line","init","synth","diff","bootstrap"],"labels":["package/tools"],"assignees":["rix0rrr"]}, {"area":"@aws-cdk/alexa-ask","keywords":["alexa-ask","alexa", "cfnskill"],"labels":["@aws-cdk/alexa-ask"],"assignees":["madeline-k"]}, {"area":"@aws-cdk/app-delivery","keywords":["app-delivery","PipelineDeployStackAction"],"labels":["@aws-cdk/app-delivery"],"assignees":["skinny85"]}, {"area":"@aws-cdk/assert","keywords":["assert"],"labels":["@aws-cdk/assert"],"assignees":["kaizen3031593"]}, {"area":"@aws-cdk/assertions","keywords":["assertions"],"labels":["@aws-cdk/assertions"],"assignees":["kaizen3031593"]}, - {"area":"@aws-cdk/assets","keywords":["assets","staging"],"labels":["@aws-cdk/assets"],"assignees":["eladb"]}, + {"area":"@aws-cdk/assets","keywords":["assets","staging"],"labels":["@aws-cdk/assets"],"assignees":["otaviomacedo"]}, {"area":"@aws-cdk/aws-accessanalyzer","keywords":["aws-accessanalyzer","accessanalyzer","cfnanalyzer"],"labels":["@aws-cdk/aws-accessanalyzer"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-acmpca","keywords":["aws-acmpca","acmpca","certificateauthority"],"labels":["@aws-cdk/aws-acmpca"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-amazonmq","keywords":["aws-amazonmq","amazonmq","cfnbroker"],"labels":["@aws-cdk/aws-amazonmq"],"assignees":["otaviomacedo"]}, @@ -39,12 +80,12 @@ jobs: {"area":"@aws-cdk/aws-appflow","keywords":["aws-appflow","appflow"],"labels":["@aws-cdk/aws-appflow"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-appintegrations","keywords":["(aws-appintegrations)","(appintegrations)"],"labels":["@aws-cdk/aws-appintegrations"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-applicationautoscaling","keywords":["aws-applicationautoscaling","application-autoscaling","scalabletarget"],"labels":["@aws-cdk/aws-applicationautoscaling"],"assignees":["comcalvi"]}, - {"area":"@aws-cdk/aws-applicationinsights","keywords":["aws-applicationinsights","application-insights"],"labels":["@aws-cdk/aws-applicationinsights"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-applicationinsights","keywords":["aws-applicationinsights","application-insights"],"labels":["@aws-cdk/aws-applicationinsights"],"assignees":["corymhall"]}, {"area":"@aws-cdk/aws-appmesh","keywords":["aws-appmesh","app-mesh","GatewayRoute","VirtualGateway","VirtualNode","VirtualRouter","VirtualService"],"labels":["@aws-cdk/aws-appmesh"],"assignees":["Seiya6329"]}, {"area":"@aws-cdk/aws-appstream","keywords":["aws-appstream","app-stream"],"labels":["@aws-cdk/aws-appstream"],"assignees":["madeline-k"]}, {"area":"@aws-cdk/aws-appsync","keywords":["aws-appsync","app-sync","appsyncfunction","graphqlapi","dynamodbdatasource","lambdadatasource","nonedatasource","rdsdatasource","resolver"],"labels":["@aws-cdk/aws-appsync"],"assignees":["otaviomacedo"]}, {"area":"@aws-cdk/aws-athena","keywords":["aws-athena","athena","cfndatacatalog","cfnnamedquery","cfnworkgroup"],"labels":["@aws-cdk/aws-athena"],"assignees":["comcalvi"]}, - {"area":"@aws-cdk/aws-auditmanager","keywords":["(aws-auditmanager)","(auditmanager)"],"labels":["@aws-cdk/aws-auditmanager"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-auditmanager","keywords":["(aws-auditmanager)","(auditmanager)"],"labels":["@aws-cdk/aws-auditmanager"],"assignees":["otaviomacedo"]}, {"area":"@aws-cdk/aws-autoscaling","keywords":["aws-autoscaling","auto-scaling","AutoScalingGroup","LifescycleHook","scheduledaction"],"labels":["@aws-cdk/aws-autoscaling"],"assignees":["comcalvi"]}, {"area":"@aws-cdk/aws-autoscaling-api","keywords":["aws-autoscaling-api","autoscaling-api"],"labels":["@aws-cdk/aws-autoscaling-api"],"assignees":["comcalvi"]}, {"area":"@aws-cdk/aws-autoscaling-common","keywords":["aws-autoscaling-common","autoscaling-common","arbitraryintervals","completescalinginterval","scalinginterval"],"labels":["@aws-cdk/aws-autoscaling-common"],"assignees":["comcalvi"]}, @@ -52,19 +93,19 @@ jobs: {"area":"@aws-cdk/aws-autoscalingplans","keywords":["aws-autoscalingplans","autoscaling-plans","cfnscalingplan"],"labels":["@aws-cdk/aws-autoscalingplans"],"assignees":["comcalvi"]}, {"area":"@aws-cdk/aws-backup","keywords":["aws-backup","backupplan","backupselection","backupvault"],"labels":["@aws-cdk/aws-backup"],"assignees":["kaizen3031593"]}, {"area":"@aws-cdk/aws-batch","keywords":["aws-batch","batch","computeenvironment","jobdefinition","jobqueue"],"labels":["@aws-cdk/aws-batch"],"assignees":["madeline-k"]}, - {"area":"@aws-cdk/aws-budgets","keywords":["aws-budgets","budgets","cfnbudget"],"labels":["@aws-cdk/aws-budgets"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-budgets","keywords":["aws-budgets","budgets","cfnbudget"],"labels":["@aws-cdk/aws-budgets"],"assignees":["otaviomacedo"]}, {"area":"@aws-cdk/aws-cassandra","keywords":["aws-cassandra","cassandra","cfnkeyspace"],"labels":["@aws-cdk/aws-cassandra"],"assignees":["otaviomacedo"]}, - {"area":"@aws-cdk/aws-ce","keywords":["aws-ce","cfnanomalymonitor","cfncostcategory"],"labels":["@aws-cdk/aws-ce"],"assignees":["njlynch"]}, - {"area":"@aws-cdk/aws-certificatemanager","keywords":["aws-certificatemanager","certificate-manager","dnsvalidatedcertificate","acm"],"labels":["@aws-cdk/aws-certificatemanager"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-ce","keywords":["aws-ce","cfnanomalymonitor","cfncostcategory"],"labels":["@aws-cdk/aws-ce"],"assignees":["corymhall"]}, + {"area":"@aws-cdk/aws-certificatemanager","keywords":["aws-certificatemanager","certificate-manager","dnsvalidatedcertificate","acm"],"labels":["@aws-cdk/aws-certificatemanager"],"assignees":["comcalvi"]}, {"area":"@aws-cdk/aws-chatbot","keywords":["aws-chatbot","chatbot","slackchannelconfiguration"],"labels":["@aws-cdk/aws-chatbot"],"assignees":["kaizen3031593"]}, {"area":"@aws-cdk/aws-cloud9","keywords":["aws-cloud9","cloud 9","ec2environment"],"labels":["@aws-cdk/aws-cloud9"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-cloudformation","keywords":["aws-cloudformation","nestedstack"],"labels":["@aws-cdk/aws-cloudformation"],"assignees":["rix0rrr"]}, - {"area":"@aws-cdk/aws-cloudfront","keywords":["aws-cloudfront","cloud front","cachepolicy","cloudfrontwebdistribution"],"labels":["@aws-cdk/aws-cloudfront"],"assignees":["njlynch"]}, - {"area":"@aws-cdk/aws-cloudfront-origins","keywords":["aws-cloudfront-origins","cloudfront origins"],"labels":["@aws-cdk/aws-cloudfront-origins"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-cloudfront","keywords":["aws-cloudfront","cloud front","cachepolicy","cloudfrontwebdistribution"],"labels":["@aws-cdk/aws-cloudfront"],"assignees":["comcalvi"]}, + {"area":"@aws-cdk/aws-cloudfront-origins","keywords":["aws-cloudfront-origins","cloudfront origins"],"labels":["@aws-cdk/aws-cloudfront-origins"],"assignees":["comcalvi"]}, {"area":"@aws-cdk/aws-cloudtrail","keywords":["aws-cloudtrail","cloud trail","trail"],"labels":["@aws-cdk/aws-cloudtrail"],"assignees":["comcalvi"]}, {"area":"@aws-cdk/aws-cloudwatch","keywords":["aws-cloudwatch","cloud watch","compositealarm","dashboard"],"labels":["@aws-cdk/aws-cloudwatch"],"assignees":["madeline-k"]}, {"area":"@aws-cdk/aws-cloudwatch-actions","keywords":["aws-cloudwatch-actions","cloudwatch actions","applicationscalingaction","autoscalingaction","ec2action","snsaction"],"labels":["@aws-cdk/aws-cloudwatch-actions"],"assignees":["madeline-k"]}, - {"area":"@aws-cdk/aws-codeartifact","keywords":["aws-codeartifact","code-artifact"],"labels":["@aws-cdk/aws-codeartifact"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-codeartifact","keywords":["aws-codeartifact","code-artifact"],"labels":["@aws-cdk/aws-codeartifact"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-codebuild","keywords":["aws-codebuild","code-build","bitbucketsourcecredentials","githubenterprisesourcecredentials","githubsourcecredentials","pipelineproject","reportgroup","untrustedcodeboundarypolicy"],"labels":["@aws-cdk/aws-codebuild"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-codecommit","keywords":["aws-codecommit","code-commit"],"labels":["@aws-cdk/aws-codecommit"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-codedeploy","keywords":["aws-codedeploy","code-deploy","customlambdadeploymentconfig","ecsapplication","lambdaapplication","lambdadeploymentgroup","serverapplication"],"labels":["@aws-cdk/aws-codedeploy"],"assignees":["skinny85"]}, @@ -85,14 +126,14 @@ jobs: {"area":"@aws-cdk/aws-detective","keywords":["aws-detective","detective"],"labels":["@aws-cdk/aws-detective"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-devopsguru","keywords":["(aws-devopsguru)","(devopsguru)"],"labels":["@aws-cdk/aws-devopsguru"],"assignees":["corymhall"]}, {"area":"@aws-cdk/aws-directoryservice","keywords":["aws-directoryservice","directory-service"],"labels":["@aws-cdk/aws-directoryservice"],"assignees":["rix0rrr"]}, - {"area":"@aws-cdk/aws-dlm","keywords":["aws-dlm","dlm"],"labels":["@aws-cdk/aws-dlm"],"assignees":["njlynch"]}, - {"area":"@aws-cdk/aws-dms","keywords":["aws-dms","dms"],"labels":["@aws-cdk/aws-dms"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-dlm","keywords":["aws-dlm","dlm"],"labels":["@aws-cdk/aws-dlm"],"assignees":["madeline-k"]}, + {"area":"@aws-cdk/aws-dms","keywords":["aws-dms","dms"],"labels":["@aws-cdk/aws-dms"],"assignees":["madeline-k"]}, {"area":"@aws-cdk/aws-docdb","keywords":["aws-docdb","doc-db"],"labels":["@aws-cdk/aws-docdb"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-dynamodb","keywords":["aws-dynamodb","dynamo-db"],"labels":["@aws-cdk/aws-dynamodb"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-dynamodb-global","keywords":["aws-dynamodb-global","dynamodb global"],"labels":["@aws-cdk/aws-dynamodb-global"],"assignees":["skinny85"]}, - {"area":"@aws-cdk/aws-ec2","keywords":["aws-ec2","ec2","vpc","privatesubnet","publicsubnet","vpngateway","vpnconnection","networkacl"],"labels":["@aws-cdk/aws-ec2"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-ec2","keywords":["aws-ec2","ec2","vpc","privatesubnet","publicsubnet","vpngateway","vpnconnection","networkacl"],"labels":["@aws-cdk/aws-ec2"],"assignees":["corymhall"]}, {"area":"@aws-cdk/aws-ecr","keywords":["aws-ecr","ecr"],"labels":["@aws-cdk/aws-ecr"],"assignees":["madeline-k"]}, - {"area":"@aws-cdk/aws-ecr-assets","keywords":["aws-ecr-assets","ecrassets"],"labels":["@aws-cdk/aws-ecr-assets"],"assignees":["eladb"]}, + {"area":"@aws-cdk/aws-ecr-assets","keywords":["aws-ecr-assets","ecrassets"],"labels":["@aws-cdk/aws-ecr-assets"],"assignees":["madeline-k"]}, {"area":"@aws-cdk/aws-ecs","keywords":["(aws-ecs)","(ecs)"],"labels":["@aws-cdk/aws-ecs"],"assignees":["madeline-k"]}, {"area":"@aws-cdk/aws-ecs-patterns","keywords":["(aws-ecs-patterns)","(ecs-patterns)"],"labels":["@aws-cdk/aws-ecs-patterns"],"assignees":["madeline-k"]}, {"area":"@aws-cdk/aws-efs","keywords":["aws-efs","efs","accesspoint"],"labels":["@aws-cdk/aws-efs"],"assignees":["corymhall"]}, @@ -100,20 +141,20 @@ jobs: {"area":"@aws-cdk/aws-eks-legacy","keywords":["(aws-eks-legacy)","(eks-legacy)"],"labels":["@aws-cdk/aws-eks-legacy"],"assignees":["otaviomacedo"]}, {"area":"@aws-cdk/aws-elasticache","keywords":["aws-elasticache","elastic-cache"],"labels":["@aws-cdk/aws-elasticache"],"assignees":["otaviomacedo"]}, {"area":"@aws-cdk/aws-elasticbeanstalk","keywords":["aws-elasticbeanstalk","elastic-beanstalk"],"labels":["@aws-cdk/aws-elasticbeanstalk"],"assignees":["skinny85"]}, - {"area":"@aws-cdk/aws-elasticloadbalancing","keywords":["aws-elasticloadbalancing","elastic-loadbalancing","elb"],"labels":["@aws-cdk/aws-elasticloadbalancing"],"assignees":["njlynch"]}, - {"area":"@aws-cdk/aws-elasticloadbalancingv2","keywords":["aws-elasticloadbalancingv2","elastic-loadbalancing-v2","elbv2","applicationlistener","applicationloadbalancer","applicationtargetgroup","networklistener","networkloadbalancer","networktargetgroup"],"labels":["@aws-cdk/aws-elasticloadbalancingv2"],"assignees":["njlynch"]}, - {"area":"@aws-cdk/aws-elasticloadbalancingv2-actions","keywords":["(aws-elasticloadbalancingv2-actions)","(elasticloadbalancingv2-actions)"],"labels":["@aws-cdk/aws-elasticloadbalancingv2-actions"],"assignees":["njlynch"]}, - {"area":"@aws-cdk/aws-elasticloadbalancingv2-targets","keywords":["aws-elasticloadbalancingv2-targets","elbv2 targets"],"labels":["@aws-cdk/aws-elasticloadbalancingv2-targets"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-elasticloadbalancing","keywords":["aws-elasticloadbalancing","elastic-loadbalancing","elb"],"labels":["@aws-cdk/aws-elasticloadbalancing"],"assignees":["corymhall"]}, + {"area":"@aws-cdk/aws-elasticloadbalancingv2","keywords":["aws-elasticloadbalancingv2","elastic-loadbalancing-v2","elbv2","applicationlistener","applicationloadbalancer","applicationtargetgroup","networklistener","networkloadbalancer","networktargetgroup"],"labels":["@aws-cdk/aws-elasticloadbalancingv2"],"assignees":["corymhall"]}, + {"area":"@aws-cdk/aws-elasticloadbalancingv2-actions","keywords":["(aws-elasticloadbalancingv2-actions)","(elasticloadbalancingv2-actions)"],"labels":["@aws-cdk/aws-elasticloadbalancingv2-actions"],"assignees":["corymhall"]}, + {"area":"@aws-cdk/aws-elasticloadbalancingv2-targets","keywords":["aws-elasticloadbalancingv2-targets","elbv2 targets"],"labels":["@aws-cdk/aws-elasticloadbalancingv2-targets"],"assignees":["corymhall"]}, {"area":"@aws-cdk/aws-elasticsearch","keywords":["aws-elasticsearch","elastic-search"],"labels":["@aws-cdk/aws-elasticsearch"],"assignees":["kaizen3031593"]}, {"area":"@aws-cdk/aws-emr","keywords":["aws-emr","emr"],"labels":["@aws-cdk/aws-emr"],"assignees":["kaizen3031593"]}, {"area":"@aws-cdk/aws-emrcontainers","keywords":["(aws-emrcontainers)","(emrcontainers)"],"labels":["@aws-cdk/aws-emrcontainers"],"assignees":["kaizen3031593"]}, {"area":"@aws-cdk/aws-events","keywords":["aws-events","events","event-bridge","eventbus"],"labels":["@aws-cdk/aws-events"],"assignees":["rix0rrr"]}, {"area":"@aws-cdk/aws-events-targets","keywords":["aws-events-targets","events-targets","events targets"],"labels":["@aws-cdk/aws-events-targets"],"assignees":["rix0rrr"]}, {"area":"@aws-cdk/aws-eventschemas","keywords":["aws-eventschemas","event schemas"],"labels":["@aws-cdk/aws-eventschemas"],"assignees":["skinny85"]}, - {"area":"@aws-cdk/aws-finspace","keywords":["(aws-finspace)","(finspace)"],"labels":["@aws-cdk/aws-finspace"],"assignees":["njlynch"]}, - {"area":"@aws-cdk/aws-fis","keywords":["(aws-fis)","(fis)"],"labels":["@aws-cdk/aws-fis"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-finspace","keywords":["(aws-finspace)","(finspace)"],"labels":["@aws-cdk/aws-finspace"],"assignees":["madeline-k"]}, + {"area":"@aws-cdk/aws-fis","keywords":["(aws-fis)","(fis)"],"labels":["@aws-cdk/aws-fis"],"assignees":["madeline-k"]}, {"area":"@aws-cdk/aws-fms","keywords":["aws-fms","fms"],"labels":["@aws-cdk/aws-fms"],"assignees":["rix0rrr"]}, - {"area":"@aws-cdk/aws-frauddetector","keywords":["(aws-frauddetector)","(frauddetector)"],"labels":["@aws-cdk/aws-frauddetector"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-frauddetector","keywords":["(aws-frauddetector)","(frauddetector)"],"labels":["@aws-cdk/aws-frauddetector"],"assignees":["comcalvi"]}, {"area":"@aws-cdk/aws-fsx","keywords":["aws-fsx","fsx","lustrefilesystem"],"labels":["@aws-cdk/aws-fsx"],"assignees":["rix0rrr"]}, {"area":"@aws-cdk/aws-gamelift","keywords":["aws-gamelift","game lift"],"labels":["@aws-cdk/aws-gamelift"],"assignees":["madeline-k"]}, {"area":"@aws-cdk/aws-globalaccelerator","keywords":["aws-globalaccelerator","global-accelerator"],"labels":["@aws-cdk/aws-globalaccelerator"],"assignees":["rix0rrr"]}, @@ -141,7 +182,7 @@ jobs: {"area":"@aws-cdk/aws-kinesisanalytics","keywords":["aws-kinesisanalytics","kinesisanalytics","kinesis-analytics"],"labels":["@aws-cdk/aws-kinesisanalytics"],"assignees":["otaviomacedo"]}, {"area":"@aws-cdk/aws-kinesisanalytics-flink","keywords":["(aws-kinesisanalytics-flink)","(kinesisanalytics-flink)"],"labels":["@aws-cdk/aws-kinesisanalytics-flink"],"assignees":["otaviomacedo"]}, {"area":"@aws-cdk/aws-kinesisfirehose","keywords":["aws-kinesisfirehose","kinesisfirehose"],"labels":["@aws-cdk/aws-kinesisfirehose"],"assignees":["otaviomacedo"]}, - {"area":"@aws-cdk/aws-kms","keywords":["key-management-service","aws-kms","kms"],"labels":["@aws-cdk/aws-kms"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-kms","keywords":["key-management-service","aws-kms","kms"],"labels":["@aws-cdk/aws-kms"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-lakeformation","keywords":["data-lake","aws-lakeformation","lakeformation"],"labels":["@aws-cdk/aws-lakeformation"],"assignees":["comcalvi"]}, {"area":"@aws-cdk/aws-lambda","keywords":["function","layerversion","aws-lambda","lambda"],"labels":["@aws-cdk/aws-lambda"],"assignees":["kaizen3031593"]}, {"area":"@aws-cdk/aws-lambda-destinations","keywords":["(aws-lambda-destinations)","(lambda-destinations)"],"labels":["@aws-cdk/aws-lambda-destinations"],"assignees":["kaizen3031593"]}, @@ -149,12 +190,12 @@ jobs: {"area":"@aws-cdk/aws-lambda-go","keywords":["(aws-lambda-go)","(lambda-go)"],"labels":["@aws-cdk/aws-lambda-go"],"assignees":["corymhall"]}, {"area":"@aws-cdk/aws-lambda-nodejs","keywords":["nodejsfunction","aws-lambda-nodejs","lambda-nodejs"],"labels":["@aws-cdk/aws-lambda-nodejs"],"assignees":["corymhall"]}, {"area":"@aws-cdk/aws-lambda-python","keywords":["aws-lambda-python","lambda-python","pythonfunction"],"labels":["@aws-cdk/aws-lambda-python"],"assignees":["corymhall"]}, - {"area":"@aws-cdk/aws-licensemanager","keywords":["(aws-licensemanager)","(licensemanager)"],"labels":["@aws-cdk/aws-licensemanager"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-licensemanager","keywords":["(aws-licensemanager)","(licensemanager)"],"labels":["@aws-cdk/aws-licensemanager"],"assignees":["madeline-k"]}, {"area":"@aws-cdk/aws-logs","keywords":["loggroup","aws-logs","logs","logretention"],"labels":["@aws-cdk/aws-logs"],"assignees":["comcalvi"]}, {"area":"@aws-cdk/aws-logs-destinations","keywords":["aws-logs-destinations","lambdadestination","kinesisdestination","logs-destinations"],"labels":["@aws-cdk/aws-logs-destinations"],"assignees":["rix0rrr"]}, {"area":"@aws-cdk/aws-lookoutmetrics","keywords":["(aws-lookoutmetrics)","(lookoutmetrics)"],"labels":["@aws-cdk/aws-lookoutmetrics"],"assignees":["comcalvi"]}, {"area":"@aws-cdk/aws-lookoutvision","keywords":["(aws-lookoutvision)","(lookoutvision)"],"labels":["@aws-cdk/aws-lookoutvision"],"assignees":["comcalvi"]}, - {"area":"@aws-cdk/aws-macie","keywords":["aws-macie","macie"],"labels":["@aws-cdk/aws-macie"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-macie","keywords":["aws-macie","macie"],"labels":["@aws-cdk/aws-macie"],"assignees":["comcalvi"]}, {"area":"@aws-cdk/aws-managedblockchain","keywords":["aws-managedblockchain","managedblockchain"],"labels":["@aws-cdk/aws-managedblockchain"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-mediaconnect","keywords":["(aws-mediaconnect)","(mediaconnect)"],"labels":["@aws-cdk/aws-mediaconnect"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-mediaconvert","keywords":["aws-mediaconvert","mediaconvert"],"labels":["@aws-cdk/aws-mediaconvert"],"assignees":["skinny85"]}, @@ -163,37 +204,37 @@ jobs: {"area":"@aws-cdk/aws-mediapackage","keywords":["aws-mediapackage","mediapackage"],"labels":["@aws-cdk/aws-mediapackage"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-msk","keywords":["aws-msk","kafka","msk","managed-streaming"],"labels":["@aws-cdk/aws-msk"],"assignees":["otaviomacedo"]}, {"area":"@aws-cdk/aws-mwaa","keywords":["(aws-mwaa)","(mwaa)"],"labels":["@aws-cdk/aws-mwaa"],"assignees":["rix0rrr"]}, - {"area":"@aws-cdk/aws-neptune","keywords":["aws-neptune","neptune"],"labels":["@aws-cdk/aws-neptune"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-neptune","keywords":["aws-neptune","neptune"],"labels":["@aws-cdk/aws-neptune"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-networkfirewall","keywords":["(aws-networkfirewall)","(networkfirewall)"],"labels":["@aws-cdk/aws-networkfirewall"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-networkmanager","keywords":["aws-networkmanager","networkmanager","globalnetwork"],"labels":["@aws-cdk/aws-networkmanager"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-nimblestudio","keywords":["(aws-nimblestudio)","(nimblestudio)"],"labels":["@aws-cdk/aws-nimblestudio"],"assignees":["madeline-k"]}, {"area":"@aws-cdk/aws-opensearchservice","keywords":["aws-opensearchservice","opensearchservice","aws-opensearch","opensearch"],"labels":["@aws-cdk/aws-opensearch"],"assignees":["kaizen3031593"]}, {"area":"@aws-cdk/aws-opsworks","keywords":["aws-opsworks","opsworks"],"labels":["@aws-cdk/aws-opsworks"],"assignees":["madeline-k"]}, {"area":"@aws-cdk/aws-opsworkscm","keywords":["aws-opsworkscm","opsworkscm"],"labels":["@aws-cdk/aws-opsworkscm"],"assignees":["madeline-k"]}, - {"area":"@aws-cdk/aws-personalize","keywords":["aws-personalize","personalize"],"labels":["@aws-cdk/aws-personalize"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-personalize","keywords":["aws-personalize","personalize"],"labels":["@aws-cdk/aws-personalize"],"assignees":["comcalvi"]}, {"area":"@aws-cdk/aws-pinpoint","keywords":["aws-pinpoint","pinpoint"],"labels":["@aws-cdk/aws-pinpoint"],"assignees":["otaviomacedo"]}, {"area":"@aws-cdk/aws-pinpointemail","keywords":["aws-pinpointemail","pinpointemail"],"labels":["@aws-cdk/aws-pinpointemail"],"assignees":["otaviomacedo"]}, {"area":"@aws-cdk/aws-qldb","keywords":["aws-qldb","qldb"],"labels":["@aws-cdk/aws-qldb"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-quicksight","keywords":["(aws-quicksight)","(quicksight)"],"labels":["@aws-cdk/aws-quicksight"],"assignees":["comcalvi"]}, {"area":"@aws-cdk/aws-ram","keywords":["aws-ram","ram", "resource-access-manager"],"labels":["@aws-cdk/aws-ram"],"assignees":["madeline-k"]}, {"area":"@aws-cdk/aws-rds","keywords":["aws-rds","rds", "database-cluster","database-instance"],"labels":["@aws-cdk/aws-rds"],"assignees":["skinny85"]}, - {"area":"@aws-cdk/aws-redshift","keywords":["aws-redshift","redshift"],"labels":["@aws-cdk/aws-redshift"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-redshift","keywords":["aws-redshift","redshift"],"labels":["@aws-cdk/aws-redshift"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-resourcegroups","keywords":["resourcegroups","aws-resourcegroups"],"labels":["@aws-cdk/aws-resourcegroups"],"assignees":["madeline-k"]}, - {"area":"@aws-cdk/aws-robomaker","keywords":["aws-robomaker","robomaker","robot"],"labels":["@aws-cdk/aws-robomaker"],"assignees":["njlynch"]}, - {"area":"@aws-cdk/aws-route53","keywords":["aws-route53","route53","recordset","record","hostedzone"],"labels":["@aws-cdk/aws-route53"],"assignees":["njlynch"]}, - {"area":"@aws-cdk/aws-route53-patterns","keywords":["aws-route53-patterns","route53-patterns"],"labels":["@aws-cdk/aws-route53-patterns"],"assignees":["njlynch"]}, - {"area":"@aws-cdk/aws-route53-targets","keywords":["aws-route53-targets","route53-targets"],"labels":["@aws-cdk/aws-route53-targets"],"assignees":["njlynch"]}, - {"area":"@aws-cdk/aws-route53resolver","keywords":["aws-route53resolver","route53resolver"],"labels":["@aws-cdk/aws-route53resolver"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-robomaker","keywords":["aws-robomaker","robomaker","robot"],"labels":["@aws-cdk/aws-robomaker"],"assignees":["comcalvi"]}, + {"area":"@aws-cdk/aws-route53","keywords":["aws-route53","route53","recordset","record","hostedzone"],"labels":["@aws-cdk/aws-route53"],"assignees":["comcalvi"]}, + {"area":"@aws-cdk/aws-route53-patterns","keywords":["aws-route53-patterns","route53-patterns"],"labels":["@aws-cdk/aws-route53-patterns"],"assignees":["comcalvi"]}, + {"area":"@aws-cdk/aws-route53-targets","keywords":["aws-route53-targets","route53-targets"],"labels":["@aws-cdk/aws-route53-targets"],"assignees":["comcalvi"]}, + {"area":"@aws-cdk/aws-route53resolver","keywords":["aws-route53resolver","route53resolver"],"labels":["@aws-cdk/aws-route53resolver"],"assignees":["comcalvi"]}, {"area":"@aws-cdk/aws-s3","keywords":["bucket","aws-s3","s3"],"labels":["@aws-cdk/aws-s3"],"assignees":["otaviomacedo"]}, {"area":"@aws-cdk/aws-s3-assets","keywords":["aws-s3-assets","s3-assets"],"labels":["@aws-cdk/aws-s3-assets"],"assignees":["otaviomacedo"]}, {"area":"@aws-cdk/aws-s3-deployment","keywords":["aws-s3-deployment","s3-deployment"],"labels":["@aws-cdk/aws-s3-deployment"],"assignees":["otaviomacedo"]}, {"area":"@aws-cdk/aws-s3-notifications","keywords":["aws-s3-notifications","s3-notifications"],"labels":["@aws-cdk/aws-s3-notifications"],"assignees":["otaviomacedo"]}, {"area":"@aws-cdk/aws-s3objectlambda","keywords":["(aws-s3objectlambda)","(s3objectlambda)"],"labels":["@aws-cdk/aws-s3objectlambda"],"assignees":["otaviomacedo"]}, {"area":"@aws-cdk/aws-s3outposts","keywords":["(aws-s3outposts)","(s3outposts)"],"labels":["@aws-cdk/aws-s3outposts"],"assignees":["otaviomacedo"]}, - {"area":"@aws-cdk/aws-sagemaker","keywords":["aws-sagemaker","sagemaker"],"labels":["@aws-cdk/aws-sagemaker"],"assignees":["njlynch"]}, - {"area":"@aws-cdk/aws-sam","keywords":["serverless-application-model","aws-sam","sam"],"labels":["@aws-cdk/aws-sam"],"assignees":["njlynch"]}, - {"area":"@aws-cdk/aws-sdb","keywords":["simpledb","aws-sdb","sdb"],"labels":["@aws-cdk/aws-sdb"],"assignees":["njlynch"]}, - {"area":"@aws-cdk/aws-secretsmanager","keywords":["secret","aws-secretsmanager","secrets-manager"],"labels":["@aws-cdk/aws-secretsmanager"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-sagemaker","keywords":["aws-sagemaker","sagemaker"],"labels":["@aws-cdk/aws-sagemaker"],"assignees":["madeline-k"]}, + {"area":"@aws-cdk/aws-sam","keywords":["serverless-application-model","aws-sam","sam"],"labels":["@aws-cdk/aws-sam"],"assignees":["madeline-k"]}, + {"area":"@aws-cdk/aws-sdb","keywords":["simpledb","aws-sdb","sdb"],"labels":["@aws-cdk/aws-sdb"],"assignees":["skinny85"]}, + {"area":"@aws-cdk/aws-secretsmanager","keywords":["secret","aws-secretsmanager","secrets-manager"],"labels":["@aws-cdk/aws-secretsmanager"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-securityhub","keywords":["aws-securityhub","security-hub"],"labels":["@aws-cdk/aws-securityhub"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-servicecatalog","keywords":["aws-servicecatalog","service-catalog"],"labels":["@aws-cdk/aws-servicecatalog"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-servicecatalogappregistry","keywords":["(aws-servicecatalogappregistry)","(servicecatalogappregistry)"],"labels":["@aws-cdk/aws-servicecatalogappregistry"],"assignees":["skinny85"]}, @@ -201,19 +242,19 @@ jobs: {"area":"@aws-cdk/aws-ses","keywords":["recipet-filter","reciept-rule","aws-ses","ses"],"labels":["@aws-cdk/aws-ses"],"assignees":["otaviomacedo"]}, {"area":"@aws-cdk/aws-ses-actions","keywords":["aws-ses-actions","ses-actions"],"labels":["@aws-cdk/aws-ses-actions"],"assignees":["otaviomacedo"]}, {"area":"@aws-cdk/aws-signer","keywords":["aws-signer","signer"],"labels":["@aws-cdk/aws-signer"],"assignees":["corymhall"]}, - {"area":"@aws-cdk/aws-sns","keywords":["simple-notification-service","aws-sns","sns","topic"],"labels":["@aws-cdk/aws-sns"],"assignees":["njlynch"]}, - {"area":"@aws-cdk/aws-sns-subscriptions","keywords":["aws-sns-subscriptions","sns-subscriptions","subscription"],"labels":["@aws-cdk/aws-sns-subscriptions"],"assignees":["njlynch"]}, - {"area":"@aws-cdk/aws-sqs","keywords":["queue","simple-queue-service","aws-sqs","sqs","fifo"],"labels":["@aws-cdk/aws-sqs"],"assignees":["njlynch"]}, - {"area":"@aws-cdk/aws-ssm","keywords":["aws-ssm","ssm","systems-manager","stringparameter","stringlistparameter"],"labels":["@aws-cdk/aws-ssm"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-sns","keywords":["simple-notification-service","aws-sns","sns","topic"],"labels":["@aws-cdk/aws-sns"],"assignees":["kaizen3031593"]}, + {"area":"@aws-cdk/aws-sns-subscriptions","keywords":["aws-sns-subscriptions","sns-subscriptions","subscription"],"labels":["@aws-cdk/aws-sns-subscriptions"],"assignees":["kaizen3031593"]}, + {"area":"@aws-cdk/aws-sqs","keywords":["queue","simple-queue-service","aws-sqs","sqs","fifo"],"labels":["@aws-cdk/aws-sqs"],"assignees":["otaviomacedo"]}, + {"area":"@aws-cdk/aws-ssm","keywords":["aws-ssm","ssm","systems-manager","stringparameter","stringlistparameter"],"labels":["@aws-cdk/aws-ssm"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-sso","keywords":["aws-sso","sso","single-sign-on"],"labels":["@aws-cdk/aws-sso"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-stepfunctions","keywords":["aws-stepfunctions","stepfunctions","state machine", "chain"],"labels":["@aws-cdk/aws-stepfunctions"],"assignees":["kaizen3031593"]}, {"area":"@aws-cdk/aws-stepfunctions-tasks","keywords":["aws-stepfunctions-tasks","stepfunctions-tasks"],"labels":["@aws-cdk/aws-stepfunctions-tasks"],"assignees":["kaizen3031593"]}, {"area":"@aws-cdk/aws-synthetics","keywords":["aws-synthetics","synthetics", "canary"],"labels":["@aws-cdk/aws-synthetics"],"assignees":["kaizen3031593"]}, {"area":"@aws-cdk/aws-timestream","keywords":["aws-timestream","timestream"],"labels":["@aws-cdk/aws-timestream"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-transfer","keywords":["aws-transfer","transfer"],"labels":["@aws-cdk/aws-transfer"],"assignees":["otaviomacedo"]}, - {"area":"@aws-cdk/aws-waf","keywords":["waf","web-application-firewall"],"labels":["@aws-cdk/aws-waf"],"assignees":["njlynch"]}, - {"area":"@aws-cdk/aws-wafregional","keywords":["wafregional","cfnwebacl"],"labels":["@aws-cdk/aws-wafregional"],"assignees":["njlynch"]}, - {"area":"@aws-cdk/aws-wafv2","keywords":["wafv2","aws-wafv2"],"labels":["@aws-cdk/aws-wafv2"],"assignees":["njlynch"]}, + {"area":"@aws-cdk/aws-waf","keywords":["waf","web-application-firewall"],"labels":["@aws-cdk/aws-waf"],"assignees":["skinny85"]}, + {"area":"@aws-cdk/aws-wafregional","keywords":["wafregional","cfnwebacl"],"labels":["@aws-cdk/aws-wafregional"],"assignees":["skinny85"]}, + {"area":"@aws-cdk/aws-wafv2","keywords":["wafv2","aws-wafv2"],"labels":["@aws-cdk/aws-wafv2"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-workspaces","keywords":["aws-workspaces","workspaces"],"labels":["@aws-cdk/aws-workspaces"],"assignees":["madeline-k"]}, {"area":"@aws-cdk/aws-xray","keywords":["(aws-xray)","(xray)"],"labels":["@aws-cdk/aws-xray"],"assignees":["corymhall"]}, {"area":"@aws-cdk/cfnspec","keywords":["cfn-spec"],"labels":["@aws-cdk/cfnspec"],"assignees":["rix0rrr"]}, @@ -224,11 +265,11 @@ jobs: {"area":"@aws-cdk/custom-resources","keywords":["custom-resource","provider"],"labels":["@aws-cdk/custom-resources"],"assignees":["rix0rrr"]}, {"area":"@aws-cdk/cx-api","keywords":["cx-api","cloudartifact","cloudassembly"],"labels":["@aws-cdk/cx-api"],"assignees":["rix0rrr"]}, {"area":"@aws-cdk/aws-lambda-layer-awscli","keywords":["(aws-lambda-layer-awscli)","(lambda-layer-awscli)"],"labels":["@aws-cdk/aws-lambda-layer-awscli"],"assignees":["rix0rrr"]}, - {"area":"@aws-cdk/aws-lambda-layer-kubectl","keywords":["(aws-lambda-layer-kubectl)","(lambda-layer-kubectl)"],"labels":["@aws-cdk/aws-lambda-layer-kubectl"],"assignees":["eladb"]}, + {"area":"@aws-cdk/aws-lambda-layer-kubectl","keywords":["(aws-lambda-layer-kubectl)","(lambda-layer-kubectl)"],"labels":["@aws-cdk/aws-lambda-layer-kubectl"],"assignees":["otaviomacedo"]}, {"area":"@aws-cdk/pipelines","keywords":["pipelines","cdk-pipelines","sourceaction","synthaction"],"labels":["@aws-cdk/pipelines"],"assignees":["rix0rrr"]}, {"area":"@aws-cdk/region-info","keywords":["region-info","fact"],"labels":["@aws-cdk/region-info"],"assignees":["skinny85"]}, - {"area":"aws-cdk-lib","keywords":["aws-cdk-lib","cdk-v2","v2","ubergen"],"labels":["aws-cdk-lib"],"assignees":["njlynch"]}, - {"area":"monocdk","keywords":["monocdk","monocdk-experiment"],"labels":["monocdk"],"assignees":["njlynch"]}, + {"area":"aws-cdk-lib","keywords":["aws-cdk-lib","cdk-v2","v2","ubergen"],"labels":["aws-cdk-lib"],"assignees":["madeline-k"]}, + {"area":"monocdk","keywords":["monocdk","monocdk-experiment"],"labels":["monocdk"],"assignees":["madeline-k"]}, {"area":"@aws-cdk/yaml-cfn","keywords":["(aws-yaml-cfn)","(yaml-cfn)"],"labels":["@aws-cdk/aws-yaml-cfn"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-apprunner","keywords":["apprunner","aws-apprunner"],"labels":["@aws-cdk/aws-apprunner"],"assignees":["corymhall"]}, {"area":"@aws-cdk/aws-lightsail","keywords":["lightsail","aws-lightsail"],"labels":["@aws-cdk/aws-lightsail"],"assignees":["corymhall"]}, diff --git a/.github/workflows/yarn-upgrade.yml b/.github/workflows/yarn-upgrade.yml index 6b8097ca1a580..f1ac15ec0831b 100644 --- a/.github/workflows/yarn-upgrade.yml +++ b/.github/workflows/yarn-upgrade.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@v2 - name: Set up Node - uses: actions/setup-node@v2.5.0 + uses: actions/setup-node@v2.5.1 with: node-version: 12 @@ -49,6 +49,7 @@ jobs: # We special-case typescript because it's not semantically versionned # We special-case constructs because we want to stay in control of the minimum compatible version # We special-case lerna because we have a patch on it that stops applying if Lerna upgrades. Remove this once https://github.com/lerna/lerna/pull/2874 releases. + # We special-case aws-sdk-mock because of breaking changes in type exports https://github.com/dwyl/aws-sdk-mock/pull/260. We are not respecting `@ts-ignore` run: |- # Upgrade dependencies at repository root ncu --upgrade --filter=@types/node,@types/fs-extra --target=minor @@ -57,7 +58,7 @@ jobs: # Upgrade all the packages lerna exec --parallel ncu -- --upgrade --filter=@types/node,@types/fs-extra --target=minor lerna exec --parallel ncu -- --upgrade --filter=typescript --target=patch - lerna exec --parallel ncu -- --upgrade --reject='@types/node,@types/fs-extra,constructs,typescript,aws-sdk,${{ steps.list-packages.outputs.list }}' --target=minor + lerna exec --parallel ncu -- --upgrade --reject='@types/node,@types/fs-extra,constructs,typescript,aws-sdk,aws-sdk-mock,${{ steps.list-packages.outputs.list }}' --target=minor # This will ensure the current lockfile is up-to-date with the dependency specifications (necessary for "yarn update" to run) - name: Run "yarn install" run: yarn install diff --git a/.mergify.yml b/.mergify.yml index 3348c3b934909..c2f007acd3d1d 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -10,7 +10,7 @@ pull_request_rules: label: add: [ contribution/core ] conditions: - - author~=^(eladb|RomainMuller|garnaat|nija-at|skinny85|rix0rrr|NGL321|Jerry-AWS|MrArnoldPalmer|NetaNir|iliapolo|njlynch|ericzbeard|ccfife|fulghum|pkandasamy91|SoManyHs|uttarasridhar|otaviomacedo|BenChaimberg|madeline-k|BryanPan342|kaizen3031593|comcalvi|Chriscbr|corymhall|peterwoodworth|ryparker)$ + - author~=^(eladb|RomainMuller|garnaat|nija-at|skinny85|rix0rrr|NGL321|Jerry-AWS|MrArnoldPalmer|NetaNir|iliapolo|njlynch|ericzbeard|ccfife|fulghum|pkandasamy91|SoManyHs|uttarasridhar|otaviomacedo|BenChaimberg|madeline-k|BryanPan342|kaizen3031593|comcalvi|Chriscbr|corymhall|peterwoodworth|ryparker|TheRealAmazonKendra)$ - -label~="contribution/core" - name: automatic merge actions: diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fe9975283c33..546daabd60989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,262 @@ 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.145.0](https://github.com/aws/aws-cdk/compare/v1.144.0...v1.145.0) (2022-02-18) + + +### Features + +* **aws-stepfunctions-tasks:** add environment property for SageMakerCreateTrainingJob ([#18976](https://github.com/aws/aws-cdk/issues/18976)) ([60d6e66](https://github.com/aws/aws-cdk/commit/60d6e66baef9d30db23e93b16f7c6d159ddf58c4)), closes [#18919](https://github.com/aws/aws-cdk/issues/18919) +* **cfnspec:** cloudformation spec v56.0.0 ([#18930](https://github.com/aws/aws-cdk/issues/18930)) ([24a52ae](https://github.com/aws/aws-cdk/commit/24a52ae1c250ec1875e64d6fc4ef8bec2f47399a)) +* **cfnspec:** cloudformation spec v57.0.0 ([#19030](https://github.com/aws/aws-cdk/issues/19030)) ([f0acbc4](https://github.com/aws/aws-cdk/commit/f0acbc469d835ad8808f4176eed53bf2af7c66e2)) +* **cli:** hotswap for appsync vtl mapping template changes ([#18881](https://github.com/aws/aws-cdk/issues/18881)) ([9858002](https://github.com/aws/aws-cdk/commit/985800228d04b9c2f3ac117e3b41c7f089547d38)) +* **codepipeline:** add support for CloudFormation StackSet actions ([#14225](https://github.com/aws/aws-cdk/issues/14225)) ([d8bc0d0](https://github.com/aws/aws-cdk/commit/d8bc0d08a9796724bb31cc5d7552cf99297678d9)) +* **config:** S3_BUCKET_LEVEL_PUBLIC_ACCESS_PROHIBITED managed rule ([#18890](https://github.com/aws/aws-cdk/issues/18890)) ([1a7e3e2](https://github.com/aws/aws-cdk/commit/1a7e3e20e005b4165a27506615c7245b88ce998b)), closes [#18888](https://github.com/aws/aws-cdk/issues/18888) +* **core:** stack synthesizer that uses CLI credentials ([#18963](https://github.com/aws/aws-cdk/issues/18963)) ([a36b72b](https://github.com/aws/aws-cdk/commit/a36b72b5045fceada7c96d00770d8c48f2ca1415)), closes [#16888](https://github.com/aws/aws-cdk/issues/16888) +* **ec2:** allow imdsv2 usage on bastion host ([#18955](https://github.com/aws/aws-cdk/issues/18955)) ([8c6777c](https://github.com/aws/aws-cdk/commit/8c6777c904588f9b911d8b8a5d63a65ae1c7aad9)) +* **ecs:** support version stages and ids for Secrets ([#18174](https://github.com/aws/aws-cdk/issues/18174)) ([6d091c2](https://github.com/aws/aws-cdk/commit/6d091c2da7749a81c3752953d0bc7db65ab48f45)), closes [#18123](https://github.com/aws/aws-cdk/issues/18123) +* **events:** API Destinations ([#13729](https://github.com/aws/aws-cdk/issues/13729)) ([2adbc14](https://github.com/aws/aws-cdk/commit/2adbc14bae8266a6bd357e752185133a32e4ca87)) +* **iot-actions:** add SNS publish action ([#18839](https://github.com/aws/aws-cdk/issues/18839)) ([3a39f6b](https://github.com/aws/aws-cdk/commit/3a39f6bf34eb428c527db1c614ed682c582821fb)), closes [#17700](https://github.com/aws/aws-cdk/issues/17700) +* **iotevents:** create new module for IoT Events actions ([#18956](https://github.com/aws/aws-cdk/issues/18956)) ([3533ea9](https://github.com/aws/aws-cdk/commit/3533ea9cb7ec7fd9e230abd27556a87d3559bdb8)), closes [/github.com/aws/aws-cdk/pull/18869#discussion_r802719713](https://github.com/aws//github.com/aws/aws-cdk/pull/18869/issues/discussion_r802719713) +* **lambda:** allow Topic to be dlq for Lambda ([#18546](https://github.com/aws/aws-cdk/issues/18546)) ([f8d8fe4](https://github.com/aws/aws-cdk/commit/f8d8fe4e1397e3d8da91a3a44f025475c8b7f592)), closes [#16246](https://github.com/aws/aws-cdk/issues/16246) +* **logs:** custom Role for Kinesis destination ([#13553](https://github.com/aws/aws-cdk/issues/13553)) ([bb96621](https://github.com/aws/aws-cdk/commit/bb96621d642fedcf1e22086a249034ca1ab63f73)), closes [#7661](https://github.com/aws/aws-cdk/issues/7661) +* **rds:** simpler way to configure parameters for instance and cluster ([#18126](https://github.com/aws/aws-cdk/issues/18126)) ([3ba9088](https://github.com/aws/aws-cdk/commit/3ba90881dab49f47220872e6e5afef3a7732ef13)), closes [#18124](https://github.com/aws/aws-cdk/issues/18124) +* **s3-deployment:** add `deployedBucket` attribute for sequencing ([#15384](https://github.com/aws/aws-cdk/issues/15384)) ([edac101](https://github.com/aws/aws-cdk/commit/edac1011574f3cf38bb0ac39400bf41c66337ffd)) + + +### Bug Fixes + +* **assertions:** 'pattern.indexOf' is not a function ([#19009](https://github.com/aws/aws-cdk/issues/19009)) ([6df26e7](https://github.com/aws/aws-cdk/commit/6df26e7ed73455b77b07707debef5bb26ae78909)) +* **assertions:** incorrect assertions when >1 messages on a resource ([#18948](https://github.com/aws/aws-cdk/issues/18948)) ([072e1b9](https://github.com/aws/aws-cdk/commit/072e1b990a43768b88a05dd436dd6d6d9649c13a)), closes [#18840](https://github.com/aws/aws-cdk/issues/18840) +* **aws-cdk:** include nested stacks when building changesets ([#17396](https://github.com/aws/aws-cdk/issues/17396)) ([a7dbeef](https://github.com/aws/aws-cdk/commit/a7dbeef9eae3e00e209d06f5cc5bb3bf3d084d18)), closes [#5722](https://github.com/aws/aws-cdk/issues/5722) +* **cli:** handle attributes of AWS::Events::EventBus when hotswapping ([#18834](https://github.com/aws/aws-cdk/issues/18834)) ([a30a32a](https://github.com/aws/aws-cdk/commit/a30a32aaa5dfb764022370fe7867564d57640bfb)), closes [#18831](https://github.com/aws/aws-cdk/issues/18831) +* **core:** undeployable due to invalid mapping ([#18922](https://github.com/aws/aws-cdk/issues/18922)) ([db28485](https://github.com/aws/aws-cdk/commit/db28485f4d2ea243e4184dd06b52395b4980beba)), closes [#18789](https://github.com/aws/aws-cdk/issues/18789) [#18789](https://github.com/aws/aws-cdk/issues/18789) +* **lambda:** unlock use case for cross-account functions w/ preconfigured permissions ([#18979](https://github.com/aws/aws-cdk/issues/18979)) ([023108a](https://github.com/aws/aws-cdk/commit/023108ac080ba34c82ef0b60fee20014c4a78428)), closes [#18228](https://github.com/aws/aws-cdk/issues/18228) [#18781](https://github.com/aws/aws-cdk/issues/18781) [#18967](https://github.com/aws/aws-cdk/issues/18967) [#18781](https://github.com/aws/aws-cdk/issues/18781) +* **lambda:** Validate Lambda "functionName" parameter ([#17970](https://github.com/aws/aws-cdk/issues/17970)) ([a416a2d](https://github.com/aws/aws-cdk/commit/a416a2d68f14c0711d42b38e81b0091d160dfd6f)), closes [#13264](https://github.com/aws/aws-cdk/issues/13264) +* **pipelines:** self-mutate always adds analytics ([#19010](https://github.com/aws/aws-cdk/issues/19010)) ([bc47b29](https://github.com/aws/aws-cdk/commit/bc47b2937a806d6522a4d9106976200bf6810024)), closes [#18933](https://github.com/aws/aws-cdk/issues/18933) +* **stepfunctions:** imported State Machine sill has region and account from its Stack, instead of its ARN ([#19026](https://github.com/aws/aws-cdk/issues/19026)) ([23329b4](https://github.com/aws/aws-cdk/commit/23329b4ac7c845efe7d0e0d7ce03499e7dd723ac)), closes [#17982](https://github.com/aws/aws-cdk/issues/17982) +* python3 version check with Python 3.10 ([#18754](https://github.com/aws/aws-cdk/issues/18754)) ([0ef6527](https://github.com/aws/aws-cdk/commit/0ef65279cc5f2269046e0bae05d44f5aabc43eb9)) +* **stepfunctions-tasks:** EMR Create Cluster does not support dynamic allocation of step concurrency level ([#18972](https://github.com/aws/aws-cdk/issues/18972)) ([d19e538](https://github.com/aws/aws-cdk/commit/d19e5386f737aa58f27c7ac2082306006dcd6d95)) +* **synthetics:** generated role has incorrect permissions for cloudwatch logs ([#18946](https://github.com/aws/aws-cdk/issues/18946)) ([f8bb85f](https://github.com/aws/aws-cdk/commit/f8bb85fad8f659a2b72d5d05d7a94c97765a76f8)), closes [#18910](https://github.com/aws/aws-cdk/issues/18910) + +## [1.144.0](https://github.com/aws/aws-cdk/compare/v1.143.0...v1.144.0) (2022-02-08) + + +### Features + +* **assets:** support networking mode for DockerImageAsset ([#18114](https://github.com/aws/aws-cdk/issues/18114)) ([a7b39f5](https://github.com/aws/aws-cdk/commit/a7b39f527976e29a7f39c1ba1813efba2e0aa209)), closes [#15516](https://github.com/aws/aws-cdk/issues/15516) +* **cfnspec:** cloudformation spec v55.0.0 ([#18827](https://github.com/aws/aws-cdk/issues/18827)) ([a1d94b3](https://github.com/aws/aws-cdk/commit/a1d94b3624eb1b6b543d8ce209ec85af8e85beda)) +* **cli:** `cdk diff` works for Nested Stacks ([#18207](https://github.com/aws/aws-cdk/issues/18207)) ([1337b24](https://github.com/aws/aws-cdk/commit/1337b247e82d9462074416623e665cf9526d2cc0)), closes [#5722](https://github.com/aws/aws-cdk/issues/5722) +* **iotevents:** add grant method to Input class ([#18617](https://github.com/aws/aws-cdk/issues/18617)) ([e89688e](https://github.com/aws/aws-cdk/commit/e89688ec1dd7a3b072d23287cddcb73bccc16fd4)) +* **iotevents:** support transition events ([#18768](https://github.com/aws/aws-cdk/issues/18768)) ([ccc1988](https://github.com/aws/aws-cdk/commit/ccc198864f92620857da09c68013123e9cd3f01d)), closes [#17711](https://github.com/aws/aws-cdk/issues/17711) +* **s3-deployment:** deploy data with deploy-time values ([#18659](https://github.com/aws/aws-cdk/issues/18659)) ([d40e332](https://github.com/aws/aws-cdk/commit/d40e332578f7590a0c949fdd01622a644cf9359b)), closes [#12903](https://github.com/aws/aws-cdk/issues/12903) + + +### Bug Fixes + +* **aws-appsync:** Strip unsupported characters from Lambda DataSource ([#18765](https://github.com/aws/aws-cdk/issues/18765)) ([bb8d6f6](https://github.com/aws/aws-cdk/commit/bb8d6f6bf5941b76ef0590c99fe8e26440e09c18)) +* **tooling:** update vscode devcontainer image ([#18455](https://github.com/aws/aws-cdk/issues/18455)) ([28647f7](https://github.com/aws/aws-cdk/commit/28647f7105da6bd02975aa7d90300d77fe85d0e6)) + +## [1.143.0](https://github.com/aws/aws-cdk/compare/v1.142.0...v1.143.0) (2022-02-02) + + +### Features + +* **amplify:** support performance mode in Branch ([#18598](https://github.com/aws/aws-cdk/issues/18598)) ([bdeb8eb](https://github.com/aws/aws-cdk/commit/bdeb8eb604f5012ce3180d2f6d887fed1834e4f4)), closes [#18557](https://github.com/aws/aws-cdk/issues/18557) +* **cfnspec:** cloudformation spec v54.0.0 ([#18764](https://github.com/aws/aws-cdk/issues/18764)) ([71601c1](https://github.com/aws/aws-cdk/commit/71601c115a6460b4532a34c83100ae70a476fad2)) +* **cloudwatch-actions:** add ssm opsitem action for cloudwatch alarm ([#16923](https://github.com/aws/aws-cdk/issues/16923)) ([9380885](https://github.com/aws/aws-cdk/commit/93808851415bff269418f28d9de3c61727e143d3)), closes [#16861](https://github.com/aws/aws-cdk/issues/16861) +* **dynamodb:** allow setting TableClass for a Table ([#18719](https://github.com/aws/aws-cdk/issues/18719)) ([73a889e](https://github.com/aws/aws-cdk/commit/73a889eba85d0aa542ac96a1124f3ae4f1d351bc)), closes [#18718](https://github.com/aws/aws-cdk/issues/18718) +* **ec2:** support KMS keys for block device mappings for both instances and launch templates ([#18326](https://github.com/aws/aws-cdk/issues/18326)) ([17dbe5f](https://github.com/aws/aws-cdk/commit/17dbe5f476ac1ccc0c0e6a0905b0de5ae6186704)), closes [#18309](https://github.com/aws/aws-cdk/issues/18309) +* **ecr:** add server-side encryption configuration ([#16966](https://github.com/aws/aws-cdk/issues/16966)) ([c46acd5](https://github.com/aws/aws-cdk/commit/c46acd5f13442c43d0c2ed339e3091dd46002741)), closes [#15400](https://github.com/aws/aws-cdk/issues/15400) [#15571](https://github.com/aws/aws-cdk/issues/15571) +* **ecs:** expose image name in container definition ([#17793](https://github.com/aws/aws-cdk/issues/17793)) ([1947d7c](https://github.com/aws/aws-cdk/commit/1947d7cc809fda0765bee3dbb2286190ec2847f7)) +* **fsx:** add support for FSx Lustre Persistent_2 deployment type ([#18626](https://github.com/aws/aws-cdk/issues/18626)) ([6036d99](https://github.com/aws/aws-cdk/commit/6036d9927bb3607e31a57361bf304976ff1891f7)) +* **iot:** add Action to republish MQTT messages to another MQTT topic ([#18661](https://github.com/aws/aws-cdk/issues/18661)) ([7ac1215](https://github.com/aws/aws-cdk/commit/7ac121546776cae972bbfb89c2a11949762e7c47)) + + +### Bug Fixes + +* **core:** correctly reference versionless secure parameters ([#18730](https://github.com/aws/aws-cdk/issues/18730)) ([9f6e10e](https://github.com/aws/aws-cdk/commit/9f6e10ed0a751c06fe0cc1d79f38d5fb4b686087)), closes [#18729](https://github.com/aws/aws-cdk/issues/18729) +* **ec2:** `UserData.addSignalOnExitCommand` does not work in combination with `userDataCausesReplacement` ([#18726](https://github.com/aws/aws-cdk/issues/18726)) ([afdc550](https://github.com/aws/aws-cdk/commit/afdc550ee372dd25d9d2eef81a545da1e923f796)), closes [#12749](https://github.com/aws/aws-cdk/issues/12749) +* **vpc:** Vpc.fromLookup should throw if subnet group name tag is explicitly given and does not exist ([#18714](https://github.com/aws/aws-cdk/issues/18714)) ([13e1c7f](https://github.com/aws/aws-cdk/commit/13e1c7f10b81fc350953fe69fcccb61ff5aa9c1e)), closes [#13962](https://github.com/aws/aws-cdk/issues/13962) + + +### Reverts + +* "chore(cloudfront): encryption and enforceSSL on distribution s3 loggingBucket ([#18264](https://github.com/aws/aws-cdk/issues/18264))" ([#18772](https://github.com/aws/aws-cdk/issues/18772)) ([121e4a1](https://github.com/aws/aws-cdk/commit/121e4a1dec13d31644f6176d0a1d703952dc1ba3)), closes [#18271](https://github.com/aws/aws-cdk/issues/18271) [/docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AWS-logs-and-resource-policy.html#AWS-logs-infrastructure-S3](https://github.com/aws//docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AWS-logs-and-resource-policy.html/issues/AWS-logs-infrastructure-S3) [#18676](https://github.com/aws/aws-cdk/issues/18676) +* "chore(ec2): enforceSSL on flowLog s3 bucket ([#18271](https://github.com/aws/aws-cdk/issues/18271))" ([#18770](https://github.com/aws/aws-cdk/issues/18770)) ([a2eb092](https://github.com/aws/aws-cdk/commit/a2eb092b2b468bffa2acde9b98ca34cefa3e48f1)), closes [/docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AWS-logs-and-resource-policy.html#AWS-logs-infrastructure-S3](https://github.com/aws//docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AWS-logs-and-resource-policy.html/issues/AWS-logs-infrastructure-S3) [#18676](https://github.com/aws/aws-cdk/issues/18676) + +## [1.142.0](https://github.com/aws/aws-cdk/compare/v1.141.0...v1.142.0) (2022-01-28) + + +### Features + +* **cfnspec:** cloudformation spec v53.1.0 ([#18680](https://github.com/aws/aws-cdk/issues/18680)) ([f385059](https://github.com/aws/aws-cdk/commit/f38505911a3e140a9cb6b269bdf22abe9803c515)) +* **cloudfront-origins:** extend `readTimeout` maximum value for `HttpOriginProps` ([#18697](https://github.com/aws/aws-cdk/issues/18697)) ([e64de67](https://github.com/aws/aws-cdk/commit/e64de677cdfc014f68e92b204f4728e60a8bb111)), closes [#18628](https://github.com/aws/aws-cdk/issues/18628) +* **eks:** cluster logging ([#18112](https://github.com/aws/aws-cdk/issues/18112)) ([872277b](https://github.com/aws/aws-cdk/commit/872277b9e853dbf5f2cac84b5afb6d26e0ed5659)), closes [#4159](https://github.com/aws/aws-cdk/issues/4159) +* **iotevents:** allow setting description, evaluation method and key of DetectorModel ([#18644](https://github.com/aws/aws-cdk/issues/18644)) ([2eeaebc](https://github.com/aws/aws-cdk/commit/2eeaebc3cdc9c5c7ef3fa312b3d1abca265dcbb6)) +* **lambda-python:** support setting environment vars for bundling ([#18635](https://github.com/aws/aws-cdk/issues/18635)) ([30e2233](https://github.com/aws/aws-cdk/commit/30e223333fef0b0d7f12287dab170a34e092d7fa)) + + +### Bug Fixes + +* **aws-lambda-nodejs:** pre compilation with tsc is not being run ([#18062](https://github.com/aws/aws-cdk/issues/18062)) ([7ac7221](https://github.com/aws/aws-cdk/commit/7ac7221aff3c612ab80e7812c371b11c56e5db0a)), closes [#18002](https://github.com/aws/aws-cdk/issues/18002) +* **pipelines:** undeployable due to dependency cycle ([#18686](https://github.com/aws/aws-cdk/issues/18686)) ([009d689](https://github.com/aws/aws-cdk/commit/009d68912267de9dcf4136a7d80a652a891b7bb9)), closes [#18492](https://github.com/aws/aws-cdk/issues/18492) [#18673](https://github.com/aws/aws-cdk/issues/18673) + +## [1.141.0](https://github.com/aws/aws-cdk/compare/v1.140.0...v1.141.0) (2022-01-27) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **servicecatalog:** `TagOptions` now have `scope` and `props` argument in constructor, and data is now passed via a `allowedValueForTags` field in props + +### Features + +* **assertions:** support assertions on stack messages ([#18521](https://github.com/aws/aws-cdk/issues/18521)) ([cb86e30](https://github.com/aws/aws-cdk/commit/cb86e30391aefdda13e6b0d4b3be2fedf76477c8)), closes [#18347](https://github.com/aws/aws-cdk/issues/18347) +* **assertions:** support for conditions ([#18577](https://github.com/aws/aws-cdk/issues/18577)) ([55ff1b2](https://github.com/aws/aws-cdk/commit/55ff1b2e69f1b42bbbecd9dc95e17f2ffc35f94e)), closes [#18560](https://github.com/aws/aws-cdk/issues/18560) +* **aws-ecs-patterns:** adding support for custom HealthCheck while creating QueueProcessingFargateService ([#18219](https://github.com/aws/aws-cdk/issues/18219)) ([0ca81a1](https://github.com/aws/aws-cdk/commit/0ca81a118d3d54b87d2d05a53fb72e4efe03b591)), closes [#15636](https://github.com/aws/aws-cdk/issues/15636) +* **certificatemanager:** DnsValidatedCertificate DNS record cleanup ([#18311](https://github.com/aws/aws-cdk/issues/18311)) ([36d356d](https://github.com/aws/aws-cdk/commit/36d356d0b3e422f7451f4b0dd2f971aa0378210e)), closes [#3333](https://github.com/aws/aws-cdk/issues/3333) [#7063](https://github.com/aws/aws-cdk/issues/7063) +* **cfnspec:** cloudformation spec v53.1.0 ([#18588](https://github.com/aws/aws-cdk/issues/18588)) ([a283a48](https://github.com/aws/aws-cdk/commit/a283a482dead64e94383ba21cc7908f10c4459a2)) +* **cfnspec:** cloudformation spec v53.1.0 ([#18658](https://github.com/aws/aws-cdk/issues/18658)) ([2eda19e](https://github.com/aws/aws-cdk/commit/2eda19e510374426190531810cff518d582644ad)) +* **ec2:** session timeout and login banner for client vpn endpoint ([#18590](https://github.com/aws/aws-cdk/issues/18590)) ([7294118](https://github.com/aws/aws-cdk/commit/72941180a7188e5560a58f1509554ef038544ec4)) +* **ecs:** add `BaseService.fromServiceArnWithCluster()` for use in CodePipeline ([#18530](https://github.com/aws/aws-cdk/issues/18530)) ([3d192a9](https://github.com/aws/aws-cdk/commit/3d192a9a832857cb246d719a68b4b8f40d807fed)) +* **iotevents:** add DetectorModel L2 Construct ([#18049](https://github.com/aws/aws-cdk/issues/18049)) ([d0960f1](https://github.com/aws/aws-cdk/commit/d0960f181e5f66daa1eb53be2190b7e62bd66030)), closes [#17711](https://github.com/aws/aws-cdk/issues/17711) [#17711](https://github.com/aws/aws-cdk/issues/17711) +* **lambda-nodejs:** Allow setting mainFields for esbuild ([#18569](https://github.com/aws/aws-cdk/issues/18569)) ([0e78aeb](https://github.com/aws/aws-cdk/commit/0e78aeb9ad62226e67f72f23c0008ba749b3a73b)) +* **s3:** custom role for the bucket notifications handler ([#17794](https://github.com/aws/aws-cdk/issues/17794)) ([43f232d](https://github.com/aws/aws-cdk/commit/43f232ddc0a18e9a2fada2fbead758ab3538adc2)), closes [#9918](https://github.com/aws/aws-cdk/issues/9918) [#13241](https://github.com/aws/aws-cdk/issues/13241) +* **servicecatalog:** Create TagOptions Construct ([#18314](https://github.com/aws/aws-cdk/issues/18314)) ([903c4b6](https://github.com/aws/aws-cdk/commit/903c4b6e4adf676fae42265a048dddd0e1386542)), closes [#17753](https://github.com/aws/aws-cdk/issues/17753) + + +### Bug Fixes + +* **apigatewayv2:** websocket api: allow all methods in grant manage connections ([#18544](https://github.com/aws/aws-cdk/issues/18544)) ([41c8a3f](https://github.com/aws/aws-cdk/commit/41c8a3fa6b50a94affb65286d862056050d02e84)), closes [#18410](https://github.com/aws/aws-cdk/issues/18410) +* **aws-apigateway:** cross region authorizer ref ([#18444](https://github.com/aws/aws-cdk/issues/18444)) ([0e0a092](https://github.com/aws/aws-cdk/commit/0e0a0922ba1d538abdfeb61a260c262109115038)) +* **cli:** hotswap should wait for lambda's `updateFunctionCode` to complete ([#18536](https://github.com/aws/aws-cdk/issues/18536)) ([0e08eeb](https://github.com/aws/aws-cdk/commit/0e08eebd2f13ab0da6cac7b91288845cad530192)), closes [#18386](https://github.com/aws/aws-cdk/issues/18386) [#18386](https://github.com/aws/aws-cdk/issues/18386) +* **ecs:** only works in 'aws' partition ([#18496](https://github.com/aws/aws-cdk/issues/18496)) ([525ac07](https://github.com/aws/aws-cdk/commit/525ac07369e33e2f36b7a0eea7913e43649484db)), closes [#18429](https://github.com/aws/aws-cdk/issues/18429) +* **ecs-patterns:** Fix Network Load Balancer Port assignments in ECS Patterns ([#18157](https://github.com/aws/aws-cdk/issues/18157)) ([1393729](https://github.com/aws/aws-cdk/commit/13937299596d0b858d56e9116bf7a7dbe039d4b4)), closes [#18073](https://github.com/aws/aws-cdk/issues/18073) +* **elasticloadbalancingv2:** ApplicationLoadBalancer.logAccessLogs does not grant all necessary permissions ([#18558](https://github.com/aws/aws-cdk/issues/18558)) ([bde1795](https://github.com/aws/aws-cdk/commit/bde17950293309b7449fc412301634770b47111f)), closes [#18367](https://github.com/aws/aws-cdk/issues/18367) +* **pipelines:** CodeBuild projects are hard to tell apart ([#18492](https://github.com/aws/aws-cdk/issues/18492)) ([f6dab8d](https://github.com/aws/aws-cdk/commit/f6dab8d8c5aa4cf56d6846e2d13c1d5641136f72)) +* **region-info:** incorrect codedeploy service principals ([#18505](https://github.com/aws/aws-cdk/issues/18505)) ([16db963](https://github.com/aws/aws-cdk/commit/16db9639e86f1fd6f26a1054f4d6df24801d0f05)) +* **route53:** add RoutingControlArn to HealthCheck patch ([#18645](https://github.com/aws/aws-cdk/issues/18645)) ([c58e8bb](https://github.com/aws/aws-cdk/commit/c58e8bbbcb0a66c37b65cddc1da8d19dfbf26b4f)), closes [#18570](https://github.com/aws/aws-cdk/issues/18570) +* **s3:** add missing safe actions to `grantWrite`, `grantReadWrite` and `grantPut` methods ([#18494](https://github.com/aws/aws-cdk/issues/18494)) ([940d043](https://github.com/aws/aws-cdk/commit/940d0439cd347f06d755f3e3dd0582470749f710)), closes [#13616](https://github.com/aws/aws-cdk/issues/13616) +* **secretsmanager:** SecretRotation for secret imported by name has incorrect permissions ([#18567](https://github.com/aws/aws-cdk/issues/18567)) ([9ed263c](https://github.com/aws/aws-cdk/commit/9ed263cde0b41959ff267720c0978bfe7449337a)), closes [#18424](https://github.com/aws/aws-cdk/issues/18424) +* **stepfunctions:** task token integration cannot be used with API Gateway ([#18595](https://github.com/aws/aws-cdk/issues/18595)) ([678eede](https://github.com/aws/aws-cdk/commit/678eeded5d5631dbacff43ead697ecbd3bd4b27d)), closes [#14184](https://github.com/aws/aws-cdk/issues/14184) [#14181](https://github.com/aws/aws-cdk/issues/14181) +* **stepfunctions-tasks:** cluster creation fails with unresolved release labels ([#18288](https://github.com/aws/aws-cdk/issues/18288)) ([9940952](https://github.com/aws/aws-cdk/commit/9940952d67bdf07f3d737dc88676dc7f7c435a12)) +* **synthetics:** correct getbucketlocation policy ([#13573](https://github.com/aws/aws-cdk/issues/13573)) ([e743525](https://github.com/aws/aws-cdk/commit/e743525b6379371110d737bb360f637c41d30ca1)), closes [#13572](https://github.com/aws/aws-cdk/issues/13572) + +## [1.140.0](https://github.com/aws/aws-cdk/compare/v1.139.0...v1.140.0) (2022-01-20) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **apigatewayv2:** `HttpIntegrationType.LAMBDA_PROXY` has been renamed to `HttpIntegrationType.AWS_PROXY` +* **iot:** the class `FirehoseStreamAction` has been renamed to `FirehosePutRecordAction` + +### Features + +* **apigatewayv2:** HttpRouteIntegration supports AWS services integrations ([#18154](https://github.com/aws/aws-cdk/issues/18154)) ([a8094c7](https://github.com/aws/aws-cdk/commit/a8094c7d9970557077f560ccd24882216094ee3c)), closes [#16287](https://github.com/aws/aws-cdk/issues/16287) +* **apigatewayv2:** support for mock integration type ([#18129](https://github.com/aws/aws-cdk/issues/18129)) ([7779c14](https://github.com/aws/aws-cdk/commit/7779c147c7445d9e8ccafa9b732521c9021a6234)), closes [#15008](https://github.com/aws/aws-cdk/issues/15008) +* **apigatewayv2:** websocket api: api keys ([#16636](https://github.com/aws/aws-cdk/issues/16636)) ([24f8f74](https://github.com/aws/aws-cdk/commit/24f8f74ebec023f5e3f5bd2bdfc89575a53b38f3)) +* **assertions:** `stringLikeRegexp()` matcher ([#18491](https://github.com/aws/aws-cdk/issues/18491)) ([b49b002](https://github.com/aws/aws-cdk/commit/b49b002e40f5b901935f52827f417bb3851badc2)) +* **assertions:** support for parameters ([#18469](https://github.com/aws/aws-cdk/issues/18469)) ([d0d6fc5](https://github.com/aws/aws-cdk/commit/d0d6fc520491351b44cac78aa90284c82a9499b2)), closes [#16720](https://github.com/aws/aws-cdk/issues/16720) +* **aws-neptune:** add autoMinorVersionUpgrade to cluster props ([#18394](https://github.com/aws/aws-cdk/issues/18394)) ([8b5320a](https://github.com/aws/aws-cdk/commit/8b5320ac5e5c320db46bc74f33b3841977dd3a5d)), closes [#17545](https://github.com/aws/aws-cdk/issues/17545) +* **aws-s3:** support number of newer versions to retain in lifecycle policy ([#18225](https://github.com/aws/aws-cdk/issues/18225)) ([e1731b1](https://github.com/aws/aws-cdk/commit/e1731b11c9417a9a4d6cf0f2089c62a721e8d074)), closes [#17996](https://github.com/aws/aws-cdk/issues/17996) [#17996](https://github.com/aws/aws-cdk/issues/17996) +* **cfnspec:** cloudformation spec v53.0.0 ([#18468](https://github.com/aws/aws-cdk/issues/18468)) ([50637e0](https://github.com/aws/aws-cdk/commit/50637e08590c2051d9a1e446ee7ded47e85d02b3)) +* **cfnspec:** cloudformation spec v53.0.0 ([#18480](https://github.com/aws/aws-cdk/issues/18480)) ([38e1fe4](https://github.com/aws/aws-cdk/commit/38e1fe42d8b30d6afaf4a3ccc90dd15d6a5d8255)) +* **cfnspec:** cloudformation spec v53.0.0 ([#18524](https://github.com/aws/aws-cdk/issues/18524)) ([517d517](https://github.com/aws/aws-cdk/commit/517d517a0bb3f7f6e98538dca736086b86b206c8)) +* **cfnspec:** cloudformation spec v53.0.0 ([#18551](https://github.com/aws/aws-cdk/issues/18551)) ([926310b](https://github.com/aws/aws-cdk/commit/926310bace65a763972d56c0df5730cdc44f8f82)) +* **cli:** support hotswapping Lambda functions that use Docker images ([#18319](https://github.com/aws/aws-cdk/issues/18319)) ([6b553b7](https://github.com/aws/aws-cdk/commit/6b553b7f84e5cde8a1fc533af95ad440c020e834)), closes [#18302](https://github.com/aws/aws-cdk/issues/18302) [#18408](https://github.com/aws/aws-cdk/issues/18408) +* **cli:** support hotswapping Lambda functions with inline code ([#18408](https://github.com/aws/aws-cdk/issues/18408)) ([d0b8512](https://github.com/aws/aws-cdk/commit/d0b8512449759bf74bb53aabbb6d5224b5f8c5ae)), closes [#18319](https://github.com/aws/aws-cdk/issues/18319) +* **cli:** watch streams resources' CloudWatch logs to the terminal ([#18159](https://github.com/aws/aws-cdk/issues/18159)) ([a9038ae](https://github.com/aws/aws-cdk/commit/a9038ae9c7d9b15b89ae24cfa24aefa6012674bc)), closes [#18122](https://github.com/aws/aws-cdk/issues/18122) +* **cognito:** identity pools ([#16190](https://github.com/aws/aws-cdk/issues/16190)) ([59fe395](https://github.com/aws/aws-cdk/commit/59fe395a5adcd35bd59c6d9c74f4a2606aec88b0)) +* **ec2:** add Hpc6a instances ([#18445](https://github.com/aws/aws-cdk/issues/18445)) ([c7f39ca](https://github.com/aws/aws-cdk/commit/c7f39ca97874c1d8d5286ab347a97fc458547830)) +* **ec2:** add support for al2022 and amzn2 with kernel 5.x ([#18117](https://github.com/aws/aws-cdk/issues/18117)) ([6b73d1d](https://github.com/aws/aws-cdk/commit/6b73d1d3d0ac05042c1e43a64068938138fe8421)) +* **ec2:** create Peers via security group ids ([#18248](https://github.com/aws/aws-cdk/issues/18248)) ([9d1b2c7](https://github.com/aws/aws-cdk/commit/9d1b2c7b1f0147089f912c32a61d7ba86edb543c)), closes [#7111](https://github.com/aws/aws-cdk/issues/7111) +* **ecs-service-extensions:** Enable default logging to CloudWatch for extensions (under feature flag) ([#17817](https://github.com/aws/aws-cdk/issues/17817)) ([06666f4](https://github.com/aws/aws-cdk/commit/06666f4727b9745d001bc20f027b535538bb8250)) +* **iot:** add Action to put record to Kinesis Data stream ([#18321](https://github.com/aws/aws-cdk/issues/18321)) ([1480213](https://github.com/aws/aws-cdk/commit/1480213a032549ab7319e0c3a66e02e9b6a9c4ab)), closes [#17703](https://github.com/aws/aws-cdk/issues/17703) +* **lambda-nodejs:** ES modules ([#18346](https://github.com/aws/aws-cdk/issues/18346)) ([e23b63f](https://github.com/aws/aws-cdk/commit/e23b63fc106c4781e3dd39a16d4a3e3c81bdd874)), closes [#13274](https://github.com/aws/aws-cdk/issues/13274) +* **opensearch:** added opensearch 1.1 to engineversion ([#18432](https://github.com/aws/aws-cdk/issues/18432)) ([e01a57a](https://github.com/aws/aws-cdk/commit/e01a57aa3085a8282123afbc3583b1b78a075c9a)), closes [#18431](https://github.com/aws/aws-cdk/issues/18431) + + +### Bug Fixes + +* **apigateway:** `enabled` property of `ApiKeyProps` is ignored ([#18407](https://github.com/aws/aws-cdk/issues/18407)) ([c31f9b4](https://github.com/aws/aws-cdk/commit/c31f9b44165f872f8dd51605e00f4801ed611d4d)) +* **applicationautoscaling:** typo in `DYANMODB_WRITE_CAPACITY_UTILIZATION` ([#18085](https://github.com/aws/aws-cdk/issues/18085)) ([626e6aa](https://github.com/aws/aws-cdk/commit/626e6aa1a27feffe7ce60a46a6fdcf26f317eaef)), closes [#17209](https://github.com/aws/aws-cdk/issues/17209) +* **assertions:** object partiality is dropped passing through arrays ([#18525](https://github.com/aws/aws-cdk/issues/18525)) ([eb29e6f](https://github.com/aws/aws-cdk/commit/eb29e6ff0308eb320ec772cc35cdbf781168198e)) +* **cli:** `cdk watch` constantly prints 'messages suppressed' ([#18486](https://github.com/aws/aws-cdk/issues/18486)) ([9b266f4](https://github.com/aws/aws-cdk/commit/9b266f49643d058709771892f908f1c2ae248f95)), closes [#18451](https://github.com/aws/aws-cdk/issues/18451) +* **cli:** warning to upgrade to bootstrap version >= undefined ([#18489](https://github.com/aws/aws-cdk/issues/18489)) ([da5a305](https://github.com/aws/aws-cdk/commit/da5a305875f0b82b896861be3fcb12fddaa0cc7b)) +* **ec2:** interface endpoints do not work with `Vpc.fromLookup()` ([#18554](https://github.com/aws/aws-cdk/issues/18554)) ([f55cd2b](https://github.com/aws/aws-cdk/commit/f55cd2bd86405cc61d3eb24c2b827c2cd133363d)), closes [#17600](https://github.com/aws/aws-cdk/issues/17600) +* **ec2:** launch template names in imdsv2 not unique across stacks (under feature flag) ([#17766](https://github.com/aws/aws-cdk/issues/17766)) ([2a80e4b](https://github.com/aws/aws-cdk/commit/2a80e4b113bac0716f5aa1d4806e425759da1743)) +* **ecs:** respect LogGroup's region for aws-log-driver ([#18212](https://github.com/aws/aws-cdk/issues/18212)) ([b6e3e51](https://github.com/aws/aws-cdk/commit/b6e3e517ac42b7951bc4ca4c1fd62422e3b49092)), closes [#17747](https://github.com/aws/aws-cdk/issues/17747) +* **elbv2:** BaseLoadBalancer.vpc is not optional ([#18474](https://github.com/aws/aws-cdk/issues/18474)) ([f511c17](https://github.com/aws/aws-cdk/commit/f511c17aac8ca4d3fa94ace051d9946dc23f40a3)), closes [aws/jsii#3342](https://github.com/aws/jsii/issues/3342) +* **iot:** `FirehoseStreamAction` is now called `FirehosePutRecordAction` ([#18356](https://github.com/aws/aws-cdk/issues/18356)) ([c016a9f](https://github.com/aws/aws-cdk/commit/c016a9fcf51f2415e6e0fcca2255da384c8abbc1)), closes [/github.com/aws/aws-cdk/pull/18321#discussion_r781620195](https://github.com/aws//github.com/aws/aws-cdk/pull/18321/issues/discussion_r781620195) +* **pipelines:** "Maximum schema version supported" error ([#18404](https://github.com/aws/aws-cdk/issues/18404)) ([a684ff4](https://github.com/aws/aws-cdk/commit/a684ff47d56038a94c82cdbad9588da939963351)), closes [#18370](https://github.com/aws/aws-cdk/issues/18370) +* **pipelines:** graphnode dependencies can have duplicates ([#18450](https://github.com/aws/aws-cdk/issues/18450)) ([2b0b5ea](https://github.com/aws/aws-cdk/commit/2b0b5ea5db7ce8103a641c1267b1c213453ac145)) +* **secretsmanager:** Secret requires KMS key for some same-account access ([#17812](https://github.com/aws/aws-cdk/issues/17812)) ([91f3539](https://github.com/aws/aws-cdk/commit/91f3539f4aa8383adcb2273790ddb469fb1274a6)), closes [#15450](https://github.com/aws/aws-cdk/issues/15450) + + +### Reverts + +* **s3:** add EventBridge bucket notifications ([#18150](https://github.com/aws/aws-cdk/issues/18150)) ([#18507](https://github.com/aws/aws-cdk/issues/18507)) ([2041278](https://github.com/aws/aws-cdk/commit/204127862d5fb1d2e6dd573a1621254e52eca4aa)) + +## [1.139.0](https://github.com/aws/aws-cdk/compare/v1.138.2...v1.139.0) (2022-01-11) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **apigatewayv2-authorizers:** `WebSocketLambdaAuthorizerProps.identitySource` default changes from `['$request.header.Authorization']` to `['route.request.header.Authorization']`. +* **cfn2ts:** some "complex" property types within the generated +CloudFormation interfaces (i.e: properties of `Cfn*` constructs) with +names starting with a capital letter `I` followed by another capital +letter are no longer incorrectly treated as behavioral interfaces, and +might hence have different usage patterns in non-TypeScript languages. +Such interfaces were previously very difficult to use in non-TypeScript +languages, and required convoluted workarounds, which can now be removed. + +### Features + +* **aws-ecs:** support runtime platform property for create fargate windows runtime. ([#17622](https://github.com/aws/aws-cdk/issues/17622)) ([fa8f2e2](https://github.com/aws/aws-cdk/commit/fa8f2e2180d60e5621d2ae9606a3d1b2dcb681d9)), closes [#17242](https://github.com/aws/aws-cdk/issues/17242) +* **bootstrap:** ECR `ScanOnPush` is now enabled by default ([#17994](https://github.com/aws/aws-cdk/issues/17994)) ([7588b51](https://github.com/aws/aws-cdk/commit/7588b517eb17bb5198f91056113eb79a34830867)) +* **cfnspec:** cloudformation spec v51.0.0 ([#18274](https://github.com/aws/aws-cdk/issues/18274)) ([c208e60](https://github.com/aws/aws-cdk/commit/c208e6043e4a184b4d3ac2508ebef1cb31bace43)) +* **cli:** diff now uses the lookup Role for new-style synthesis ([#18277](https://github.com/aws/aws-cdk/issues/18277)) ([2256680](https://github.com/aws/aws-cdk/commit/225668050caef9bfdaa25b8ae984d3886108397f)) +* **eks:** cluster tagging ([#4995](https://github.com/aws/aws-cdk/issues/4995)) ([#18109](https://github.com/aws/aws-cdk/issues/18109)) ([304f5b6](https://github.com/aws/aws-cdk/commit/304f5b6974f1121a8a5ff802076dffe2eff9f407)) +* **iam:** generate AccessKeys ([#18180](https://github.com/aws/aws-cdk/issues/18180)) ([beb5706](https://github.com/aws/aws-cdk/commit/beb5706e0c80300c8adba2b75b573f6c6def3de6)), closes [#8432](https://github.com/aws/aws-cdk/issues/8432) +* **lambda-event-sources:** adds `AuthenticationMethod.CLIENT_CERTIFICATE_TLS_AUTH` to kafka ([#17920](https://github.com/aws/aws-cdk/issues/17920)) ([93cd776](https://github.com/aws/aws-cdk/commit/93cd7769b7b68ab6985c357c4d2f2137bb631553)) +* **pipelines:** step dependencies ([#18256](https://github.com/aws/aws-cdk/issues/18256)) ([e3359e0](https://github.com/aws/aws-cdk/commit/e3359e0b79a8b999ed32c93fdbd19625bbbefaf8)), closes [#17945](https://github.com/aws/aws-cdk/issues/17945) +* **pipelines:** support timeout in CodeBuildStep ([#17351](https://github.com/aws/aws-cdk/issues/17351)) ([2aa3b8e](https://github.com/aws/aws-cdk/commit/2aa3b8e6e3ce75aaa7d4158f55e162eb26050ba1)) +* **s3:** add EventBridge bucket notifications ([#18150](https://github.com/aws/aws-cdk/issues/18150)) ([912aeda](https://github.com/aws/aws-cdk/commit/912aeda295820920ed880b9c85a98c56421647b8)), closes [#18076](https://github.com/aws/aws-cdk/issues/18076) +* **sqs:** add DLQ readonly property to Queue ([#18232](https://github.com/aws/aws-cdk/issues/18232)) ([caa6788](https://github.com/aws/aws-cdk/commit/caa6788781690c629226a54bb1f9529722d67887)), closes [#18083](https://github.com/aws/aws-cdk/issues/18083) + + +### Bug Fixes + +* **apigatewayv2-authorizers:** incorrect `identitySource` default for `WebSocketLambdaAuthorizer` ([#18315](https://github.com/aws/aws-cdk/issues/18315)) ([74eee1e](https://github.com/aws/aws-cdk/commit/74eee1e5b8fa404dde129f001b986d615f435c73)), closes [#18307](https://github.com/aws/aws-cdk/issues/18307) +* **appmesh:** allow a Virtual Node have as a backend a Virtual Service whose provider is that Node ([#18265](https://github.com/aws/aws-cdk/issues/18265)) ([272b6b1](https://github.com/aws/aws-cdk/commit/272b6b1abe22b7415eed5cdba82056d154fc31d7)), closes [#17322](https://github.com/aws/aws-cdk/issues/17322) +* **aws-kinesis:** remove default shard count when stream mode is on-demand and set default mode to provisioned ([#18221](https://github.com/aws/aws-cdk/issues/18221)) ([cac11bb](https://github.com/aws/aws-cdk/commit/cac11bba2ea0714dec8e23b069496d1b9d940685)), closes [#18139](https://github.com/aws/aws-cdk/issues/18139) +* **aws-lambda-event-sources:** unsupported properties for SelfManagedKafkaEventSource and ManagedKafkaEventSource ([#17965](https://github.com/aws/aws-cdk/issues/17965)) ([5ddaef4](https://github.com/aws/aws-cdk/commit/5ddaef491d7962616f75f170cf7547cd9229338f)), closes [#17934](https://github.com/aws/aws-cdk/issues/17934) +* **cfn2ts:** some property times have behavioral-interface names ([#18275](https://github.com/aws/aws-cdk/issues/18275)) ([6359c12](https://github.com/aws/aws-cdk/commit/6359c12e3242e23d9b3bf0a42cac7c361c8d4d8a)) +* **cli:** assets are KMS-encrypted using wrong key ([#18340](https://github.com/aws/aws-cdk/issues/18340)) ([64ae9f3](https://github.com/aws/aws-cdk/commit/64ae9f3dc8a169ad0a7a2d02cb04f857debd3653)), closes [#17668](https://github.com/aws/aws-cdk/issues/17668) [#18262](https://github.com/aws/aws-cdk/issues/18262) +* **cli:** breaks due to faulty version of `colors` ([#18324](https://github.com/aws/aws-cdk/issues/18324)) ([ddc2bc6](https://github.com/aws/aws-cdk/commit/ddc2bc6ae64fe14ddb4a03122c90dfcf954f149f)) +* **codebuild:** setting Cache.none() renders nothing in the template ([#18194](https://github.com/aws/aws-cdk/issues/18194)) ([cd51a5d](https://github.com/aws/aws-cdk/commit/cd51a5dae1780e34aecd90d85783fb6d3c239903)), closes [#18165](https://github.com/aws/aws-cdk/issues/18165) +* **lambda:** imported Function still has region and account from its Stack, instead of its ARN ([#18255](https://github.com/aws/aws-cdk/issues/18255)) ([01bbe4c](https://github.com/aws/aws-cdk/commit/01bbe4ca6c38ca7fe2239f8885bbec5ab537c9ad)), closes [#18228](https://github.com/aws/aws-cdk/issues/18228) +* **lambda-python:** asset files are generated inside the 'asset-input' folder ([#18306](https://github.com/aws/aws-cdk/issues/18306)) ([aff607a](https://github.com/aws/aws-cdk/commit/aff607a65e061ade5c3ec9e29f82fdaa8b57f638)) +* **lambda-python:** bundle asset files correctly ([#18335](https://github.com/aws/aws-cdk/issues/18335)) ([3822c85](https://github.com/aws/aws-cdk/commit/3822c855cf92ee0cd4539dee33e55f57d995bf89)), closes [#18301](https://github.com/aws/aws-cdk/issues/18301) +* **logs:** respect region when importing log group ([#18215](https://github.com/aws/aws-cdk/issues/18215)) ([be909bc](https://github.com/aws/aws-cdk/commit/be909bc90822db947ec0a932621709d0cb07e50e)), closes [#18214](https://github.com/aws/aws-cdk/issues/18214) +* **pipelines:** `DockerCredential.dockerHub()` silently fails auth ([#18313](https://github.com/aws/aws-cdk/issues/18313)) ([c2c87d9](https://github.com/aws/aws-cdk/commit/c2c87d9dd861a25dcbd9aa830e81ecb4d76ba509)), closes [#15737](https://github.com/aws/aws-cdk/issues/15737) +* **route53:** support multiple cross account DNS delegations ([#17837](https://github.com/aws/aws-cdk/issues/17837)) ([76b5c0d](https://github.com/aws/aws-cdk/commit/76b5c0d12e1e692efcf6a557ee4ddb6df3709e4d)), closes [#17836](https://github.com/aws/aws-cdk/issues/17836) + +## [1.138.2](https://github.com/aws/aws-cdk/compare/v1.138.1...v1.138.2) (2022-01-09) + + +### Bug Fixes + +* **cli:** breaks due to faulty version of `colors` ([#18324](https://github.com/aws/aws-cdk/issues/18324)) ([43bf9ae](https://github.com/aws/aws-cdk/commit/43bf9aec0b3c5e06d5382b29f4e8e0c91cd796ca)) + +## [1.138.1](https://github.com/aws/aws-cdk/compare/v1.138.0...v1.138.1) (2022-01-07) + + +### Bug Fixes + +* **lambda-python:** asset files are generated inside the 'asset-input' folder ([#18306](https://github.com/aws/aws-cdk/issues/18306)) ([b00b44e](https://github.com/aws/aws-cdk/commit/b00b44efd6e402744725e711906b456a28cebc5b)) + ## [1.138.0](https://github.com/aws/aws-cdk/compare/v1.137.0...v1.138.0) (2022-01-04) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e7a389384d07..25ed091ee83e4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,7 @@ let us know if it's not up-to-date (even better, submit a PR with your correcti - [Step 5: Merge](#step-5-merge) - [Breaking Changes](#breaking-changes) - [Documentation](#documentation) - - [rosetta](#rosetta) + - [Rosetta](#rosetta) - [Tools](#tools) - [Linters](#linters) - [cfn2ts](#cfn2ts) @@ -46,15 +46,14 @@ let us know if it's not up-to-date (even better, submit a PR with your correcti The following steps describe how to set up the AWS CDK repository on your local machine. The alternative is to use [Gitpod](https://www.gitpod.io/), a Cloud IDE for your development. -See [Gitpod section](#gitpod) on how to set up the CDK repo on Gitpod. +See [Gitpod section](#gitpod-alternative) on how to set up the CDK repo on Gitpod. ### Setup The following tools need to be installed on your system prior to installing the CDK: -- [Node.js >= 10.13.0](https://nodejs.org/download/release/latest-v10.x/) +- [Node.js >= 14.15.0](https://nodejs.org/download/release/latest-v14.x/) - We recommend using a version in [Active LTS](https://nodejs.org/en/about/releases/) - - ⚠️ versions `13.0.0` to `13.6.0` are not supported due to compatibility issues with our dependencies. - [Yarn >= 1.19.1, < 2](https://yarnpkg.com/lang/en/docs/install) - [.NET Core SDK 3.1.x](https://www.microsoft.com/net/download) - [Python >= 3.6.5, < 4.0](https://www.python.org/downloads/release/python-365/) @@ -70,7 +69,7 @@ $ yarn install ``` We recommend that you use [Visual Studio Code](https://code.visualstudio.com/) to work on the CDK. -We use `eslint` to keep our code consistent in terms of style and reducing defects. We recommend installing the +We use `eslint` to keep our code consistent in terms of style and reducing defects. We recommend installing the [eslint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) as well. ### Repo Layout @@ -218,6 +217,8 @@ Work your magic. Here are some guidelines: Watch out for their error messages and adjust your code accordingly. * Every change requires a unit test * If you change APIs, make sure to update the module's README file + * When you add new examples to the module's README file, you must also ensure they compile - the PR build will fail + if they do not. To learn more about how to ensure that they compile, see [Documentation](#documentation). * Try to maintain a single feature/bugfix per pull request. It's okay to introduce a little bit of housekeeping changes along the way, but try to avoid conflating multiple features. Eventually, all these are going to go into a single commit, so you can use that to frame your scope. @@ -509,7 +510,7 @@ the README for the `aws-ec2` module - https://docs.aws.amazon.com/cdk/api/latest ### Rosetta -The README file contains code snippets written as typescript code. Code snippets typed in fenced code blocks +The README file contains code snippets written as typescript code. Code snippets typed in fenced code blocks (such as `` ```ts ``) will be automatically extracted, compiled and translated to other languages when the during the [pack](#pack) step. We call this feature 'rosetta'. @@ -542,11 +543,12 @@ When no fixture is specified, the fixture with the name `rosetta/default.ts-fixture` will be used if present. `nofixture` can be used to opt out of that behavior. -In an `@example` block, which is unfenced, the first line of the example can -contain three slashes to achieve the same effect: +In an `@example` block, which is unfenced, additional information pertaining to +the example can be provided via the `@exampleMetadata` tag: ``` /** + * @exampleMetadata fixture=with-bucket * @example * /// fixture=with-bucket * bucket.addLifecycleTransition({ ...props }); @@ -583,21 +585,21 @@ cases where some of those do not apply - good judgement is to be applied): // ...rest of the example... ``` -- Within `.ts-fixture` files, make use of `declare` statements instead of - writing a compatible value (this will make your fixtures more durable): +- Make use of `declare` statements directly in examples for values that are + necessary for compilation but unimportant to the example: ```ts - // An hypothetical 'rosetta/default.ts-fixture' file in `@aws-cdk/core` - import * as kms from '@aws-cdk/aws-kms'; - import * as s3 from '@aws-cdk/aws-s3'; - import { StackProps } from '@aws-cdk/core'; - - declare const kmsKey: kms.IKey; - declare const bucket: s3.Bucket; - - declare const props: StackProps; + // An example about adding a stage to a pipeline in the @aws-cdk/pipelines library + declare const pipeline: pipelines.CodePipeline; + declare const myStage: Stage; + pipeline.addStage(myStage); ``` +- Utilize the `default.ts-fixture` that already exists rather than writing new + `.ts-fixture` files. This is because values stored in `.ts-fixture` files do + not surface to the examples visible in the docs, so while they help successful + compilation, they do not help users understand the example. + ## Tools (Advanced) ### scripts/foreach.sh @@ -663,7 +665,7 @@ extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-e #### pkglint -The `pkglint` tool "lints" package.json files across the repo according to [rules.ts](tools/pkglint/lib/rules.ts). +The `pkglint` tool "lints" package.json files across the repo according to [rules.ts](tools/@aws-cdk/pkglint/lib/rules.ts). To evaluate (and attempt to fix) all package linting issues in the repo, run the following command from the root of the repository (after bootstrapping): @@ -727,7 +729,7 @@ these directories. ### Linking against this repository If you are developing your own CDK application or library and want to use the locally checked out version of the -AWS CDK, instead of the the version of npm, the `./link-all.sh` script will help here. +AWS CDK, instead of the version of npm, the `./link-all.sh` script will help here. This script symlinks the built modules from the local AWS CDK repo under the `node_modules/` folder of the CDK app or library. @@ -804,35 +806,17 @@ The pattern is simple: with the name of the context key that **enables** this new feature (for example, `ENABLE_STACK_NAME_DUPLICATES`). The context key should be in the form `module.Type:feature` (e.g. `@aws-cdk/core:enableStackNameDuplicates`). -2. Use `node.tryGetContext(cxapi.ENABLE_XXX)` to check if this feature is enabled +2. Use `FeatureFlags.of(construct).isEnabled(cxapi.ENABLE_XXX)` to check if this feature is enabled in your code. If it is not defined, revert to the legacy behavior. 3. Add your feature flag to the `FUTURE_FLAGS` map in [cx-api/lib/features.ts](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/cx-api/lib/features.ts). This map is inserted to generated `cdk.json` files for new projects created through `cdk init`. -4. In your PR title (which goes into CHANGELOG), add a `(under feature flag)` suffix. e.g: +4. In your tests, use the `testFutureBehavior` and `testLegacyBehavior` [jest helper methods] to test the enabled and disabled behavior. +5. In your PR title (which goes into CHANGELOG), add a `(under feature flag)` suffix. e.g: `fix(core): impossible to use the same physical stack name for two stacks (under feature flag)` -In the [next major version of the -CDK](https://github.com/aws/aws-cdk/issues/3398) we will either remove the -legacy behavior or flip the logic for all these features and then -reset the `FEATURE_FLAGS` map for the next cycle. - -### Feature Flags - CDKv2 - -We have started working on the next version of the CDK, specifically CDKv2. This is currently being maintained -on a separate branch `v2-main` whereas `master` continues to track versions `1.x`. - -Feature flags introduced in the CDK 1.x and removed in 2.x, must be added to the `FUTURE_FLAGS_EXPIRED` list in -[cx-api/lib/features.ts](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/cx-api/lib/features.ts) -on the `v2-main` branch. -This will make the default behaviour in CDKv2 as if the flag is enabled and also prevents users from disabling -the feature flag. - -A couple of [jest helper methods] are available for use with unit tests. These help run unit tests that test -behaviour when flags are enabled or disabled in the two major versions. - [jest helper methods]: https://github.com/aws/aws-cdk/blob/master/tools/@aws-cdk/cdk-build-tools/lib/feature-flag.ts ## Versioning and Release diff --git a/README.md b/README.md index 6f9bb422faa7d..7349b1909d6b1 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,8 @@ infrastructure definition and share it without worrying about boilerplate logic. The CDK is available in the following languages: -* JavaScript, TypeScript ([Node.js ≥ 10.13.0](https://nodejs.org/download/release/latest-v10.x/)) +* JavaScript, TypeScript ([Node.js ≥ 14.15.0](https://nodejs.org/download/release/latest-v14.x/)) - We recommend using a version in [Active LTS](https://nodejs.org/en/about/releases/) - - ⚠️ versions `13.0.0` to `13.6.0` are not supported due to compatibility issues with our dependencies. * Python ([Python ≥ 3.6](https://www.python.org/downloads/)) * Java ([Java ≥ 8](https://www.oracle.com/technetwork/java/javase/downloads/index.html) and [Maven ≥ 3.5.4](https://maven.apache.org/download.cgi)) * .NET ([.NET Core ≥ 3.1](https://dotnet.microsoft.com/download)) @@ -77,8 +76,7 @@ in the CDK Developer Guide. For a detailed walkthrough, see the [tutorial](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html#hello_world_tutorial) in the AWS CDK [Developer Guide](https://docs.aws.amazon.com/cdk/latest/guide/home.html). ### At a glance -Install or update the [AWS CDK CLI] from npm (requires [Node.js ≥ 10.13.0](https://nodejs.org/download/release/latest-v10.x/)). We recommend using a version in [Active LTS](https://nodejs.org/en/about/releases/) -⚠️ versions `13.0.0` to `13.6.0` are not supported due to compatibility issues with our dependencies. +Install or update the [AWS CDK CLI] from npm (requires [Node.js ≥ 14.15.0](https://nodejs.org/download/release/latest-v14.x/)). We recommend using a version in [Active LTS](https://nodejs.org/en/about/releases/) ```console $ npm i -g aws-cdk @@ -155,6 +153,12 @@ this capability, please see the ## More Resources * [CDK Workshop](https://cdkworkshop.com/) * [Construct Hub](https://constructs.dev) - Find and use open-source Cloud Development Kit (CDK) libraries +* Best Practices + * [Best practices for developing cloud applications with AWS CDK](https://aws.amazon.com/blogs/devops/best-practices-for-developing-cloud-applications-with-aws-cdk/) + * [Align with best practices while creating infrastructure using cdk aspects](https://aws.amazon.com/blogs/devops/align-with-best-practices-while-creating-infrastructure-using-cdk-aspects/) + * [Recommended AWS CDK project structure for Python applications](https://aws.amazon.com/blogs/developer/recommended-aws-cdk-project-structure-for-python-applications/) + * [Best practices for discoverability of a construct library on Construct Hub](https://aws.amazon.com/blogs/opensource/best-practices-for-discoverability-of-a-construct-library-on-construct-hub/) +* [All developer blog posts about AWS CDK](https://aws.amazon.com/blogs/developer/category/developer-tools/aws-cloud-development-kit/) * **[CDK Construction Zone](https://www.twitch.tv/collections/9kCOGphNZBYVdA)** - A Twitch live coding series hosted by the CDK team, season one episodes: * Triggers: Join us as we implement [Triggers](https://github.com/aws/aws-cdk-rfcs/issues/71), a Construct for configuring deploy time actions. Episodes 1-3: * [S1E1](https://www.twitch.tv/videos/917691798): Triggers (part 1); **Participants:** @NetaNir, @eladb, @richardhboyd diff --git a/allowed-breaking-changes.txt b/allowed-breaking-changes.txt index fa3498335f679..864ea5178512b 100644 --- a/allowed-breaking-changes.txt +++ b/allowed-breaking-changes.txt @@ -93,3 +93,36 @@ incompatible-argument:@aws-cdk/aws-autoscaling-hooktargets.FunctionHook.bind incompatible-argument:@aws-cdk/aws-autoscaling-hooktargets.QueueHook.bind incompatible-argument:@aws-cdk/aws-autoscaling-hooktargets.TopicHook.bind incompatible-argument:@aws-cdk/aws-autoscaling.ILifecycleHookTarget.bind + +# removed properties from kafka eventsources as they are not supported +removed:@aws-cdk/aws-lambda-event-sources.KafkaEventSourceProps.bisectBatchOnError +removed:@aws-cdk/aws-lambda-event-sources.KafkaEventSourceProps.maxRecordAge +removed:@aws-cdk/aws-lambda-event-sources.KafkaEventSourceProps.parallelizationFactor +removed:@aws-cdk/aws-lambda-event-sources.KafkaEventSourceProps.reportBatchItemFailures +removed:@aws-cdk/aws-lambda-event-sources.KafkaEventSourceProps.retryAttempts +removed:@aws-cdk/aws-lambda-event-sources.KafkaEventSourceProps.tumblingWindow +removed:@aws-cdk/aws-lambda-event-sources.ManagedKafkaEventSourceProps.bisectBatchOnError +removed:@aws-cdk/aws-lambda-event-sources.ManagedKafkaEventSourceProps.maxRecordAge +removed:@aws-cdk/aws-lambda-event-sources.ManagedKafkaEventSourceProps.parallelizationFactor +removed:@aws-cdk/aws-lambda-event-sources.ManagedKafkaEventSourceProps.reportBatchItemFailures +removed:@aws-cdk/aws-lambda-event-sources.ManagedKafkaEventSourceProps.retryAttempts +removed:@aws-cdk/aws-lambda-event-sources.ManagedKafkaEventSourceProps.tumblingWindow +removed:@aws-cdk/aws-lambda-event-sources.SelfManagedKafkaEventSourceProps.bisectBatchOnError +removed:@aws-cdk/aws-lambda-event-sources.SelfManagedKafkaEventSourceProps.maxRecordAge +removed:@aws-cdk/aws-lambda-event-sources.SelfManagedKafkaEventSourceProps.parallelizationFactor +removed:@aws-cdk/aws-lambda-event-sources.SelfManagedKafkaEventSourceProps.reportBatchItemFailures +removed:@aws-cdk/aws-lambda-event-sources.SelfManagedKafkaEventSourceProps.retryAttempts +removed:@aws-cdk/aws-lambda-event-sources.SelfManagedKafkaEventSourceProps.tumblingWindow +base-types:@aws-cdk/aws-lambda-event-sources.KafkaEventSourceProps +base-types:@aws-cdk/aws-lambda-event-sources.ManagedKafkaEventSourceProps +base-types:@aws-cdk/aws-lambda-event-sources.SelfManagedKafkaEventSourceProps + +# fixed vpc property of BaseLoadBalancer so it correctly implements I(Application|Network)LoadBalancer (i.e: must be optional) +changed-type:@aws-cdk/aws-elasticloadbalancingv2.ApplicationLoadBalancer.vpc +changed-type:@aws-cdk/aws-elasticloadbalancingv2.BaseLoadBalancer.vpc +changed-type:@aws-cdk/aws-elasticloadbalancingv2.NetworkLoadBalancer.vpc + +# removed methods and properties related to event bridge notifications for S3 buckets as they are not yet supported (19 Jan 2022) +removed:@aws-cdk/aws-s3.Bucket.enableEventBridgeNotification +removed:@aws-cdk/aws-s3.BucketBase.enableEventBridgeNotification +removed:@aws-cdk/aws-s3.BucketProps.eventBridgeEnabled \ No newline at end of file diff --git a/buildspec.yaml b/buildspec.yaml index 94a75e2357f78..db462c8b987be 100644 --- a/buildspec.yaml +++ b/buildspec.yaml @@ -27,6 +27,8 @@ phases: - /bin/bash ./scripts/transform.sh post_build: commands: + # Short-circuit: Don't run pack if the above build failed. + - '[ ${CODEBUILD_BUILD_SUCCEEDING:-0} -eq 1 ] || exit 1' - "[ -f .BUILD_COMPLETED ] && /bin/bash ./pack.sh" - /bin/bash ./scripts/cache-store.sh artifacts: diff --git a/design/aws-ecs/aws-ecs-fargate-capacity-providers.md b/design/aws-ecs/aws-ecs-fargate-capacity-providers.md index 66451ece2843a..cae9d594e583f 100644 --- a/design/aws-ecs/aws-ecs-fargate-capacity-providers.md +++ b/design/aws-ecs/aws-ecs-fargate-capacity-providers.md @@ -2,7 +2,7 @@ ## Objective -Since Capacity Providers are now supported in CloudFormation, incorporating support for Fargate Spot capacity has been one of the [top asks](https://github.com/aws/aws-cdk/issues?q=is%3Aissue+is%3Aopen+label%3A%40aws-cdk%2Faws-ecs+sort%3Areactions-%2B1-desc) for the ECS CDK module, with over 60 customer reactions. While there are still some outstanding issues regarding capacity provider support in general, specifically regarding cyclic workflows with named clusters (See: [CFN issue](http://%20https//github.com/aws/containers-roadmap/issues/631#issuecomment-702580141)), we should be able to move ahead with supporting `FARGATE` and `FARGATE_SPOT` capacity providers with our existing FargateService construct. +Since Capacity Providers are now supported in CloudFormation, incorporating support for Fargate Spot capacity has been one of the [top asks](https://github.com/aws/aws-cdk/issues?q=is%3Aissue+is%3Aopen+label%3A%40aws-cdk%2Faws-ecs+sort%3Areactions-%2B1-desc) for the ECS CDK module, with over 60 customer reactions. While there are still some outstanding issues regarding capacity provider support in general, specifically regarding cyclic workflows with named clusters (See: [CFN issue](https://github.com/aws/containers-roadmap/issues/631#issuecomment-702580141)), we should be able to move ahead with supporting `FARGATE` and `FARGATE_SPOT` capacity providers with our existing FargateService construct. See: https://github.com/aws/aws-cdk/issues/5850 @@ -118,4 +118,3 @@ One alternative considered was to provide a more magical experience by populatin For future extensibility, we can however add an `addCapacityProvider` method on the Cluster resource, to allow modifying the cluster CapacityProviders field post-construction. Another option would be to create a new FargateCluster resource, that would have the two Fargate capacity providers set by default. The main advantage with this alternative would be that it would be consistent with the current Console experience, which sets the Fargate capacity providers for you if you choose the “Networking Only” cluster template via the cluster wizard. The downside is that it would be a more restrictive resource model that would go back on the decision to have a single generic ECS Cluster resource that could potentially contain both Fargate and EC2 services or tasks. Given that we are moving towards more generic versions of ECS resources, this is not a preferable solution. That being said, in the current iteration we can set the Fargate Capacity Providers on the cluster by default, but put them behind a feature flag, which we would be able to remove in the v2 version of the ECS module. Using the feature flag would ensure that there would not be a diff in the generated CFN template for existing customers defining ECS clusters in their stack who redeploy using an updated version of the CDK. - diff --git a/package.json b/package.json index 462d2c605eabd..9c9efa4f00664 100644 --- a/package.json +++ b/package.json @@ -16,22 +16,25 @@ }, "devDependencies": { "@yarnpkg/lockfile": "^1.1.0", - "cdk-generate-synthetic-examples": "^0.1.1", + "cdk-generate-synthetic-examples": "^0.1.6", "conventional-changelog-cli": "^2.2.2", "fs-extra": "^9.1.0", - "graceful-fs": "^4.2.8", + "graceful-fs": "^4.2.9", "jest-junit": "^13.0.0", - "jsii-diff": "^1.50.0", - "jsii-pacmak": "^1.50.0", - "jsii-reflect": "^1.50.0", - "jsii-rosetta": "^1.50.0", + "jsii-diff": "^1.54.0", + "jsii-pacmak": "^1.54.0", + "jsii-reflect": "^1.54.0", + "jsii-rosetta": "^1.54.0", "lerna": "^4.0.0", "patch-package": "^6.4.7", + "semver": "^6.3.0", "standard-version": "^9.3.2", "typescript": "~3.9.10" }, "resolutions": { - "string-width": "^4.2.3" + "colors": "1.4.0", + "string-width": "^4.2.3", + "markdown-it": "^12.3.2" }, "repository": { "type": "git", @@ -122,8 +125,8 @@ "aws-cdk-lib/@balena/dockerignore/**", "aws-cdk-lib/case", "aws-cdk-lib/case/**", - "aws-cdk-lib/colors", - "aws-cdk-lib/colors/**", + "aws-cdk-lib/chalk", + "aws-cdk-lib/chalk/**", "aws-cdk-lib/diff", "aws-cdk-lib/diff/**", "aws-cdk-lib/fast-deep-equal", @@ -150,8 +153,8 @@ "monocdk/@balena/dockerignore/**", "monocdk/case", "monocdk/case/**", - "monocdk/colors", - "monocdk/colors/**", + "monocdk/chalk", + "monocdk/chalk/**", "monocdk/diff", "monocdk/diff/**", "monocdk/fast-deep-equal", diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/README.md b/packages/@aws-cdk-containers/ecs-service-extensions/README.md index 115c8785ddbc1..84dd42fc93eff 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/README.md +++ b/packages/@aws-cdk-containers/ecs-service-extensions/README.md @@ -109,7 +109,43 @@ nameDescription.add(new Container({ Every `ServiceDescription` requires at minimum that you add a `Container` extension which defines the main application (essential) container to run for the service. -After that, you can optionally enable additional features for the service using the `ServiceDescription.add()` method: +### Logging using `awslogs` log driver + +If no observability extensions have been configured for a service, the ECS Service Extensions configures an `awslogs` log driver for the application container of the service to send the container logs to CloudWatch Logs. + +You can either provide a log group to the `Container` extension or one will be created for you by the CDK. + +Following is an example of an application with an `awslogs` log driver configured for the application container: + +```ts +const environment = new Environment(stack, 'production'); + +const nameDescription = new ServiceDescription(); +nameDescription.add(new Container({ + cpu: 1024, + memoryMiB: 2048, + trafficPort: 80, + image: ContainerImage.fromRegistry('nathanpeck/name'), + environment: { + PORT: '80', + }, + logGroup: new awslogs.LogGroup(stack, 'MyLogGroup'), +})); +``` + +If a log group is not provided, no observability extensions have been created, and the `ECS_SERVICE_EXTENSIONS_ENABLE_DEFAULT_LOG_DRIVER` feature flag is enabled, then logging will be configured by default and a log group will be created for you. + +The `ECS_SERVICE_EXTENSIONS_ENABLE_DEFAULT_LOG_DRIVER` feature flag is enabled by default in any CDK apps that are created with CDK v1.140.0 or v2.8.0 and later. + +To enable default logging for previous versions, ensure that the `ECS_SERVICE_EXTENSIONS_ENABLE_DEFAULT_LOG_DRIVER` flag within the application stack context is set to true, like so: + +```ts +stack.node.setContext(cxapi.ECS_SERVICE_EXTENSIONS_ENABLE_DEFAULT_LOG_DRIVER, true); +``` + +Alternatively, you can also set the feature flag in the `cdk.json` file. For more information, refer the [docs](https://docs.aws.amazon.com/cdk/v2/guide/featureflags.html). + +After adding the `Container` extension, you can optionally enable additional features for the service using the `ServiceDescription.add()` method: ```ts nameDescription.add(new AppMeshExtension({ mesh })); diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/container.ts b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/container.ts index 10c61401e90d3..2711108e4f0ff 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/container.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/container.ts @@ -1,4 +1,7 @@ import * as ecs from '@aws-cdk/aws-ecs'; +import * as awslogs from '@aws-cdk/aws-logs'; +import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Service } from '../service'; import { ServiceExtension } from './extension-interfaces'; @@ -38,6 +41,13 @@ export interface ContainerExtensionProps { readonly environment?: { [key: string]: string, } + + /** + * The log group into which application container logs should be routed. + * + * @default - A log group is automatically created for you if the `ECS_SERVICE_EXTENSIONS_ENABLE_DEFAULT_LOG_DRIVER` feature flag is set. + */ + readonly logGroup?: awslogs.ILogGroup; } /** @@ -51,6 +61,11 @@ export class Container extends ServiceExtension { */ public readonly trafficPort: number; + /** + * The log group into which application container logs should be routed. + */ + public logGroup?: awslogs.ILogGroup; + /** * The settings for the container. */ @@ -60,11 +75,12 @@ export class Container extends ServiceExtension { super('service-container'); this.props = props; this.trafficPort = props.trafficPort; + this.logGroup = props.logGroup; } - // @ts-ignore - Ignore unused params that are required for abstract class extend public prehook(service: Service, scope: Construct) { this.parentService = service; + this.scope = scope; } // This hook sets the overall task resource requirements to the @@ -93,6 +109,31 @@ export class Container extends ServiceExtension { containerProps = hookProvider.mutateContainerDefinition(containerProps); }); + // If no observability extensions have been added to the service description then we can configure the `awslogs` log driver + if (!containerProps.logging) { + // Create a log group for the service if one is not provided by the user (only if feature flag is set) + if (!this.logGroup && this.parentService.node.tryGetContext(cxapi.ECS_SERVICE_EXTENSIONS_ENABLE_DEFAULT_LOG_DRIVER)) { + this.logGroup = new awslogs.LogGroup(this.scope, `${this.parentService.id}-logs`, { + logGroupName: `${this.parentService.id}-logs`, + removalPolicy: cdk.RemovalPolicy.DESTROY, + retention: awslogs.RetentionDays.ONE_MONTH, + }); + } + + if (this.logGroup) { + containerProps = { + ...containerProps, + logging: new ecs.AwsLogDriver({ + streamPrefix: this.parentService.id, + logGroup: this.logGroup, + }), + }; + } + } else { + if (this.logGroup) { + throw Error(`Log configuration already specified. You cannot provide a log group for the application container of service '${this.parentService.id}' while also adding log configuration separately using service extensions.`); + } + } this.container = taskDefinition.addContainer('app', containerProps); // Create a port mapping for the container diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/scale-on-cpu-utilization.ts b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/scale-on-cpu-utilization.ts index ec6ce9662535f..2aa60ebc96364 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/scale-on-cpu-utilization.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/scale-on-cpu-utilization.ts @@ -2,8 +2,13 @@ import * as ecs from '@aws-cdk/aws-ecs'; import * as cdk from '@aws-cdk/core'; import { ServiceExtension, ServiceBuild } from './extension-interfaces'; + /** * The autoscaling settings. + * + * @deprecated use the `minTaskCount` and `maxTaskCount` properties of `autoScaleTaskCount` in the `Service` construct + * to configure the auto scaling target for the service. For more information, please refer + * https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk-containers/ecs-service-extensions/README.md#task-auto-scaling . */ export interface CpuScalingProps { /** @@ -61,6 +66,9 @@ const cpuScalingPropsDefault = { /** * This extension helps you scale your service according to CPU utilization. + * + * @deprecated To enable target tracking based on CPU utilization, use the `targetCpuUtilization` property of `autoScaleTaskCount` in the `Service` construct. + * For more information, please refer https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk-containers/ecs-service-extensions/README.md#task-auto-scaling . */ export class ScaleOnCpuUtilization extends ServiceExtension { /** @@ -126,6 +134,9 @@ export class ScaleOnCpuUtilization extends ServiceExtension { // This hook utilizes the resulting service construct // once it is created. public useService(service: ecs.Ec2Service | ecs.FargateService) { + if (this.parentService.scalableTaskCount) { + throw Error('Cannot specify \'autoScaleTaskCount\' in the Service construct and also provide a \'ScaleOnCpuUtilization\' extension. \'ScaleOnCpuUtilization\' is deprecated. Please only provide \'autoScaleTaskCount\'.'); + } const scalingTarget = service.autoScaleTaskCount({ minCapacity: this.minTaskCount, maxCapacity: this.maxTaskCount, @@ -136,5 +147,6 @@ export class ScaleOnCpuUtilization extends ServiceExtension { scaleInCooldown: this.scaleInCooldown, scaleOutCooldown: this.scaleOutCooldown, }); + this.parentService.enableAutoScalingPolicy(); } } diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/lib/service.ts b/packages/@aws-cdk-containers/ecs-service-extensions/lib/service.ts index 2214f209fb935..c90c59edaea31 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/lib/service.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/lib/service.ts @@ -54,6 +54,8 @@ export interface ServiceProps { /** * The options for configuring the auto scaling target. + * + * @default none */ readonly autoScaleTaskCount?: AutoScalingOptions; } @@ -196,7 +198,6 @@ export class Service extends Construct { // Ensure that the task definition supports both EC2 and Fargate compatibility: ecs.Compatibility.EC2_AND_FARGATE, } as ecs.TaskDefinitionProps; - for (const extensions in this.serviceDescription.extensions) { if (this.serviceDescription.extensions[extensions]) { taskDefProps = this.serviceDescription.extensions[extensions].modifyTaskDefinitionProps(taskDefProps); @@ -221,9 +222,6 @@ export class Service extends Construct { } } - // Set desiredCount to `undefined` if auto scaling is configured for the service - const desiredCount = props.autoScaleTaskCount ? undefined : (props.desiredCount || 1); - // Give each extension a chance to mutate the service props before // service creation let serviceProps = { @@ -231,7 +229,7 @@ export class Service extends Construct { taskDefinition: this.taskDefinition, minHealthyPercent: 100, maxHealthyPercent: 200, - desiredCount, + desiredCount: props.desiredCount ?? 1, } as ServiceBuild; for (const extensions in this.serviceDescription.extensions) { @@ -273,6 +271,14 @@ export class Service extends Construct { } } + // Set desiredCount to `undefined` if auto scaling is configured for the service + if (props.autoScaleTaskCount || this.autoScalingPoliciesEnabled) { + serviceProps = { + ...serviceProps, + desiredCount: undefined, + }; + } + // Now that the service props are determined we can create // the service if (this.capacityType === EnvironmentCapacityType.EC2) { @@ -291,17 +297,17 @@ export class Service extends Construct { }); if (props.autoScaleTaskCount.targetCpuUtilization) { - const targetUtilizationPercent = props.autoScaleTaskCount.targetCpuUtilization; - this.scalableTaskCount.scaleOnCpuUtilization(`${this.id}-target-cpu-utilization-${targetUtilizationPercent}`, { - targetUtilizationPercent, + const targetCpuUtilizationPercent = props.autoScaleTaskCount.targetCpuUtilization; + this.scalableTaskCount.scaleOnCpuUtilization(`${this.id}-target-cpu-utilization-${targetCpuUtilizationPercent}`, { + targetUtilizationPercent: targetCpuUtilizationPercent, }); this.enableAutoScalingPolicy(); } if (props.autoScaleTaskCount.targetMemoryUtilization) { - const targetUtilizationPercent = props.autoScaleTaskCount.targetMemoryUtilization; - this.scalableTaskCount.scaleOnMemoryUtilization(`${this.id}-target-memory-utilization-${targetUtilizationPercent}`, { - targetUtilizationPercent, + const targetMemoryUtilizationPercent = props.autoScaleTaskCount.targetMemoryUtilization; + this.scalableTaskCount.scaleOnMemoryUtilization(`${this.id}-target-memory-utilization-${targetMemoryUtilizationPercent}`, { + targetUtilizationPercent: targetMemoryUtilizationPercent, }); this.enableAutoScalingPolicy(); } diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/package.json b/packages/@aws-cdk-containers/ecs-service-extensions/package.json index 7feb17909361e..3587f60c10eda 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/package.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/package.json @@ -37,14 +37,14 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/aws-autoscaling": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", - "jest": "^27.4.5", + "jest": "^27.5.1", "@aws-cdk/pkglint": "0.0.0", - "@aws-cdk/assert-internal": "0.0.0" + "@aws-cdk/assertions": "0.0.0" }, "dependencies": { "@aws-cdk/aws-applicationautoscaling": "0.0.0", @@ -70,6 +70,7 @@ "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/custom-resources": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/region-info": "0.0.0", "constructs": "^3.3.69" }, @@ -98,6 +99,7 @@ "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/custom-resources": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/region-info": "0.0.0", "constructs": "^3.3.69" }, diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/appmesh.test.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/appmesh.test.ts index 64ec202869f73..02a572e9e2ced 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/appmesh.test.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/appmesh.test.ts @@ -1,8 +1,8 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as appmesh from '@aws-cdk/aws-appmesh'; import * as ecs from '@aws-cdk/aws-ecs'; import * as cdk from '@aws-cdk/core'; -import { AppMeshExtension, Container, Environment, ScaleOnCpuUtilization, ServiceDescription, Service } from '../lib'; +import { AppMeshExtension, Container, Environment, ServiceDescription, Service } from '../lib'; describe('appmesh', () => { test('should be able to add AWS App Mesh to a service', () => { @@ -33,9 +33,8 @@ describe('appmesh', () => { }); // THEN - // Ensure that task has an App Mesh sidecar - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Cpu: 256, @@ -211,7 +210,7 @@ describe('appmesh', () => { }); // Ensure that the service has the right settings - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { Cluster: { Ref: 'productionenvironmentclusterC6599D2D', }, @@ -257,8 +256,6 @@ describe('appmesh', () => { Ref: 'myservicetaskdefinitionF3E2D86F', }, }); - - }); test('should have the right maximumPercentage at desired count == 1', () => { @@ -276,9 +273,6 @@ describe('appmesh', () => { trafficPort: 80, image: ecs.ContainerImage.fromRegistry('nathanpeck/name'), })); - serviceDescription.add(new ScaleOnCpuUtilization({ - initialTaskCount: 1, - })); const mesh = new appmesh.Mesh(stack, 'my-mesh'); @@ -289,17 +283,16 @@ describe('appmesh', () => { new Service(stack, 'my-service', { environment, serviceDescription, + desiredCount: 1, }); - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentConfiguration: { MaximumPercent: 200, MinimumHealthyPercent: 100, }, DesiredCount: 1, }); - - }); test('should have the right maximumPercentage at desired count == 2', () => { @@ -317,9 +310,6 @@ describe('appmesh', () => { trafficPort: 80, image: ecs.ContainerImage.fromRegistry('nathanpeck/name'), })); - serviceDescription.add(new ScaleOnCpuUtilization({ - initialTaskCount: 2, - })); const mesh = new appmesh.Mesh(stack, 'my-mesh'); @@ -330,17 +320,16 @@ describe('appmesh', () => { new Service(stack, 'my-service', { environment, serviceDescription, + desiredCount: 2, }); - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentConfiguration: { MaximumPercent: 150, MinimumHealthyPercent: 100, }, DesiredCount: 2, }); - - }); test('should have the right maximumPercentage at desired count == 3', () => { @@ -358,9 +347,6 @@ describe('appmesh', () => { trafficPort: 80, image: ecs.ContainerImage.fromRegistry('nathanpeck/name'), })); - serviceDescription.add(new ScaleOnCpuUtilization({ - initialTaskCount: 3, - })); const mesh = new appmesh.Mesh(stack, 'my-mesh'); @@ -371,17 +357,16 @@ describe('appmesh', () => { new Service(stack, 'my-service', { environment, serviceDescription, + desiredCount: 3, }); - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentConfiguration: { MaximumPercent: 150, MinimumHealthyPercent: 100, }, DesiredCount: 3, }); - - }); test('should have the right maximumPercentage at desired count == 4', () => { @@ -399,9 +384,6 @@ describe('appmesh', () => { trafficPort: 80, image: ecs.ContainerImage.fromRegistry('nathanpeck/name'), })); - serviceDescription.add(new ScaleOnCpuUtilization({ - initialTaskCount: 4, - })); const mesh = new appmesh.Mesh(stack, 'my-mesh'); @@ -412,17 +394,16 @@ describe('appmesh', () => { new Service(stack, 'my-service', { environment, serviceDescription, + desiredCount: 4, }); - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentConfiguration: { MaximumPercent: 125, MinimumHealthyPercent: 100, }, DesiredCount: 4, }); - - }); test('should have the right maximumPercentage at desired count > 4', () => { @@ -440,9 +421,6 @@ describe('appmesh', () => { trafficPort: 80, image: ecs.ContainerImage.fromRegistry('nathanpeck/name'), })); - serviceDescription.add(new ScaleOnCpuUtilization({ - initialTaskCount: 8, - })); const mesh = new appmesh.Mesh(stack, 'my-mesh'); @@ -453,17 +431,16 @@ describe('appmesh', () => { new Service(stack, 'my-service', { environment, serviceDescription, + desiredCount: 8, }); - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentConfiguration: { MaximumPercent: 125, MinimumHealthyPercent: 100, }, DesiredCount: 8, }); - - }); test('should be able to create multiple App Mesh enabled services and connect', () => { @@ -527,9 +504,7 @@ describe('appmesh', () => { greeterService.connectTo(greetingService); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition'); - - + Template.fromStack(stack).hasResource('AWS::ECS::TaskDefinition', Match.anyValue()); }); test('should detect when attempting to connect services from two different envs', () => { @@ -583,7 +558,5 @@ describe('appmesh', () => { expect(() => { developmentNameService.connectTo(productionNameService); }).toThrow(/Unable to connect service 'name-development' in environment 'development' to service 'name-production' in environment 'production' because services can not be connected across environment boundaries/); - - }); }); \ No newline at end of file diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/assign-public-ip.test.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/assign-public-ip.test.ts index 173a573b96e78..be8d6e6bcac51 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/assign-public-ip.test.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/assign-public-ip.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; @@ -29,15 +29,13 @@ describe('assign public ip', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { NetworkConfiguration: { AwsvpcConfiguration: { AssignPublicIp: 'ENABLED', }, }, }); - - }); test('errors when adding a public ip to ec2-backed service', () => { @@ -76,8 +74,6 @@ describe('assign public ip', () => { serviceDescription, }); }).toThrow(/Fargate/i); - - }); test('should not add a task record manager by default', () => { @@ -103,8 +99,6 @@ describe('assign public ip', () => { // THEN expect(service.ecsService.node.tryFindChild('TaskRecordManager')).toBeUndefined(); - - }); test('should add a task record manager when dns is requested', () => { @@ -138,8 +132,6 @@ describe('assign public ip', () => { // THEN expect(service.ecsService.node.tryFindChild('TaskRecordManager')).toBeDefined(); - - }); test('task record manager listens for ecs events', () => { @@ -172,7 +164,7 @@ describe('assign public ip', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { EventPattern: { 'source': ['aws.ecs'], 'detail-type': [ @@ -185,7 +177,7 @@ describe('assign public ip', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { EventPattern: { 'source': ['aws.ecs'], 'detail-type': [ @@ -197,7 +189,5 @@ describe('assign public ip', () => { }, }, }); - - }); }); diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/cloudwatch-agent.test.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/cloudwatch-agent.test.ts index 08cd8dfbe8a04..929f94180b3a2 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/cloudwatch-agent.test.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/cloudwatch-agent.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ecs from '@aws-cdk/aws-ecs'; import * as cdk from '@aws-cdk/core'; import { CloudwatchAgentExtension, Container, Environment, Service, ServiceDescription } from '../lib'; @@ -27,8 +27,7 @@ describe('cloudwatch agent', () => { }); // THEN - - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Cpu: 256, @@ -103,8 +102,5 @@ describe('cloudwatch agent', () => { ], }, }); - - }); - }); \ No newline at end of file diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/container.test.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/container.test.ts new file mode 100644 index 0000000000000..80fbe097b08d0 --- /dev/null +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/container.test.ts @@ -0,0 +1,309 @@ +import { Template } from '@aws-cdk/assertions'; +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 iam from '@aws-cdk/aws-iam'; +import * as awslogs from '@aws-cdk/aws-logs'; +import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import { Container, Environment, EnvironmentCapacityType, FireLensExtension, Service, ServiceDescription } from '../lib'; + +describe('container', () => { + test('should be able to add a container to the service', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addAsgCapacityProvider(new ecs.AsgCapacityProvider(stack, 'Provider', { + autoScalingGroup: new autoscaling.AutoScalingGroup(stack, 'DefaultAutoScalingGroup', { + vpc, + machineImage: ec2.MachineImage.latestAmazonLinux(), + instanceType: new ec2.InstanceType('t2.micro'), + }), + })); + + const environment = new Environment(stack, 'production', { + vpc, + cluster, + capacityType: EnvironmentCapacityType.EC2, + }); + const serviceDescription = new ServiceDescription(); + const taskRole = new iam.Role(stack, 'CustomTaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + serviceDescription.add(new Container({ + cpu: 256, + memoryMiB: 512, + trafficPort: 80, + image: ecs.ContainerImage.fromRegistry('nathanpeck/name'), + })); + + new Service(stack, 'my-service', { + environment, + serviceDescription, + taskRole, + }); + + // THEN + Template.fromStack(stack).resourceCountIs('AWS::ECS::Service', 1); + + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Cpu: 256, + Essential: true, + Image: 'nathanpeck/name', + Memory: 512, + Name: 'app', + PortMappings: [ + { + ContainerPort: 80, + Protocol: 'tcp', + }, + ], + Ulimits: [ + { + HardLimit: 1024000, + Name: 'nofile', + SoftLimit: 1024000, + }, + ], + }, + ], + Cpu: '256', + Family: 'myservicetaskdefinition', + Memory: '512', + NetworkMode: 'awsvpc', + RequiresCompatibilities: [ + 'EC2', + 'FARGATE', + ], + TaskRoleArn: { + 'Fn::GetAtt': [ + 'CustomTaskRole3C6B13FD', + 'Arn', + ], + }, + }); + }); + + test('should be able to enable default logging behavior - with enable default log driver feature flag', () => { + // GIVEN + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ECS_SERVICE_EXTENSIONS_ENABLE_DEFAULT_LOG_DRIVER, true); + + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addAsgCapacityProvider(new ecs.AsgCapacityProvider(stack, 'Provider', { + autoScalingGroup: new autoscaling.AutoScalingGroup(stack, 'DefaultAutoScalingGroup', { + vpc, + machineImage: ec2.MachineImage.latestAmazonLinux(), + instanceType: new ec2.InstanceType('t2.micro'), + }), + })); + + const environment = new Environment(stack, 'production', { + vpc, + cluster, + capacityType: EnvironmentCapacityType.EC2, + }); + const serviceDescription = new ServiceDescription(); + const taskRole = new iam.Role(stack, 'CustomTaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + serviceDescription.add(new Container({ + cpu: 256, + memoryMiB: 512, + trafficPort: 80, + image: ecs.ContainerImage.fromRegistry('nathanpeck/name'), + })); + + new Service(stack, 'my-service', { + environment, + serviceDescription, + taskRole, + }); + + // THEN + Template.fromStack(stack).resourceCountIs('AWS::ECS::Service', 1); + + // Ensure that the log group was created + Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 1); + + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Cpu: 256, + Essential: true, + Image: 'nathanpeck/name', + LogConfiguration: { + LogDriver: 'awslogs', + Options: { + 'awslogs-group': { + Ref: 'myservicelogs176EE19F', + }, + 'awslogs-stream-prefix': 'my-service', + 'awslogs-region': { + Ref: 'AWS::Region', + }, + }, + }, + Memory: 512, + Name: 'app', + PortMappings: [ + { + ContainerPort: 80, + Protocol: 'tcp', + }, + ], + Ulimits: [ + { + HardLimit: 1024000, + Name: 'nofile', + SoftLimit: 1024000, + }, + ], + }, + ], + Cpu: '256', + Family: 'myservicetaskdefinition', + Memory: '512', + NetworkMode: 'awsvpc', + RequiresCompatibilities: [ + 'EC2', + 'FARGATE', + ], + TaskRoleArn: { + 'Fn::GetAtt': [ + 'CustomTaskRole3C6B13FD', + 'Arn', + ], + }, + }); + }); + + test('should be able to add user-provided log group in the log driver options', () => { + // GIVEN + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ECS_SERVICE_EXTENSIONS_ENABLE_DEFAULT_LOG_DRIVER, true); + + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addAsgCapacityProvider(new ecs.AsgCapacityProvider(stack, 'Provider', { + autoScalingGroup: new autoscaling.AutoScalingGroup(stack, 'DefaultAutoScalingGroup', { + vpc, + machineImage: ec2.MachineImage.latestAmazonLinux(), + instanceType: new ec2.InstanceType('t2.micro'), + }), + })); + + const environment = new Environment(stack, 'production', { + vpc, + cluster, + capacityType: EnvironmentCapacityType.EC2, + }); + const serviceDescription = new ServiceDescription(); + const taskRole = new iam.Role(stack, 'CustomTaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + serviceDescription.add(new Container({ + cpu: 256, + memoryMiB: 512, + trafficPort: 80, + image: ecs.ContainerImage.fromRegistry('nathanpeck/name'), + logGroup: new awslogs.LogGroup(stack, 'MyLogGroup'), + })); + + new Service(stack, 'my-service', { + environment, + serviceDescription, + taskRole, + }); + + // THEN + Template.fromStack(stack).resourceCountIs('AWS::ECS::Service', 1); + + // Ensure that the log group was created + Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 1); + + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Cpu: 256, + Essential: true, + Image: 'nathanpeck/name', + LogConfiguration: { + LogDriver: 'awslogs', + Options: { + 'awslogs-group': { + Ref: 'MyLogGroup5C0DAD85', + }, + 'awslogs-stream-prefix': 'my-service', + 'awslogs-region': { + Ref: 'AWS::Region', + }, + }, + }, + Memory: 512, + Name: 'app', + PortMappings: [ + { + ContainerPort: 80, + Protocol: 'tcp', + }, + ], + Ulimits: [ + { + HardLimit: 1024000, + Name: 'nofile', + SoftLimit: 1024000, + }, + ], + }, + ], + Cpu: '256', + Family: 'myservicetaskdefinition', + Memory: '512', + NetworkMode: 'awsvpc', + RequiresCompatibilities: [ + 'EC2', + 'FARGATE', + ], + TaskRoleArn: { + 'Fn::GetAtt': [ + 'CustomTaskRole3C6B13FD', + 'Arn', + ], + }, + }); + }); + + test('should error when log group is provided in the container extension and another observability extension is added', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const environment = new Environment(stack, 'production'); + const serviceDescription = new ServiceDescription(); + + serviceDescription.add(new Container({ + cpu: 256, + memoryMiB: 512, + trafficPort: 80, + image: ecs.ContainerImage.fromRegistry('nathanpeck/name'), + logGroup: new awslogs.LogGroup(stack, 'MyLogGroup'), + })); + serviceDescription.add(new FireLensExtension()); + + // THEN + expect(() => { + new Service(stack, 'my-service', { + environment, + serviceDescription, + }); + }).toThrow(/Log configuration already specified. You cannot provide a log group for the application container of service 'my-service' while also adding log configuration separately using service extensions./); + }); +}); diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/environment.test.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/environment.test.ts index 443c9fa9f9f68..dc49c70a1b9d8 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/environment.test.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/environment.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; @@ -27,9 +27,9 @@ describe('environment', () => { }); // THEN - expect(stack).toCountResources('AWS::ECS::Service', 1); + Template.fromStack(stack).resourceCountIs('AWS::ECS::Service', 1); - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Cpu: 256, @@ -67,8 +67,6 @@ describe('environment', () => { ], }, }); - - }); test('should be able to create a Fargate environment with a given VPC and cluster', () => { @@ -98,9 +96,9 @@ describe('environment', () => { }); // THEN - expect(stack).toCountResources('AWS::ECS::Service', 1); + Template.fromStack(stack).resourceCountIs('AWS::ECS::Service', 1); - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Cpu: 256, @@ -138,8 +136,6 @@ describe('environment', () => { ], }, }); - - }); test('should be able to create an environment for EC2', () => { @@ -177,9 +173,9 @@ describe('environment', () => { }); // THEN - expect(stack).toCountResources('AWS::ECS::Service', 1); + Template.fromStack(stack).resourceCountIs('AWS::ECS::Service', 1); - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Cpu: 256, @@ -217,8 +213,6 @@ describe('environment', () => { ], }, }); - - }); test('should be able to create an environment from attributes', () => { @@ -246,7 +240,5 @@ describe('environment', () => { expect(environment.cluster).toEqual(cluster); expect(environment.vpc).toEqual(vpc); expect(environment.id).toEqual('Environment'); - - }); }); diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/firelens.test.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/firelens.test.ts index 2f30e6139e33f..6cdd83d07610c 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/firelens.test.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/firelens.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ecs from '@aws-cdk/aws-ecs'; import * as cdk from '@aws-cdk/core'; import { Container, Environment, FireLensExtension, Service, ServiceDescription } from '../lib'; @@ -29,11 +29,11 @@ describe('firelens', () => { // THEN // Ensure that the log group was created - expect(stack).toHaveResource('AWS::Logs::LogGroup'); + Template.fromStack(stack).hasResource('AWS::Logs::LogGroup', Match.anyValue()); // Ensure that task has a Firelens sidecar and a log configuration // pointing at the sidecar - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Cpu: 256, @@ -114,8 +114,5 @@ describe('firelens', () => { ], }, }); - - }); - }); \ No newline at end of file diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/http-load-balancer.test.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/http-load-balancer.test.ts index e06c1632d93b5..8af4ef5d44ab6 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/http-load-balancer.test.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/http-load-balancer.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ecs from '@aws-cdk/aws-ecs'; import * as cdk from '@aws-cdk/core'; import { Container, Environment, HttpLoadBalancerExtension, Service, ServiceDescription } from '../lib'; @@ -27,7 +27,7 @@ describe('http load balancer', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Cpu: 256, @@ -66,10 +66,8 @@ describe('http load balancer', () => { }, }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer'); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener'); - - + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::LoadBalancer', 1); + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::Listener', 1); }); test('allows scaling on request count for the HTTP load balancer', () => { @@ -98,12 +96,12 @@ describe('http load balancer', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { MaxCapacity: 5, MinCapacity: 1, }); - expect(stack).toHaveResourceLike('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { @@ -139,5 +137,4 @@ describe('http load balancer', () => { }); }).toThrow(/Auto scaling target for the service 'my-service' hasn't been configured. Please use Service construct to configure 'minTaskCount' and 'maxTaskCount'./); }); - }); \ No newline at end of file diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/injecter.test.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/injecter.test.ts index 3cfd8f4918f48..e52c498f17455 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/injecter.test.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/injecter.test.ts @@ -1,4 +1,4 @@ -import { countResources, expect, haveResource } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as ecs from '@aws-cdk/aws-ecs'; import * as sns from '@aws-cdk/aws-sns'; import * as cdk from '@aws-cdk/core'; @@ -42,10 +42,10 @@ describe('injecter', () => { // THEN // Ensure creation of provided topics - expect(stack).to(countResources('AWS::SNS::Topic', 2)); + Template.fromStack(stack).resourceCountIs('AWS::SNS::Topic', 2); // Ensure the task role is given permissions to publish events to topics - expect(stack).to(haveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -65,10 +65,10 @@ describe('injecter', () => { ], Version: '2012-10-17', }, - })); + }); // Ensure that the topic ARNs have been correctly appended to the environment variables - expect(stack).to(haveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Cpu: 256, @@ -109,6 +109,6 @@ describe('injecter', () => { ], }, ], - })); + }); }); }); \ No newline at end of file diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json index bd9224e586933..04e3fb87689fa 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json @@ -3185,22 +3185,12 @@ "Backends": [ { "VirtualService": { - "VirtualServiceName": { - "Fn::GetAtt": [ - "namevirtualservice3DDDDF1E", - "VirtualServiceName" - ] - } + "VirtualServiceName": "name.production" } }, { "VirtualService": { - "VirtualServiceName": { - "Fn::GetAtt": [ - "greetingvirtualservice60AD3AD9", - "VirtualServiceName" - ] - } + "VirtualServiceName": "greeting.production" } } ], diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.assign-public-ip.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.assign-public-ip.expected.json index faee3f71397ab..556524b695267 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.assign-public-ip.expected.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.assign-public-ip.expected.json @@ -291,6 +291,18 @@ ], "Essential": true, "Image": "nathanpeck/name", + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "namelogsF4B17D31" + }, + "awslogs-stream-prefix": "name", + "awslogs-region": { + "Ref": "AWS::Region" + } + } + }, "Memory": 512, "Name": "app", "PortMappings": [ @@ -309,6 +321,12 @@ } ], "Cpu": "256", + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "nametaskdefinitionExecutionRole45AC5C9A", + "Arn" + ] + }, "Family": "awsecsintegnametaskdefinition0EA6A1A0", "Memory": "512", "NetworkMode": "awsvpc", @@ -329,6 +347,71 @@ "nameserviceTaskRecordManagerRuleStopped66D08B70" ] }, + "nametaskdefinitionExecutionRole45AC5C9A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "DependsOn": [ + "nameserviceTaskRecordManagerCleanupE19F1043", + "nameserviceTaskRecordManagerRuleRunningCD85F46F", + "nameserviceTaskRecordManagerRuleStopped66D08B70" + ] + }, + "nametaskdefinitionExecutionRoleDefaultPolicyF7942D20": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "namelogsF4B17D31", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "nametaskdefinitionExecutionRoleDefaultPolicyF7942D20", + "Roles": [ + { + "Ref": "nametaskdefinitionExecutionRole45AC5C9A" + } + ] + }, + "DependsOn": [ + "nameserviceTaskRecordManagerCleanupE19F1043", + "nameserviceTaskRecordManagerRuleRunningCD85F46F", + "nameserviceTaskRecordManagerRuleStopped66D08B70" + ] + }, + "namelogsF4B17D31": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "name-logs", + "RetentionInDays": 30 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "nameserviceService8015C8D6": { "Type": "AWS::ECS::Service", "Properties": { @@ -714,7 +797,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters1cd4a64795df8938c7ff3d71caa4b3fd27d3d5caa222517813b08ae2a6494d3eS3Bucket1AECFCFD" + "Ref": "AssetParameters8f06a3db22794ebc7ff89b4745fd706afd46e17816fe46da72e5125cabae725dS3Bucket1E7F92B6" }, "S3Key": { "Fn::Join": [ @@ -727,7 +810,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1cd4a64795df8938c7ff3d71caa4b3fd27d3d5caa222517813b08ae2a6494d3eS3VersionKey2ACFB47F" + "Ref": "AssetParameters8f06a3db22794ebc7ff89b4745fd706afd46e17816fe46da72e5125cabae725dS3VersionKey86FCA825" } ] } @@ -740,7 +823,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1cd4a64795df8938c7ff3d71caa4b3fd27d3d5caa222517813b08ae2a6494d3eS3VersionKey2ACFB47F" + "Ref": "AssetParameters8f06a3db22794ebc7ff89b4745fd706afd46e17816fe46da72e5125cabae725dS3VersionKey86FCA825" } ] } @@ -877,7 +960,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters1cd4a64795df8938c7ff3d71caa4b3fd27d3d5caa222517813b08ae2a6494d3eS3Bucket1AECFCFD" + "Ref": "AssetParameters8f06a3db22794ebc7ff89b4745fd706afd46e17816fe46da72e5125cabae725dS3Bucket1E7F92B6" }, "S3Key": { "Fn::Join": [ @@ -890,7 +973,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1cd4a64795df8938c7ff3d71caa4b3fd27d3d5caa222517813b08ae2a6494d3eS3VersionKey2ACFB47F" + "Ref": "AssetParameters8f06a3db22794ebc7ff89b4745fd706afd46e17816fe46da72e5125cabae725dS3VersionKey86FCA825" } ] } @@ -903,7 +986,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1cd4a64795df8938c7ff3d71caa4b3fd27d3d5caa222517813b08ae2a6494d3eS3VersionKey2ACFB47F" + "Ref": "AssetParameters8f06a3db22794ebc7ff89b4745fd706afd46e17816fe46da72e5125cabae725dS3VersionKey86FCA825" } ] } @@ -1190,7 +1273,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters6ee0a36dd10d630708c265bcf7616c64030040c1bbc383b34150db74b744cad2S3BucketF482197E" + "Ref": "AssetParameters3744fa896361f81b76b1efde632ac07b1920ce09a4ca1ff15ab486f262a19b87S3Bucket36F31A16" }, "S3Key": { "Fn::Join": [ @@ -1203,7 +1286,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters6ee0a36dd10d630708c265bcf7616c64030040c1bbc383b34150db74b744cad2S3VersionKey38B69632" + "Ref": "AssetParameters3744fa896361f81b76b1efde632ac07b1920ce09a4ca1ff15ab486f262a19b87S3VersionKeyF80D542B" } ] } @@ -1216,7 +1299,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters6ee0a36dd10d630708c265bcf7616c64030040c1bbc383b34150db74b744cad2S3VersionKey38B69632" + "Ref": "AssetParameters3744fa896361f81b76b1efde632ac07b1920ce09a4ca1ff15ab486f262a19b87S3VersionKeyF80D542B" } ] } @@ -1242,17 +1325,17 @@ } }, "Parameters": { - "AssetParameters1cd4a64795df8938c7ff3d71caa4b3fd27d3d5caa222517813b08ae2a6494d3eS3Bucket1AECFCFD": { + "AssetParameters8f06a3db22794ebc7ff89b4745fd706afd46e17816fe46da72e5125cabae725dS3Bucket1E7F92B6": { "Type": "String", - "Description": "S3 bucket for asset \"1cd4a64795df8938c7ff3d71caa4b3fd27d3d5caa222517813b08ae2a6494d3e\"" + "Description": "S3 bucket for asset \"8f06a3db22794ebc7ff89b4745fd706afd46e17816fe46da72e5125cabae725d\"" }, - "AssetParameters1cd4a64795df8938c7ff3d71caa4b3fd27d3d5caa222517813b08ae2a6494d3eS3VersionKey2ACFB47F": { + "AssetParameters8f06a3db22794ebc7ff89b4745fd706afd46e17816fe46da72e5125cabae725dS3VersionKey86FCA825": { "Type": "String", - "Description": "S3 key for asset version \"1cd4a64795df8938c7ff3d71caa4b3fd27d3d5caa222517813b08ae2a6494d3e\"" + "Description": "S3 key for asset version \"8f06a3db22794ebc7ff89b4745fd706afd46e17816fe46da72e5125cabae725d\"" }, - "AssetParameters1cd4a64795df8938c7ff3d71caa4b3fd27d3d5caa222517813b08ae2a6494d3eArtifactHashC1CF90D5": { + "AssetParameters8f06a3db22794ebc7ff89b4745fd706afd46e17816fe46da72e5125cabae725dArtifactHash0F81F2AB": { "Type": "String", - "Description": "Artifact hash for asset \"1cd4a64795df8938c7ff3d71caa4b3fd27d3d5caa222517813b08ae2a6494d3e\"" + "Description": "Artifact hash for asset \"8f06a3db22794ebc7ff89b4745fd706afd46e17816fe46da72e5125cabae725d\"" }, "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1": { "Type": "String", @@ -1266,17 +1349,17 @@ "Type": "String", "Description": "Artifact hash for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" }, - "AssetParameters6ee0a36dd10d630708c265bcf7616c64030040c1bbc383b34150db74b744cad2S3BucketF482197E": { + "AssetParameters3744fa896361f81b76b1efde632ac07b1920ce09a4ca1ff15ab486f262a19b87S3Bucket36F31A16": { "Type": "String", - "Description": "S3 bucket for asset \"6ee0a36dd10d630708c265bcf7616c64030040c1bbc383b34150db74b744cad2\"" + "Description": "S3 bucket for asset \"3744fa896361f81b76b1efde632ac07b1920ce09a4ca1ff15ab486f262a19b87\"" }, - "AssetParameters6ee0a36dd10d630708c265bcf7616c64030040c1bbc383b34150db74b744cad2S3VersionKey38B69632": { + "AssetParameters3744fa896361f81b76b1efde632ac07b1920ce09a4ca1ff15ab486f262a19b87S3VersionKeyF80D542B": { "Type": "String", - "Description": "S3 key for asset version \"6ee0a36dd10d630708c265bcf7616c64030040c1bbc383b34150db74b744cad2\"" + "Description": "S3 key for asset version \"3744fa896361f81b76b1efde632ac07b1920ce09a4ca1ff15ab486f262a19b87\"" }, - "AssetParameters6ee0a36dd10d630708c265bcf7616c64030040c1bbc383b34150db74b744cad2ArtifactHash4BE92B79": { + "AssetParameters3744fa896361f81b76b1efde632ac07b1920ce09a4ca1ff15ab486f262a19b87ArtifactHash40DDF5EE": { "Type": "String", - "Description": "Artifact hash for asset \"6ee0a36dd10d630708c265bcf7616c64030040c1bbc383b34150db74b744cad2\"" + "Description": "Artifact hash for asset \"3744fa896361f81b76b1efde632ac07b1920ce09a4ca1ff15ab486f262a19b87\"" } }, "Outputs": { diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.custom-service-extension.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.custom-service-extension.expected.json index 5fbaf177162df..af4f8829a1501 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.custom-service-extension.expected.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.custom-service-extension.expected.json @@ -548,6 +548,18 @@ ], "Essential": true, "Image": "nathanpeck/name", + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "namelogsF4B17D31" + }, + "awslogs-stream-prefix": "name", + "awslogs-region": { + "Ref": "AWS::Region" + } + } + }, "Memory": 2048, "Name": "app", "PortMappings": [ @@ -566,6 +578,12 @@ } ], "Cpu": "1024", + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "nametaskdefinitionExecutionRole45AC5C9A", + "Arn" + ] + }, "Family": "awsecsintegnametaskdefinition0EA6A1A0", "Memory": "2048", "NetworkMode": "awsvpc", @@ -581,6 +599,61 @@ } } }, + "nametaskdefinitionExecutionRole45AC5C9A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "nametaskdefinitionExecutionRoleDefaultPolicyF7942D20": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "namelogsF4B17D31", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "nametaskdefinitionExecutionRoleDefaultPolicyF7942D20", + "Roles": [ + { + "Ref": "nametaskdefinitionExecutionRole45AC5C9A" + } + ] + } + }, + "namelogsF4B17D31": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "name-logs", + "RetentionInDays": 30 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "nameserviceService8015C8D6": { "Type": "AWS::ECS::Service", "Properties": { diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.imported-environment.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.imported-environment.expected.json index c85f5d9b0231d..48775db8f3c84 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.imported-environment.expected.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.imported-environment.expected.json @@ -209,6 +209,18 @@ ], "Essential": true, "Image": "nathanpeck/name", + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "Servicelogs9F4E1F70" + }, + "awslogs-stream-prefix": "Service", + "awslogs-region": { + "Ref": "AWS::Region" + } + } + }, "Memory": 512, "Name": "app", "PortMappings": [ @@ -227,6 +239,12 @@ } ], "Cpu": "256", + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "ServicetaskdefinitionExecutionRoleD09F4578", + "Arn" + ] + }, "Family": "importedenvironmentintegServicetaskdefinition63936B87", "Memory": "512", "NetworkMode": "awsvpc", @@ -242,6 +260,61 @@ } } }, + "ServicetaskdefinitionExecutionRoleD09F4578": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ServicetaskdefinitionExecutionRoleDefaultPolicy25CEAFC5": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Servicelogs9F4E1F70", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ServicetaskdefinitionExecutionRoleDefaultPolicy25CEAFC5", + "Roles": [ + { + "Ref": "ServicetaskdefinitionExecutionRoleD09F4578" + } + ] + } + }, + "Servicelogs9F4E1F70": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "Service-logs", + "RetentionInDays": 30 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "ServiceserviceService6A153CB8": { "Type": "AWS::ECS::Service", "Properties": { diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json index 91ce21c4a2c5f..fa97c69cefc1c 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json @@ -1103,6 +1103,18 @@ ], "Essential": true, "Image": "nathanpeck/name", + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "nameproductionlogsD0BFFE8C" + }, + "awslogs-stream-prefix": "name-production", + "awslogs-region": { + "Ref": "AWS::Region" + } + } + }, "Memory": 2048, "Name": "app", "PortMappings": [ @@ -1266,11 +1278,6 @@ } } }, - "nameproductiontaskdefinitionenvoyLogGroupF79A2732": { - "Type": "AWS::Logs::LogGroup", - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" - }, "nameproductiontaskdefinitionExecutionRoleB72DD86B": { "Type": "AWS::IAM::Role", "Properties": { @@ -1293,6 +1300,19 @@ "Properties": { "PolicyDocument": { "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "nameproductionlogsD0BFFE8C", + "Arn" + ] + } + }, { "Action": [ "ecr:BatchCheckLayerAvailability", @@ -1356,6 +1376,20 @@ ] } }, + "nameproductiontaskdefinitionenvoyLogGroupF79A2732": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "nameproductionlogsD0BFFE8C": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "name-production-logs", + "RetentionInDays": 30 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "nameproductionenvoytoappmesh1B44B04A": { "Type": "AWS::IAM::Policy", "Properties": { @@ -1631,6 +1665,18 @@ ], "Essential": true, "Image": "nathanpeck/name", + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "namedevelopmentlogs108670CC" + }, + "awslogs-stream-prefix": "name-development", + "awslogs-region": { + "Ref": "AWS::Region" + } + } + }, "Memory": 2048, "Name": "app", "PortMappings": [ @@ -1794,11 +1840,6 @@ } } }, - "namedevelopmenttaskdefinitionenvoyLogGroupF8FCAFD6": { - "Type": "AWS::Logs::LogGroup", - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" - }, "namedevelopmenttaskdefinitionExecutionRole48B53E4E": { "Type": "AWS::IAM::Role", "Properties": { @@ -1821,6 +1862,19 @@ "Properties": { "PolicyDocument": { "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "namedevelopmentlogs108670CC", + "Arn" + ] + } + }, { "Action": [ "ecr:BatchCheckLayerAvailability", @@ -1884,6 +1938,20 @@ ] } }, + "namedevelopmenttaskdefinitionenvoyLogGroupF8FCAFD6": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "namedevelopmentlogs108670CC": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "name-development-logs", + "RetentionInDays": 30 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "namedevelopmentenvoytoappmesh45FF08AA": { "Type": "AWS::IAM::Policy", "Properties": { diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.publish-subscribe.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.publish-subscribe.expected.json index 465a7ce4b3374..24cd1d0a4e434 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.publish-subscribe.expected.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.publish-subscribe.expected.json @@ -596,6 +596,18 @@ ], "Essential": true, "Image": "nathanpeck/name", + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "PublisherlogsDF0C1067" + }, + "awslogs-stream-prefix": "Publisher", + "awslogs-region": { + "Ref": "AWS::Region" + } + } + }, "Memory": 512, "Name": "app", "PortMappings": [ @@ -614,6 +626,12 @@ } ], "Cpu": "256", + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "PublishertaskdefinitionExecutionRole5C00C542", + "Arn" + ] + }, "Family": "awsecsintegPublishertaskdefinitionD50610D0", "Memory": "512", "NetworkMode": "awsvpc", @@ -629,6 +647,61 @@ } } }, + "PublishertaskdefinitionExecutionRole5C00C542": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PublishertaskdefinitionExecutionRoleDefaultPolicy681FD8E6": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PublisherlogsDF0C1067", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PublishertaskdefinitionExecutionRoleDefaultPolicy681FD8E6", + "Roles": [ + { + "Ref": "PublishertaskdefinitionExecutionRole5C00C542" + } + ] + } + }, + "PublisherlogsDF0C1067": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "Publisher-logs", + "RetentionInDays": 30 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "PublisherserviceService9EB00F60": { "Type": "AWS::ECS::Service", "Properties": { @@ -901,6 +974,18 @@ ], "Essential": true, "Image": "nathanpeck/name", + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "Workerlogs2994AC4D" + }, + "awslogs-stream-prefix": "Worker", + "awslogs-region": { + "Ref": "AWS::Region" + } + } + }, "Memory": 512, "Name": "app", "PortMappings": [ @@ -919,6 +1004,12 @@ } ], "Cpu": "256", + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "WorkertaskdefinitionExecutionRole3C1A1848", + "Arn" + ] + }, "Family": "awsecsintegWorkertaskdefinition32B60762", "Memory": "512", "NetworkMode": "awsvpc", @@ -934,6 +1025,61 @@ } } }, + "WorkertaskdefinitionExecutionRole3C1A1848": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "WorkertaskdefinitionExecutionRoleDefaultPolicy6E199B19": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Workerlogs2994AC4D", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "WorkertaskdefinitionExecutionRoleDefaultPolicy6E199B19", + "Roles": [ + { + "Ref": "WorkertaskdefinitionExecutionRole3C1A1848" + } + ] + } + }, + "Workerlogs2994AC4D": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "Worker-logs", + "RetentionInDays": 30 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "WorkerserviceService68C5A5C3": { "Type": "AWS::ECS::Service", "Properties": { @@ -1184,7 +1330,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameterscc8d03e1cef62b38b47438d429cdc3828f57a52cffd1a84c4cda032bc21be19dS3Bucket151170D5" + "Ref": "AssetParametersa820140ad8525b8ed56ad2a7bcd9da99d6afc2490e8c91e34620886c011bdc91S3Bucket1FFDEA8D" }, "S3Key": { "Fn::Join": [ @@ -1197,7 +1343,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterscc8d03e1cef62b38b47438d429cdc3828f57a52cffd1a84c4cda032bc21be19dS3VersionKey3D692C3D" + "Ref": "AssetParametersa820140ad8525b8ed56ad2a7bcd9da99d6afc2490e8c91e34620886c011bdc91S3VersionKeyA60C027B" } ] } @@ -1210,7 +1356,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterscc8d03e1cef62b38b47438d429cdc3828f57a52cffd1a84c4cda032bc21be19dS3VersionKey3D692C3D" + "Ref": "AssetParametersa820140ad8525b8ed56ad2a7bcd9da99d6afc2490e8c91e34620886c011bdc91S3VersionKeyA60C027B" } ] } @@ -1326,17 +1472,17 @@ } }, "Parameters": { - "AssetParameterscc8d03e1cef62b38b47438d429cdc3828f57a52cffd1a84c4cda032bc21be19dS3Bucket151170D5": { + "AssetParametersa820140ad8525b8ed56ad2a7bcd9da99d6afc2490e8c91e34620886c011bdc91S3Bucket1FFDEA8D": { "Type": "String", - "Description": "S3 bucket for asset \"cc8d03e1cef62b38b47438d429cdc3828f57a52cffd1a84c4cda032bc21be19d\"" + "Description": "S3 bucket for asset \"a820140ad8525b8ed56ad2a7bcd9da99d6afc2490e8c91e34620886c011bdc91\"" }, - "AssetParameterscc8d03e1cef62b38b47438d429cdc3828f57a52cffd1a84c4cda032bc21be19dS3VersionKey3D692C3D": { + "AssetParametersa820140ad8525b8ed56ad2a7bcd9da99d6afc2490e8c91e34620886c011bdc91S3VersionKeyA60C027B": { "Type": "String", - "Description": "S3 key for asset version \"cc8d03e1cef62b38b47438d429cdc3828f57a52cffd1a84c4cda032bc21be19d\"" + "Description": "S3 key for asset version \"a820140ad8525b8ed56ad2a7bcd9da99d6afc2490e8c91e34620886c011bdc91\"" }, - "AssetParameterscc8d03e1cef62b38b47438d429cdc3828f57a52cffd1a84c4cda032bc21be19dArtifactHash167B1C30": { + "AssetParametersa820140ad8525b8ed56ad2a7bcd9da99d6afc2490e8c91e34620886c011bdc91ArtifactHashC1953821": { "Type": "String", - "Description": "Artifact hash for asset \"cc8d03e1cef62b38b47438d429cdc3828f57a52cffd1a84c4cda032bc21be19d\"" + "Description": "Artifact hash for asset \"a820140ad8525b8ed56ad2a7bcd9da99d6afc2490e8c91e34620886c011bdc91\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/queue.test.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/queue.test.ts index 7aa00581361d9..dfc6bd8a9bcdd 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/queue.test.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/queue.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ecs from '@aws-cdk/aws-ecs'; import * as sns from '@aws-cdk/aws-sns'; import * as sqs from '@aws-cdk/aws-sqs'; @@ -33,11 +33,11 @@ describe('queue', () => { // THEN // Ensure creation of default queue and queue policy allowing SNS Topics to send message to the queue - expect(stack).toHaveResource('AWS::SQS::Queue', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::Queue', { MessageRetentionPeriod: 1209600, }); - expect(stack).toHaveResource('AWS::SQS::Queue', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::Queue', { RedrivePolicy: { deadLetterTargetArn: { 'Fn::GetAtt': [ @@ -50,7 +50,7 @@ describe('queue', () => { }); // Ensure the task role is given permissions to consume messages from the queue - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -75,10 +75,10 @@ describe('queue', () => { }); // Ensure there are no SNS Subscriptions created - expect(stack).toCountResources('AWS::SNS::Subscription', 0); + Template.fromStack(stack).resourceCountIs('AWS::SNS::Subscription', 0); // Ensure that the queue URL has been correctly appended to the environment variables - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Cpu: 256, @@ -114,8 +114,6 @@ describe('queue', () => { }, ], }); - - }); test('should be able to subscribe default events queue created by the extension to given topics', () => { @@ -153,11 +151,11 @@ describe('queue', () => { // THEN // Ensure creation of default queue and queue policy allowing SNS Topics to send message to the queue - expect(stack).toHaveResource('AWS::SQS::Queue', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::Queue', { MessageRetentionPeriod: 1209600, }); - expect(stack).toHaveResource('AWS::SQS::Queue', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::Queue', { RedrivePolicy: { deadLetterTargetArn: { 'Fn::GetAtt': [ @@ -169,7 +167,7 @@ describe('queue', () => { }, }); - expect(stack).toHaveResource('AWS::SQS::QueuePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::QueuePolicy', { PolicyDocument: { Statement: [ { @@ -218,7 +216,7 @@ describe('queue', () => { }); // Ensure the task role is given permissions to consume messages from the queue - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -243,7 +241,7 @@ describe('queue', () => { }); // Ensure SNS Subscriptions for given topics - expect(stack).toHaveResource('AWS::SNS::Subscription', { + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { Protocol: 'sqs', TopicArn: { Ref: 'topic152D84A37', @@ -256,7 +254,7 @@ describe('queue', () => { }, }); - expect(stack).toHaveResource('AWS::SNS::Subscription', { + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { Protocol: 'sqs', TopicArn: { Ref: 'topic2A4FB547F', @@ -270,7 +268,7 @@ describe('queue', () => { }); // Ensure that the queue URL has been correctly appended to the environment variables - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Cpu: 256, @@ -306,8 +304,6 @@ describe('queue', () => { }, ], }); - - }); test('should be able to subscribe user-provided queue to given topics', () => { @@ -346,7 +342,7 @@ describe('queue', () => { // THEN // Ensure queue policy allows SNS Topics to send message to the queue - expect(stack).toHaveResource('AWS::SQS::QueuePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::QueuePolicy', { PolicyDocument: { Statement: [ { @@ -374,7 +370,7 @@ describe('queue', () => { }, }); - expect(stack).toHaveResource('AWS::SQS::QueuePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::QueuePolicy', { PolicyDocument: { Statement: [ { @@ -403,7 +399,7 @@ describe('queue', () => { }); // Ensure the task role is given permissions to consume messages from the queue - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -444,7 +440,7 @@ describe('queue', () => { }); // Ensure SNS Subscriptions for given topics - expect(stack).toHaveResource('AWS::SNS::Subscription', { + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { Protocol: 'sqs', TopicArn: { Ref: 'topic152D84A37', @@ -457,7 +453,7 @@ describe('queue', () => { }, }); - expect(stack).toHaveResource('AWS::SNS::Subscription', { + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { Protocol: 'sqs', TopicArn: { Ref: 'topic2A4FB547F', @@ -471,7 +467,7 @@ describe('queue', () => { }); // Ensure that the queue URL has been correctly added to the environment variables - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Cpu: 256, @@ -562,12 +558,12 @@ describe('queue', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { MaxCapacity: 10, MinCapacity: 1, }); - expect(stack).toHaveResourceLike('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { CustomizedMetricSpecification: { @@ -639,12 +635,12 @@ describe('queue', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { MaxCapacity: 10, MinCapacity: 1, }); - expect(stack).toHaveResourceLike('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { CustomizedMetricSpecification: { @@ -668,7 +664,7 @@ describe('queue', () => { }, }); - expect(stack).toHaveResourceLike('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { CustomizedMetricSpecification: { @@ -692,7 +688,7 @@ describe('queue', () => { }, }); - expect(stack).toHaveResourceLike('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { CustomizedMetricSpecification: { diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/scale-on-cpu-utilization.test.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/scale-on-cpu-utilization.test.ts index 1b8c9f82fb1a6..bfcf67e394609 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/scale-on-cpu-utilization.test.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/scale-on-cpu-utilization.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ecs from '@aws-cdk/aws-ecs'; import * as cdk from '@aws-cdk/core'; import { Container, Environment, ScaleOnCpuUtilization, Service, ServiceDescription } from '../lib'; @@ -27,7 +27,7 @@ describe('scale on cpu utilization', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentConfiguration: { MaximumPercent: 200, MinimumHealthyPercent: 100, @@ -35,7 +35,7 @@ describe('scale on cpu utilization', () => { DesiredCount: 2, }); - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { MaxCapacity: 8, MinCapacity: 2, ResourceId: { @@ -76,7 +76,7 @@ describe('scale on cpu utilization', () => { ServiceNamespace: 'ecs', }); - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyName: 'myserviceserviceTaskCountTargetmyservicetargetcpuutilization50E6628660', PolicyType: 'TargetTrackingScaling', ScalingTargetId: { @@ -91,8 +91,6 @@ describe('scale on cpu utilization', () => { TargetValue: 50, }, }); - - }); test('should be able to set a custom scaling policy as well', () => { @@ -125,7 +123,7 @@ describe('scale on cpu utilization', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentConfiguration: { MaximumPercent: 200, MinimumHealthyPercent: 100, @@ -133,20 +131,45 @@ describe('scale on cpu utilization', () => { DesiredCount: 25, }); - expect(stack).toHaveResourceLike('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { MaxCapacity: 30, MinCapacity: 15, }); - expect(stack).toHaveResourceLike('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { TargetTrackingScalingPolicyConfiguration: { ScaleInCooldown: 180, ScaleOutCooldown: 180, TargetValue: 75, }, }); + }); + + test('should error if configuring autoscaling target both in the extension and the Service', () => { + // GIVEN + const stack = new cdk.Stack(); + // WHEN + const environment = new Environment(stack, 'production'); + const serviceDescription = new ServiceDescription(); - }); + serviceDescription.add(new Container({ + cpu: 256, + memoryMiB: 512, + trafficPort: 80, + image: ecs.ContainerImage.fromRegistry('nathanpeck/name'), + })); + serviceDescription.add(new ScaleOnCpuUtilization()); + // THEN + expect(() => { + new Service(stack, 'my-service', { + environment, + serviceDescription, + autoScaleTaskCount: { + maxTaskCount: 5, + }, + }); + }).toThrow('Cannot specify \'autoScaleTaskCount\' in the Service construct and also provide a \'ScaleOnCpuUtilization\' extension. \'ScaleOnCpuUtilization\' is deprecated. Please only provide \'autoScaleTaskCount\'.'); + }); }); \ No newline at end of file diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/service.test.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/service.test.ts index 39d26ef371f17..9879282f7e254 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/service.test.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/service.test.ts @@ -1,11 +1,7 @@ -import { ABSENT } 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 { Match, Template } from '@aws-cdk/assertions'; import * as ecs from '@aws-cdk/aws-ecs'; -import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { Container, EnvironmentCapacityType, Environment, Service, ServiceDescription } from '../lib'; +import { Container, Environment, Service, ServiceDescription } from '../lib'; describe('service', () => { test('should error if a service is prepared with no addons', () => { @@ -23,89 +19,6 @@ describe('service', () => { serviceDescription, }); }).toThrow(/Service 'my-service' must have a Container extension/); - - - }); - - test('should be able to add a container to the service', () => { - // GIVEN - const stack = new cdk.Stack(); - const vpc = new ec2.Vpc(stack, 'VPC'); - const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); - cluster.addAsgCapacityProvider(new ecs.AsgCapacityProvider(stack, 'Provider', { - autoScalingGroup: new autoscaling.AutoScalingGroup(stack, 'DefaultAutoScalingGroup', { - vpc, - machineImage: ec2.MachineImage.latestAmazonLinux(), - instanceType: new ec2.InstanceType('t2.micro'), - }), - })); - - const environment = new Environment(stack, 'production', { - vpc, - cluster, - capacityType: EnvironmentCapacityType.EC2, - }); - const serviceDescription = new ServiceDescription(); - const taskRole = new iam.Role(stack, 'CustomTaskRole', { - assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), - }); - - serviceDescription.add(new Container({ - cpu: 256, - memoryMiB: 512, - trafficPort: 80, - image: ecs.ContainerImage.fromRegistry('nathanpeck/name'), - })); - - new Service(stack, 'my-service', { - environment, - serviceDescription, - taskRole, - }); - - // THEN - expect(stack).toCountResources('AWS::ECS::Service', 1); - - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { - ContainerDefinitions: [ - { - Cpu: 256, - Essential: true, - Image: 'nathanpeck/name', - Memory: 512, - Name: 'app', - PortMappings: [ - { - ContainerPort: 80, - Protocol: 'tcp', - }, - ], - Ulimits: [ - { - HardLimit: 1024000, - Name: 'nofile', - SoftLimit: 1024000, - }, - ], - }, - ], - Cpu: '256', - Family: 'myservicetaskdefinition', - Memory: '512', - NetworkMode: 'awsvpc', - RequiresCompatibilities: [ - 'EC2', - 'FARGATE', - ], - TaskRoleArn: { - 'Fn::GetAtt': [ - 'CustomTaskRole3C6B13FD', - 'Arn', - ], - }, - }); - - }); test('allows scaling on a target CPU utilization', () => { @@ -133,16 +46,16 @@ describe('service', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { - DesiredCount: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + DesiredCount: Match.absent(), }); - expect(stack).toHaveResourceLike('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { MaxCapacity: 5, MinCapacity: 1, }); - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'ECSServiceAverageCPUUtilization' }, @@ -176,16 +89,16 @@ describe('service', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { - DesiredCount: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + DesiredCount: Match.absent(), }); - expect(stack).toHaveResourceLike('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { MaxCapacity: 5, MinCapacity: 1, }); - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'ECSServiceAverageMemoryUtilization' }, @@ -220,5 +133,4 @@ describe('service', () => { }); }).toThrow(/The auto scaling target for the service 'my-service' has been created but no auto scaling policies have been configured./); }); - }); \ No newline at end of file diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/xray.test.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/xray.test.ts index 6fce84bca7338..f8f096369d50b 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/xray.test.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/xray.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ecs from '@aws-cdk/aws-ecs'; import * as cdk from '@aws-cdk/core'; import { Container, Environment, XRayExtension, Service, ServiceDescription } from '../lib'; @@ -27,7 +27,7 @@ describe('xray', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Cpu: 256, @@ -114,8 +114,5 @@ describe('xray', () => { ], }, }); - - }); - }); \ No newline at end of file diff --git a/packages/@aws-cdk/alexa-ask/package.json b/packages/@aws-cdk/alexa-ask/package.json index c375bab31a3e0..2d533773cdc92 100644 --- a/packages/@aws-cdk/alexa-ask/package.json +++ b/packages/@aws-cdk/alexa-ask/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -77,7 +84,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/app-delivery/README.md b/packages/@aws-cdk/app-delivery/README.md index a60f4590dc39f..cd8e80e218bba 100644 --- a/packages/@aws-cdk/app-delivery/README.md +++ b/packages/@aws-cdk/app-delivery/README.md @@ -59,6 +59,10 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; import * as cdk from '@aws-cdk/core'; import * as cicd from '@aws-cdk/app-delivery'; +import * as iam from '@aws-cdk/aws-iam'; + +class MyServiceStackA extends cdk.Stack {} +class MyServiceStackB extends cdk.Stack {} const app = new cdk.App(); @@ -77,7 +81,9 @@ const sourceOutput = new codepipeline.Artifact(); const source = new codepipeline_actions.GitHubSourceAction({ actionName: 'GitHub', output: sourceOutput, - /* ... */ + owner: 'myName', + repo: 'myRepo', + oauthToken: cdk.SecretValue.plainText('secret'), }); pipeline.addStage({ stageName: 'source', @@ -129,10 +135,11 @@ deployStage.addAction(deployServiceAAction); // is passed to CloudFormation and needs the permissions necessary to deploy // stack. Alternatively you can enable [Administrator](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_job-functions.html#jf_administrator) permissions above, // users should understand the privileged nature of this role. -deployServiceAAction.addToRolePolicy(new iam.PolicyStatement({ - actions: ['service:SomeAction'], - resources: [myResource.myResourceArn], - // add more Action(s) and/or Resource(s) here, as needed +const myResourceArn = 'arn:partition:service:region:account-id:resource-id'; +deployServiceAAction.addToDeploymentRolePolicy(new iam.PolicyStatement({ + actions: ['service:SomeAction'], + resources: [myResourceArn], + // add more Action(s) and/or Resource(s) here, as needed })); const serviceStackB = new MyServiceStackB(app, 'ServiceStackB', { /* ... */ }); diff --git a/packages/@aws-cdk/app-delivery/package.json b/packages/@aws-cdk/app-delivery/package.json index 1ab7b169360a4..cb0dc160c9850 100644 --- a/packages/@aws-cdk/app-delivery/package.json +++ b/packages/@aws-cdk/app-delivery/package.json @@ -29,7 +29,14 @@ } }, "outdir": "dist", - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "scripts": { "build": "cdk-build", @@ -60,14 +67,14 @@ "constructs": "^3.3.69" }, "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "fast-check": "^2.20.0", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "fast-check": "^2.22.0", + "jest": "^27.5.1" }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/app-delivery/test/integ.cicd.expected.json b/packages/@aws-cdk/app-delivery/test/integ.cicd.expected.json index 9b2ed51da9ff2..9d53a0d8b915c 100644 --- a/packages/@aws-cdk/app-delivery/test/integ.cicd.expected.json +++ b/packages/@aws-cdk/app-delivery/test/integ.cicd.expected.json @@ -34,6 +34,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/app-delivery/test/pipeline-deploy-stack-action.test.ts b/packages/@aws-cdk/app-delivery/test/pipeline-deploy-stack-action.test.ts index b0c8e4f941a04..ccd9c230fe284 100644 --- a/packages/@aws-cdk/app-delivery/test/pipeline-deploy-stack-action.test.ts +++ b/packages/@aws-cdk/app-delivery/test/pipeline-deploy-stack-action.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { isSuperObject } from '@aws-cdk/assert-internal'; +import { Match, Matcher, Template } from '@aws-cdk/assertions'; import * as cfn from '@aws-cdk/aws-cloudformation'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; @@ -135,56 +134,43 @@ describeDeprecated('pipeline deploy stack action', () => { capabilities: [cfn.CloudFormationCapabilities.ANONYMOUS_IAM, cfn.CloudFormationCapabilities.AUTO_EXPAND], adminPermissions: false, })); - expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ - Configuration: { - StackName: 'TestStack', - ActionMode: 'CHANGE_SET_REPLACE', - Capabilities: 'CAPABILITY_NAMED_IAM', - }, + + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', hasPipelineActionConfiguration({ + StackName: 'TestStack', + ActionMode: 'CHANGE_SET_REPLACE', + Capabilities: 'CAPABILITY_NAMED_IAM', })); - expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ - Configuration: { - StackName: 'AnonymousIAM', - ActionMode: 'CHANGE_SET_REPLACE', - Capabilities: 'CAPABILITY_IAM', - }, + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', hasPipelineActionConfiguration({ + StackName: 'AnonymousIAM', + ActionMode: 'CHANGE_SET_REPLACE', + Capabilities: 'CAPABILITY_IAM', })); - expect(pipelineStack).not.toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ - Configuration: { - StackName: 'NoCapStack', - ActionMode: 'CHANGE_SET_REPLACE', - Capabilities: 'CAPABILITY_NAMED_IAM', - }, + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', Match.not(hasPipelineActionConfiguration({ + StackName: 'NoCapStack', + ActionMode: 'CHANGE_SET_REPLACE', + Capabilities: 'CAPABILITY_NAMED_IAM', + }))); + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', Match.not(hasPipelineActionConfiguration({ + StackName: 'NoCapStack', + ActionMode: 'CHANGE_SET_REPLACE', + Capabilities: 'CAPABILITY_IAM', + }))); + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', hasPipelineActionConfiguration({ + StackName: 'NoCapStack', + ActionMode: 'CHANGE_SET_REPLACE', })); - expect(pipelineStack).not.toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ - Configuration: { - StackName: 'NoCapStack', - ActionMode: 'CHANGE_SET_REPLACE', - Capabilities: 'CAPABILITY_IAM', - }, + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', hasPipelineActionConfiguration({ + StackName: 'AutoExpand', + ActionMode: 'CHANGE_SET_REPLACE', + Capabilities: 'CAPABILITY_AUTO_EXPAND', })); - expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ - Configuration: { - StackName: 'NoCapStack', - ActionMode: 'CHANGE_SET_REPLACE', - }, + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', hasPipelineActionConfiguration({ + StackName: 'AnonymousIAMAndAutoExpand', + ActionMode: 'CHANGE_SET_REPLACE', + Capabilities: 'CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND', })); - expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ - Configuration: { - StackName: 'AutoExpand', - ActionMode: 'CHANGE_SET_REPLACE', - Capabilities: 'CAPABILITY_AUTO_EXPAND', - }, - })); - expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ - Configuration: { - StackName: 'AnonymousIAMAndAutoExpand', - ActionMode: 'CHANGE_SET_REPLACE', - Capabilities: 'CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND', - }, - })); - }); + test('users can use admin permissions', () => { const pipelineStack = getTestStack(); const selfUpdatingStack = createSelfUpdatingStack(pipelineStack); @@ -196,7 +182,7 @@ describeDeprecated('pipeline deploy stack action', () => { input: selfUpdatingStack.synthesizedApp, adminPermissions: true, })); - expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -251,15 +237,13 @@ describeDeprecated('pipeline deploy stack action', () => { ], }, }); - expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ - Configuration: { - StackName: 'TestStack', - ActionMode: 'CHANGE_SET_REPLACE', - Capabilities: 'CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND', - }, + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', hasPipelineActionConfiguration({ + StackName: 'TestStack', + ActionMode: 'CHANGE_SET_REPLACE', + Capabilities: 'CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND', })); - }); + test('users can supply a role for deploy action', () => { const pipelineStack = getTestStack(); const selfUpdatingStack = createSelfUpdatingStack(pipelineStack); @@ -313,7 +297,7 @@ describeDeprecated('pipeline deploy stack action', () => { })); // THEN // - expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -391,7 +375,7 @@ describeDeprecated('pipeline deploy stack action', () => { const app = new cdk.App(); const deployedStack = new cdk.Stack(app, 'DeployedStack'); - for (let i = 0 ; i < assetCount ; i++) { + for (let i = 0; i < assetCount; i++) { deployedStack.node.addMetadata(cxschema.ArtifactMetadataEntryType.ASSET, {}); } @@ -406,7 +390,6 @@ describeDeprecated('pipeline deploy stack action', () => { }, ), ); - }); test('allows overriding the ChangeSet and Execute action names', () => { @@ -425,25 +408,21 @@ describeDeprecated('pipeline deploy stack action', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: [ - {}, - {}, - { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([ + Match.objectLike({ Name: 'Deploy', - Actions: [ - { + Actions: Match.arrayWith([ + Match.objectLike({ Name: 'Prepare', - }, - { + }), + Match.objectLike({ Name: 'Deploy', - }, - ], - }, - ], + }), + ]), + }), + ]), }); - - }); }); @@ -481,7 +460,7 @@ function createSelfUpdatingStack(pipelineStack: cdk.Stack): SelfUpdatingPipeline }); // simple source - const bucket = s3.Bucket.fromBucketArn( pipeline, 'PatternBucket', 'arn:aws:s3:::totally-fake-bucket'); + const bucket = s3.Bucket.fromBucketArn(pipeline, 'PatternBucket', 'arn:aws:s3:::totally-fake-bucket'); const sourceOutput = new codepipeline.Artifact('SourceOutput'); const sourceAction = new cpactions.S3SourceAction({ actionName: 'S3Source', @@ -509,15 +488,16 @@ function createSelfUpdatingStack(pipelineStack: cdk.Stack): SelfUpdatingPipeline return { synthesizedApp: buildOutput, pipeline }; } -function hasPipelineAction(expectedAction: any): (props: any) => boolean { - return (props: any) => { - for (const stage of props.Stages) { - for (const action of stage.Actions) { - if (isSuperObject(action, expectedAction, [], true)) { - return true; - } - } - } - return false; - }; -} +function hasPipelineActionConfiguration(expectedActionConfiguration: any): Matcher { + return Match.objectLike({ + Stages: Match.arrayWith([ + Match.objectLike({ + Actions: Match.arrayWith([ + Match.objectLike({ + Configuration: expectedActionConfiguration, + }), + ]), + }), + ]), + }); +} \ No newline at end of file diff --git a/packages/@aws-cdk/assert-internal/README.md b/packages/@aws-cdk/assert-internal/README.md index a74f9304f60d6..60490bbb7108f 100644 --- a/packages/@aws-cdk/assert-internal/README.md +++ b/packages/@aws-cdk/assert-internal/README.md @@ -3,15 +3,14 @@ --- -![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) +![Deprecated](https://img.shields.io/badge/deprecated-critical.svg?style=for-the-badge) -> The APIs of higher level constructs in this module are experimental and under active development. -> They are subject to non-backward compatible changes or removal in any future version. These are -> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be -> announced in the release notes. This means that while you may use them, you may need to update -> your source code when upgrading to a newer version of this package. + > This API may emit warnings. Backward compatibility is not guaranteed. -If using monocdk, use [@monocdk-experiment/assert](https://www.npmjs.com/package/@monocdk-experiment/assert) instead. +## Replacement recommended + +This library has been deprecated. We recommend you use the +[@aws-cdk/assertions](https://docs.aws.amazon.com/cdk/api/v1/docs/assertions-readme.html) module instead. --- diff --git a/packages/@aws-cdk/assert-internal/package.json b/packages/@aws-cdk/assert-internal/package.json index 2fc8d38ae228d..b1074a77bd1a4 100644 --- a/packages/@aws-cdk/assert-internal/package.json +++ b/packages/@aws-cdk/assert-internal/package.json @@ -26,9 +26,9 @@ "devDependencies": { "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5", - "ts-jest": "^27.1.2" + "@types/jest": "^27.4.1", + "jest": "^27.5.1", + "ts-jest": "^27.1.3" }, "dependencies": { "@aws-cdk/cloud-assembly-schema": "0.0.0", @@ -40,7 +40,7 @@ "peerDependencies": { "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69", - "jest": "^27.4.5" + "jest": "^27.5.1" }, "repository": { "url": "https://github.com/aws/aws-cdk.git", @@ -55,8 +55,8 @@ "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, - "stability": "experimental", - "maturity": "experimental", + "stability": "deprecated", + "maturity": "deprecated", "publishConfig": { "tag": "latest" }, diff --git a/packages/@aws-cdk/assert/package.json b/packages/@aws-cdk/assert/package.json index 774e1e459f410..dc22fd90c4d0d 100644 --- a/packages/@aws-cdk/assert/package.json +++ b/packages/@aws-cdk/assert/package.json @@ -37,11 +37,11 @@ "@aws-cdk/assert-internal": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "aws-cdk-migration": "0.0.0", "constructs": "^3.3.69", "jest": "^27.3.1", - "ts-jest": "^27.1.2" + "ts-jest": "^27.1.3" }, "dependencies": { "@aws-cdk/cloudformation-diff": "0.0.0", @@ -71,8 +71,8 @@ "exclude": true }, "nozem": false, - "stability": "experimental", - "maturity": "developer-preview", + "stability": "deprecated", + "maturity": "deprecated", "publishConfig": { "tag": "latest-1" } diff --git a/packages/@aws-cdk/assertions/MIGRATING.md b/packages/@aws-cdk/assertions/MIGRATING.md new file mode 100644 index 0000000000000..afa40bf9b1c5f --- /dev/null +++ b/packages/@aws-cdk/assertions/MIGRATING.md @@ -0,0 +1,123 @@ +# Migrating to Assertions + +Most of the APIs in the old `assert` module has a corresponding API in `assertions`. +Make the following modifications to your CDK test files to migrate to the +`@aws-cdk/assertions` module. + +For a migration script that handles most common use cases for you, see +[Migration Script](migration-script). + +## Translation Guide + +- Rewrite module imports that use `@aws-cdk/aws-assert` to `@aws-cdk/aws-assertions`. + For example: + + ```ts + import '@aws-cdk/assert/jest'; + import { ABSENT, SynthUtils, ResourcePart } from '@aws-cdk/assert'; + ``` + + ...becomes... + + ```ts + import { Template } from '@aws-cdk/assertions'; + import { Match, Template } from '@aws-cdk/assertions'; + ``` + +- Replace instances of `toHaveResource()` with `hasResourceProperties()` or `hasResource()`. + For example: + + ```ts + expect(stack).toHaveResource('FOO::BAR', {/*...*/}); + expect(stack).toHaveResource('FOO::BAR', {/*...*/}, ResourcePart.CompleteDefinition); + ``` + + ...becomes... + + ```ts + Template.fromStack(stack).hasResourceProperties('FOO::BAR', {/*...*/}); + Template.fromStack(stacK).hasResource('FOO::BAR', {/*...*/}); + ``` + +- Replace instances of `toCountResources()` with `resourceCountIs`. For example: + + ```ts + expect(stack).toCountResources('FOO::BAR', 1); + ``` + + ...becomes... + + ```ts + Template.fromStack(stack).resourceCountIs('FOO::BAR', 1); + ``` +- Replace instances of `toMatchTemplate()` with `templateMatches()`. For example: + + ```ts + expect(stack).toMatchTemplate({/*...*/}); + ``` + + ...becomes... + + ```ts + Template.fromStack(stack).templateMatches({/*...*/}); + ``` + +- Replace `arrayWith()` with `Match.arrayWith()`, `objectLike()` with `Match.objectLike()`, and + `ABSENT` with `Match.absent()`. + +- `not` can be replaced with `Match.not()` _or_ `resourceCountIs()` depending on the use case. + + ```ts + // asserting that the stack does not have a particular resource. + expect(stack).not.toHaveResource('FOO::BAR'); + ``` + + ...becomes... + + ```ts + Template.fromStack(stack).resourceCountIs('FOO::BAR', 0); + ``` + + ```ts + // asserting that the stack does not have a resource with these properties + expect(stack).not.toHaveResource('FOO::BAR', { + prop: 'does not exist', + }); + ``` + + ...becomes... + + ```ts + Template.fromStack(stack).hasResourceProperties('FOO::BAR', Match.not({ + prop: 'does not exist', + })); + ``` + +- `SynthUtils.synthesize(stack)` can be replaced as well. For example: + + ```ts + expect(SynthUtils.synthesize(stack).template).toEqual(/*...*/); + SynthUtils.syntesize(stack); + ``` + + ...becomes... + + ```ts + expect(Template.fromStack(stack).toJSON()).toEqual(/*...*/); + App.of(stack).synth(); + ``` + +## Migration Script + +> NOTE: We have some code rewrite rules that will make it easier to migrate from one library +> to the other. This tool will not do a complete rewrite and is not guaranteed to produce +> compilable code! It will just save you the effort of performing a lot of code substitutions +> you would otherwise have to do by hand. + +Comby is a tool used to do structured code rewriting. You can install it +[here](https://comby.dev/). Download the [rewrite.toml](rewrite.toml) file from our GitHub +repository, and run the following command in the root directory of your project: + +```bash +comby -config ~/rewrite.toml -f .ts -d test -in-place -timeout 10 +``` \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/NOTICE b/packages/@aws-cdk/assertions/NOTICE index 7904e6da91128..1ed908383e334 100644 --- a/packages/@aws-cdk/assertions/NOTICE +++ b/packages/@aws-cdk/assertions/NOTICE @@ -85,4 +85,4 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------- \ No newline at end of file +---------------- diff --git a/packages/@aws-cdk/assertions/README.md b/packages/@aws-cdk/assertions/README.md index 55b8d3520d550..d23c37a08b099 100644 --- a/packages/@aws-cdk/assertions/README.md +++ b/packages/@aws-cdk/assertions/README.md @@ -9,6 +9,9 @@ +If you're migrating from the old `assert` library, the migration guide can be found in +[our GitHub repository](https://github.com/aws/aws-cdk/blob/master/packages/@aws-cdk/assertions/MIGRATING.md). + Functions for writing test asserting against CDK applications, with focus on CloudFormation templates. The `Template` class includes a set of methods for writing assertions against CloudFormation templates. Use one of the `Template.fromXxx()` static methods to create an instance of this class. @@ -139,7 +142,7 @@ expect(result.Foo).toEqual({ Value: 'Fred', Description: 'FooFred' }); expect(result.Bar).toEqual({ Value: 'Fred', Description: 'BarFred' }); ``` -The APIs `hasMapping()` and `findMappings()` provide similar functionalities. +The APIs `hasMapping()`, `findMappings()`, `hasCondition()`, and `hasCondtions()` provide similar functionalities. ## Special Matchers @@ -251,7 +254,7 @@ This matcher can be combined with any of the other matchers. // The following will NOT throw an assertion error template.hasResourceProperties('Foo::Bar', { Fred: { - Wobble: [ Match.anyValue(), "Flip" ], + Wobble: [ Match.anyValue(), Match.anyValue() ], }, }); @@ -299,6 +302,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. +### String Matchers + +The `Match.stringLikeRegexp()` API can be used to assert that the target matches the +provided regular expression. + +```ts +// Given a template - +// { +// "Resources": { +// "MyBar": { +// "Type": "Foo::Bar", +// "Properties": { +// "Template": "const includeHeaders = true;" +// } +// } +// } +// } + +// The following will NOT throw an assertion error +template.hasResourceProperties('Foo::Bar', { + Template: Match.stringLikeRegexp('includeHeaders = (true|false)'), +}); + +// The following will throw an assertion error +template.hasResourceProperties('Foo::Bar', { + Template: Match.stringLikeRegexp('includeHeaders = null'), +}); +``` + ### Not Matcher The not matcher inverts the search pattern and matches all patterns in the path that does @@ -371,7 +403,7 @@ template.hasResourceProperties('Foo::Bar', { ## Capturing Values -This matcher APIs documented above allow capturing values in the matching entry +The matcher APIs documented above allow capturing values in the matching entry (Resource, Output, Mapping, etc.). The following code captures a string from a matching resource. @@ -463,3 +495,74 @@ fredCapture.asString(); // returns "Flob" fredCapture.next(); // returns true fredCapture.asString(); // returns "Quib" ``` + +## Asserting Annotations + +In addition to template matching, we provide an API for annotation matching. +[Annotations](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.Annotations.html) +can be added via the [Aspects](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.Aspects.html) +API. You can learn more about Aspects [here](https://docs.aws.amazon.com/cdk/v2/guide/aspects.html). + +Say you have a `MyAspect` and a `MyStack` that uses `MyAspect`: + +```ts nofixture +import * as cdk from '@aws-cdk/core'; +import { Construct, IConstruct } from 'constructs'; + +class MyAspect implements cdk.IAspect { + public visit(node: IConstruct): void { + if (node instanceof cdk.CfnResource && node.cfnResourceType === 'Foo::Bar') { + this.error(node, 'we do not want a Foo::Bar resource'); + } + } + + protected error(node: IConstruct, message: string): void { + cdk.Annotations.of(node).addError(message); + } +} + +class MyStack extends cdk.Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const stack = new cdk.Stack(); + new cdk.CfnResource(stack, 'Foo', { + type: 'Foo::Bar', + properties: { + Fred: 'Thud', + }, + }); + cdk.Aspects.of(stack).add(new MyAspect()); + } +} +``` + +We can then assert that the stack contains the expected Error: + +```ts +// import { Annotations } from '@aws-cdk/assertions'; + +Annotations.fromStack(stack).hasError( + '/Default/Foo', + 'we do not want a Foo::Bar resource', +); +``` + +Here are the available APIs for `Annotations`: + +- `hasError()` and `findError()` +- `hasWarning()` and `findWarning()` +- `hasInfo()` and `findInfo()` + +The corresponding `findXxx()` API is complementary to the `hasXxx()` API, except instead +of asserting its presence, it returns the set of matching messages. + +In addition, this suite of APIs is compatable with `Matchers` for more fine-grained control. +For example, the following assertion works as well: + +```ts +Annotations.fromStack(stack).hasError( + '/Default/Foo', + Match.stringLikeRegexp('.*Foo::Bar.*'), +); +``` diff --git a/packages/@aws-cdk/assertions/lib/annotations.ts b/packages/@aws-cdk/assertions/lib/annotations.ts new file mode 100644 index 0000000000000..09f4309044e4a --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/annotations.ts @@ -0,0 +1,129 @@ +import { Stack, Stage } from '@aws-cdk/core'; +import { SynthesisMessage } from '@aws-cdk/cx-api'; +import { Messages } from './private/message'; +import { findMessage, hasMessage } from './private/messages'; + +/** + * Suite of assertions that can be run on a CDK Stack. + * Focused on asserting annotations. + */ +export class Annotations { + /** + * Base your assertions on the messages returned by a synthesized CDK `Stack`. + * @param stack the CDK Stack to run assertions on + */ + public static fromStack(stack: Stack): Annotations { + return new Annotations(toMessages(stack)); + } + + private readonly _messages: Messages; + + private constructor(messages: SynthesisMessage[]) { + this._messages = convertArrayToMessagesType(messages); + } + + /** + * Assert that an error with the given message exists in the synthesized CDK `Stack`. + * + * @param constructPath the construct path to the error. Provide `'*'` to match all errors in the template. + * @param message the error message as should be expected. This should be a string or Matcher object. + */ + public hasError(constructPath: string, message: any): void { + const matchError = hasMessage(this._messages, constructPath, constructMessage('error', message)); + if (matchError) { + throw new Error(matchError); + } + } + + /** + * Get the set of matching errors of a given construct path and message. + * + * @param constructPath the construct path to the error. Provide `'*'` to match all errors in the template. + * @param message the error message as should be expected. This should be a string or Matcher object. + */ + public findError(constructPath: string, message: any): SynthesisMessage[] { + return convertMessagesTypeToArray(findMessage(this._messages, constructPath, constructMessage('error', message)) as Messages); + } + + /** + * Assert that an warning with the given message exists in the synthesized CDK `Stack`. + * + * @param constructPath the construct path to the warning. Provide `'*'` to match all warnings in the template. + * @param message the warning message as should be expected. This should be a string or Matcher object. + */ + public hasWarning(constructPath: string, message: any): void { + const matchError = hasMessage(this._messages, constructPath, constructMessage('warning', message)); + if (matchError) { + throw new Error(matchError); + } + } + + /** + * Get the set of matching warning of a given construct path and message. + * + * @param constructPath the construct path to the warning. Provide `'*'` to match all warnings in the template. + * @param message the warning message as should be expected. This should be a string or Matcher object. + */ + public findWarning(constructPath: string, message: any): SynthesisMessage[] { + return convertMessagesTypeToArray(findMessage(this._messages, constructPath, constructMessage('warning', message)) as Messages); + } + + /** + * Assert that an info with the given message exists in the synthesized CDK `Stack`. + * + * @param constructPath the construct path to the info. Provide `'*'` to match all info in the template. + * @param message the info message as should be expected. This should be a string or Matcher object. + */ + public hasInfo(constructPath: string, message: any): void { + const matchError = hasMessage(this._messages, constructPath, constructMessage('info', message)); + if (matchError) { + throw new Error(matchError); + } + } + + /** + * Get the set of matching infos of a given construct path and message. + * + * @param constructPath the construct path to the info. Provide `'*'` to match all infos in the template. + * @param message the info message as should be expected. This should be a string or Matcher object. + */ + public findInfo(constructPath: string, message: any): SynthesisMessage[] { + return convertMessagesTypeToArray(findMessage(this._messages, constructPath, constructMessage('info', message)) as Messages); + } +} + +function constructMessage(type: 'info' | 'warning' | 'error', message: any): {[key:string]: any } { + return { + level: type, + entry: { + data: message, + }, + }; +} + +function convertArrayToMessagesType(messages: SynthesisMessage[]): Messages { + return messages.reduce((obj, item, index) => { + return { + ...obj, + [index]: item, + }; + }, {}) as Messages; +} + +function convertMessagesTypeToArray(messages: Messages): SynthesisMessage[] { + return Object.values(messages) as SynthesisMessage[]; +} + +function toMessages(stack: Stack): any { + const root = stack.node.root; + if (!Stage.isStage(root)) { + throw new Error('unexpected: all stacks must be part of a Stage or an App'); + } + + // to support incremental assertions (i.e. "expect(stack).toNotContainSomething(); doSomething(); expect(stack).toContainSomthing()") + const force = true; + + const assembly = root.synth({ force }); + + return assembly.getStackArtifact(stack.artifactId).messages; +} diff --git a/packages/@aws-cdk/assertions/lib/index.ts b/packages/@aws-cdk/assertions/lib/index.ts index 492fad1227af3..eccbfac38637f 100644 --- a/packages/@aws-cdk/assertions/lib/index.ts +++ b/packages/@aws-cdk/assertions/lib/index.ts @@ -1,4 +1,5 @@ export * from './capture'; export * from './template'; export * from './match'; -export * from './matcher'; \ No newline at end of file +export * from './matcher'; +export * from './annotations'; \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/match.ts b/packages/@aws-cdk/assertions/lib/match.ts index e3afa681e6541..dd03bba1950a2 100644 --- a/packages/@aws-cdk/assertions/lib/match.ts +++ b/packages/@aws-cdk/assertions/lib/match.ts @@ -79,6 +79,13 @@ export abstract class Match { public static anyValue(): Matcher { return new AnyMatch('anyValue'); } + + /** + * Matches targets according to a regular expression + */ + public static stringLikeRegexp(pattern: string): Matcher { + return new StringLikeRegexpMatch('stringLikeRegexp', pattern); + } } /** @@ -115,7 +122,7 @@ class LiteralMatch extends Matcher { public test(actual: any): MatchResult { if (Array.isArray(this.pattern)) { - return new ArrayMatch(this.name, this.pattern, { subsequence: false }).test(actual); + return new ArrayMatch(this.name, this.pattern, { subsequence: false, partialObjects: this.partialObjects }).test(actual); } if (typeof this.pattern === 'object') { @@ -155,6 +162,13 @@ interface ArrayMatchOptions { * @default true */ readonly subsequence?: boolean; + + /** + * Whether to continue matching objects inside the array partially + * + * @default false + */ + readonly partialObjects?: boolean; } /** @@ -162,6 +176,7 @@ interface ArrayMatchOptions { */ class ArrayMatch extends Matcher { private readonly subsequence: boolean; + private readonly partialObjects: boolean; constructor( public readonly name: string, @@ -170,6 +185,7 @@ class ArrayMatch extends Matcher { super(); this.subsequence = options.subsequence ?? true; + this.partialObjects = options.partialObjects ?? false; } public test(actual: any): MatchResult { @@ -195,7 +211,10 @@ class ArrayMatch extends Matcher { while (patternIdx < this.pattern.length && actualIdx < actual.length) { const patternElement = this.pattern[patternIdx]; - const matcher = Matcher.isMatcher(patternElement) ? patternElement : new LiteralMatch(this.name, patternElement); + const matcher = Matcher.isMatcher(patternElement) + ? patternElement + : new LiteralMatch(this.name, patternElement, { partialObjects: this.partialObjects }); + const matcherName = matcher.name; if (this.subsequence && (matcherName == 'absent' || matcherName == 'anyValue')) { // array subsequence matcher is not compatible with anyValue() or absent() matcher. They don't make sense to be used together. @@ -378,3 +397,37 @@ class AnyMatch extends Matcher { return result; } } + +class StringLikeRegexpMatch extends Matcher { + constructor( + public readonly name: string, + private readonly pattern: string) { + + super(); + } + + test(actual: any): MatchResult { + const result = new MatchResult(actual); + + const regex = new RegExp(this.pattern, 'gm'); + + if (typeof actual !== 'string') { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected a string, but got '${typeof actual}'`, + }); + } + + if (!regex.test(actual)) { + result.recordFailure({ + matcher: this, + path: [], + message: `String '${actual}' did not match pattern '${this.pattern}'`, + }); + } + + return result; + } + +} diff --git a/packages/@aws-cdk/assertions/lib/private/conditions.ts b/packages/@aws-cdk/assertions/lib/private/conditions.ts new file mode 100644 index 0000000000000..6ed10379dea9e --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/conditions.ts @@ -0,0 +1,30 @@ +import { filterLogicalId, formatFailure, matchSection } from './section'; +import { Template } from './template'; + +export function findConditions(template: Template, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { + const section: { [key: string] : {} } = template.Conditions ?? {}; + const result = matchSection(filterLogicalId(section, logicalId), props); + + if (!result.match) { + return {}; + } + + return result.matches; +} + +export function hasCondition(template: Template, logicalId: string, props: any): string | void { + const section: { [key: string] : {} } = template.Conditions ?? {}; + const result = matchSection(filterLogicalId(section, logicalId), props); + if (result.match) { + return; + } + + if (result.closestResult === undefined) { + return 'No conditions found in the template'; + } + + return [ + `Template has ${result.analyzedCount} conditions, but none match as expected.`, + formatFailure(result.closestResult), + ].join('\n'); +} diff --git a/packages/@aws-cdk/assertions/lib/private/cyclic.ts b/packages/@aws-cdk/assertions/lib/private/cyclic.ts new file mode 100644 index 0000000000000..85aa0cbf07147 --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/cyclic.ts @@ -0,0 +1,180 @@ +import { Resource, Template } from './template'; + +/** + * Check a template for cyclic dependencies + * + * This will make sure that we don't happily validate templates + * in unit tests that wouldn't deploy to CloudFormation anyway. + */ +export function checkTemplateForCyclicDependencies(template: Template): void { + const logicalIds = new Set(Object.keys(template.Resources ?? {})); + + const dependencies = new Map>(); + for (const [logicalId, resource] of Object.entries(template.Resources ?? {})) { + dependencies.set(logicalId, intersect(findResourceDependencies(resource), logicalIds)); + } + + // We will now progressively remove entries from the map of 'dependencies' that have + // 0 elements in them. If we can't do that anymore and the map isn't empty, we + // have a cyclic dependency. + while (dependencies.size > 0) { + const free = Array.from(dependencies.entries()).filter(([_, deps]) => deps.size === 0); + if (free.length === 0) { + // Oops! + const cycle = findCycle(dependencies); + + const cycleResources: any = {}; + for (const logicalId of cycle) { + cycleResources[logicalId] = template.Resources?.[logicalId]; + } + + throw new Error(`Template is undeployable, these resources have a dependency cycle: ${cycle.join(' -> ')}:\n\n${JSON.stringify(cycleResources, undefined, 2)}`); + } + + for (const [logicalId, _] of free) { + for (const deps of dependencies.values()) { + deps.delete(logicalId); + } + dependencies.delete(logicalId); + } + } +} + +function findResourceDependencies(res: Resource): Set { + return new Set([ + ...toArray(res.DependsOn ?? []), + ...findExpressionDependencies(res.Properties), + ]); +} + +function toArray(x: A | A[]): A[] { + return Array.isArray(x) ? x : [x]; +} + +function findExpressionDependencies(obj: any): Set { + const ret = new Set(); + recurse(obj); + return ret; + + function recurse(x: any): void { + if (!x) { return; } + if (Array.isArray(x)) { + x.forEach(recurse); + } + if (typeof x === 'object') { + const keys = Object.keys(x); + if (keys.length === 1 && keys[0] === 'Ref') { + ret.add(x[keys[0]]); + } else if (keys.length === 1 && keys[0] === 'Fn::GetAtt') { + ret.add(x[keys[0]][0]); + } else if (keys.length === 1 && keys[0] === 'Fn::Sub') { + const argument = x[keys[0]]; + const pattern = Array.isArray(argument) ? argument[0] : argument; + + // pattern should always be a string, but we've encountered some cases in which + // it isn't. Better safeguard. + if (typeof pattern === 'string') { + for (const logId of logicalIdsInSubString(pattern)) { + ret.add(logId); + } + } + const contextDict = Array.isArray(argument) ? argument[1] : undefined; + if (contextDict && typeof contextDict === 'object') { + Object.values(contextDict).forEach(recurse); + } + } else { + Object.values(x).forEach(recurse); + } + } + } +} + +/** + * Return the logical IDs found in a {Fn::Sub} format string + */ +function logicalIdsInSubString(x: string): string[] { + return analyzeSubPattern(x).flatMap((fragment) => { + switch (fragment.type) { + case 'getatt': + case 'ref': + return [fragment.logicalId]; + case 'literal': + return []; + } + }); +} + + +function analyzeSubPattern(pattern: string): SubFragment[] { + const ret: SubFragment[] = []; + let start = 0; + + let ph0 = pattern.indexOf('${', start); + while (ph0 > -1) { + if (pattern[ph0 + 2] === '!') { + // "${!" means "don't actually substitute" + start = ph0 + 3; + ph0 = pattern.indexOf('${', start); + continue; + } + + const ph1 = pattern.indexOf('}', ph0 + 2); + if (ph1 === -1) { + break; + } + const placeholder = pattern.substring(ph0 + 2, ph1); + + if (ph0 > start) { + ret.push({ type: 'literal', content: pattern.substring(start, ph0) }); + } + if (placeholder.includes('.')) { + const [logicalId, attr] = placeholder.split('.'); + ret.push({ type: 'getatt', logicalId: logicalId!, attr: attr! }); + } else { + ret.push({ type: 'ref', logicalId: placeholder }); + } + + start = ph1 + 1; + ph0 = pattern.indexOf('${', start); + } + + if (start < pattern.length - 1) { + ret.push({ type: 'literal', content: pattern.substr(start) }); + } + + return ret; +} + +type SubFragment = + | { readonly type: 'literal'; readonly content: string } + | { readonly type: 'ref'; readonly logicalId: string } + | { readonly type: 'getatt'; readonly logicalId: string; readonly attr: string }; + + +function intersect(xs: Set, ys: Set): Set { + return new Set(Array.from(xs).filter(x => ys.has(x))); +} + +/** + * Find cycles in a graph + * + * Not the fastest, but effective and should be rare + */ +function findCycle(deps: ReadonlyMap>): string[] { + for (const node of deps.keys()) { + const cycle = recurse(node, [node]); + if (cycle) { return cycle; } + } + throw new Error('No cycle found. Assertion failure!'); + + function recurse(node: string, path: string[]): string[] | undefined { + for (const dep of deps.get(node) ?? []) { + if (dep === path[0]) { return [...path, dep]; } + + const cycle = recurse(dep, [...path, dep]); + if (cycle) { return cycle; } + } + + return undefined; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/private/mappings.ts b/packages/@aws-cdk/assertions/lib/private/mappings.ts index e080843dd87f8..e8788fb2ef112 100644 --- a/packages/@aws-cdk/assertions/lib/private/mappings.ts +++ b/packages/@aws-cdk/assertions/lib/private/mappings.ts @@ -2,7 +2,7 @@ import { filterLogicalId, formatFailure, matchSection } from './section'; import { Template } from './template'; export function findMappings(template: Template, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { - const section: { [key: string] : {} } = template.Mappings; + const section: { [key: string] : {} } = template.Mappings ?? {}; const result = matchSection(filterLogicalId(section, logicalId), props); if (!result.match) { @@ -13,7 +13,7 @@ export function findMappings(template: Template, logicalId: string, props: any = } export function hasMapping(template: Template, logicalId: string, props: any): string | void { - const section: { [key: string]: {} } = template.Mappings; + const section: { [key: string]: {} } = template.Mappings ?? {}; const result = matchSection(filterLogicalId(section, logicalId), props); if (result.match) { diff --git a/packages/@aws-cdk/assertions/lib/private/message.ts b/packages/@aws-cdk/assertions/lib/private/message.ts new file mode 100644 index 0000000000000..1a14fe6be1b00 --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/message.ts @@ -0,0 +1,5 @@ +import { SynthesisMessage } from '@aws-cdk/cx-api'; + +export type Messages = { + [key: string]: SynthesisMessage; +} diff --git a/packages/@aws-cdk/assertions/lib/private/messages.ts b/packages/@aws-cdk/assertions/lib/private/messages.ts new file mode 100644 index 0000000000000..95e898b687183 --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/messages.ts @@ -0,0 +1,50 @@ +import { SynthesisMessage } from '@aws-cdk/cx-api'; +import { Messages } from './message'; +import { formatFailure, matchSection } from './section'; + +export function findMessage(messages: Messages, constructPath: string, props: any = {}): { [key: string]: { [key: string]: any } } { + const section: { [key: string]: SynthesisMessage } = messages; + const result = matchSection(filterPath(section, constructPath), props); + + if (!result.match) { + return {}; + } + + return result.matches; +} + +export function hasMessage(messages: Messages, constructPath: string, props: any): string | void { + const section: { [key: string]: SynthesisMessage } = messages; + const result = matchSection(filterPath(section, constructPath), props); + + if (result.match) { + return; + } + + if (result.closestResult === undefined) { + return 'No messages found in the stack'; + } + + handleTrace(result.closestResult.target); + return [ + `Stack has ${result.analyzedCount} messages, but none match as expected.`, + formatFailure(result.closestResult), + ].join('\n'); +} + +// We redact the stack trace by default because it is unnecessarily long and unintelligible. +// If there is a use case for rendering the trace, we can add it later. +function handleTrace(match: any, redact: boolean = true): void { + if (redact && match.entry?.trace !== undefined) { + match.entry.trace = 'redacted'; + }; +} + +function filterPath(section: { [key: string]: SynthesisMessage }, path: string): { [key: string]: SynthesisMessage } { + // default signal for all paths is '*' + if (path === '*') return section; + + return Object.entries(section ?? {}) + .filter(([_, v]) => v.id === path) + .reduce((agg, [k, v]) => { return { ...agg, [k]: v }; }, {}); +} diff --git a/packages/@aws-cdk/assertions/lib/private/outputs.ts b/packages/@aws-cdk/assertions/lib/private/outputs.ts index f00f05bc9bb0f..39509698d0e43 100644 --- a/packages/@aws-cdk/assertions/lib/private/outputs.ts +++ b/packages/@aws-cdk/assertions/lib/private/outputs.ts @@ -2,7 +2,7 @@ import { filterLogicalId, formatFailure, matchSection } from './section'; import { Template } from './template'; export function findOutputs(template: Template, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { - const section = template.Outputs; + const section = template.Outputs ?? {}; const result = matchSection(filterLogicalId(section, logicalId), props); if (!result.match) { @@ -13,7 +13,7 @@ export function findOutputs(template: Template, logicalId: string, props: any = } export function hasOutput(template: Template, logicalId: string, props: any): string | void { - const section: { [key: string]: {} } = template.Outputs; + const section: { [key: string]: {} } = template.Outputs ?? {}; const result = matchSection(filterLogicalId(section, logicalId), props); if (result.match) { return; diff --git a/packages/@aws-cdk/assertions/lib/private/parameters.ts b/packages/@aws-cdk/assertions/lib/private/parameters.ts new file mode 100644 index 0000000000000..0e73160ea5a75 --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/parameters.ts @@ -0,0 +1,30 @@ +import { filterLogicalId, formatFailure, matchSection } from './section'; +import { Template } from './template'; + +export function findParameters(template: Template, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { + const section: { [key: string] : {} } = template.Parameters ?? {}; + const result = matchSection(filterLogicalId(section, logicalId), props); + + if (!result.match) { + return {}; + } + + return result.matches; +} + +export function hasParameter(template: Template, logicalId: string, props: any): string | void { + const section: { [key: string] : {} } = template.Parameters ?? {}; + const result = matchSection(filterLogicalId(section, logicalId), props); + if (result.match) { + return; + } + + if (result.closestResult === undefined) { + return 'No parameters found in the template'; + } + + return [ + `Template has ${result.analyzedCount} parameters, but none match as expected.`, + formatFailure(result.closestResult), + ].join('\n'); +} diff --git a/packages/@aws-cdk/assertions/lib/private/resources.ts b/packages/@aws-cdk/assertions/lib/private/resources.ts index 68e8e6c2ddff8..00a57c05f9b26 100644 --- a/packages/@aws-cdk/assertions/lib/private/resources.ts +++ b/packages/@aws-cdk/assertions/lib/private/resources.ts @@ -4,7 +4,7 @@ import { formatFailure, matchSection } from './section'; import { Resource, Template } from './template'; export function findResources(template: Template, type: string, props: any = {}): { [key: string]: { [key: string]: any } } { - const section = template.Resources; + const section = template.Resources ?? {}; const result = matchSection(filterType(section, type), props); if (!result.match) { @@ -15,7 +15,7 @@ export function findResources(template: Template, type: string, props: any = {}) } export function hasResource(template: Template, type: string, props: any): string | void { - const section = template.Resources; + const section = template.Resources ?? {}; const result = matchSection(filterType(section, type), props); if (result.match) { return; @@ -46,14 +46,14 @@ export function hasResourceProperties(template: Template, type: string, props: a } export function countResources(template: Template, type: string): number { - const section = template.Resources; + const section = template.Resources ?? {}; const types = filterType(section, type); return Object.entries(types).length; } function addEmptyProperties(template: Template): Template { - let section = template.Resources; + let section = template.Resources ?? {}; Object.keys(section).map((key) => { if (!section[key].hasOwnProperty('Properties')) { diff --git a/packages/@aws-cdk/assertions/lib/private/template.ts b/packages/@aws-cdk/assertions/lib/private/template.ts index 3b44368138435..4aea34a2b5132 100644 --- a/packages/@aws-cdk/assertions/lib/private/template.ts +++ b/packages/@aws-cdk/assertions/lib/private/template.ts @@ -1,16 +1,29 @@ // Partial types for CloudFormation Template export type Template = { - Resources: { [logicalId: string]: Resource }, - Outputs: { [logicalId: string]: Output }, - Mappings: { [logicalId: string]: Mapping } + // In actuality this is not optional, but we sometimes don't generate it so we + // need to account for that. + Resources?: { [logicalId: string]: Resource }, + Outputs?: { [logicalId: string]: Output }, + Mappings?: { [logicalId: string]: Mapping }, + Parameters?: { [logicalId: string]: Parameter }, + Conditions?: { [logicalId: string]: Condition }, } export type Resource = { Type: string; + DependsOn?: string | string[]; + Properties?: { [key: string]: any }; [key: string]: any; } export type Output = { [key: string]: any }; -export type Mapping = { [key: string]: any }; \ No newline at end of file +export type Mapping = { [key: string]: any }; + +export type Parameter = { + Type: string; + [key: string]: any; +} + +export type Condition = { [key: string]: any }; \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/template.ts b/packages/@aws-cdk/assertions/lib/template.ts index dfc830cf8d822..6399d3a971897 100644 --- a/packages/@aws-cdk/assertions/lib/template.ts +++ b/packages/@aws-cdk/assertions/lib/template.ts @@ -3,8 +3,11 @@ import { Stack, Stage } from '@aws-cdk/core'; import * as fs from 'fs-extra'; import { Match } from './match'; import { Matcher } from './matcher'; +import { findConditions, hasCondition } from './private/conditions'; +import { checkTemplateForCyclicDependencies } from './private/cyclic'; import { findMappings, hasMapping } from './private/mappings'; import { findOutputs, hasOutput } from './private/outputs'; +import { findParameters, hasParameter } from './private/parameters'; import { countResources, findResources, hasResource, hasResourceProperties } from './private/resources'; import { Template as TemplateType } from './private/template'; @@ -45,6 +48,7 @@ export class Template { private constructor(template: { [key: string]: any }) { this.template = template as TemplateType; + checkTemplateForCyclicDependencies(this.template); } /** @@ -108,6 +112,31 @@ export class Template { return findResources(this.template, type, props); } + /** + * Assert that a Parameter with the given properties exists in the CloudFormation template. + * By default, performs partial matching on the parameter, via the `Match.objectLike()`. + * To configure different behavior, use other matchers in the `Match` class. + * @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template. + * @param props the parameter as should be expected in the template. + */ + public hasParameter(logicalId: string, props: any): void { + const matchError = hasParameter(this.template, logicalId, props); + if (matchError) { + throw new Error(matchError); + } + } + + /** + * Get the set of matching Parameters that match the given properties in the CloudFormation template. + * @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template. + * @param props by default, matches all Parameters in the template. + * When a literal object is provided, performs a partial match via `Match.objectLike()`. + * Use the `Match` APIs to configure a different behaviour. + */ + public findParameters(logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { + return findParameters(this.template, logicalId, props); + } + /** * Assert that an Output with the given properties exists in the CloudFormation template. * By default, performs partial matching on the resource, via the `Match.objectLike()`. @@ -158,6 +187,31 @@ export class Template { return findMappings(this.template, logicalId, props); } + /** + * Assert that a Condition with the given properties exists in the CloudFormation template. + * By default, performs partial matching on the resource, via the `Match.objectLike()`. + * To configure different behavour, use other matchers in the `Match` class. + * @param logicalId the name of the mapping. Provide `'*'` to match all conditions in the template. + * @param props the output as should be expected in the template. + */ + public hasCondition(logicalId: string, props: any): void { + const matchError = hasCondition(this.template, logicalId, props); + if (matchError) { + throw new Error(matchError); + } + } + + /** + * Get the set of matching Conditions that match the given properties in the CloudFormation template. + * @param logicalId the name of the condition. Provide `'*'` to match all conditions in the template. + * @param props by default, matches all Conditions in the template. + * When a literal object is provided, performs a partial match via `Match.objectLike()`. + * Use the `Match` APIs to configure a different behaviour. + */ + public findConditions(logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { + return findConditions(this.template, logicalId, props); + } + /** * Assert that the CloudFormation template matches the given value * @param expected the expected CloudFormation template as key-value pairs. @@ -180,6 +234,7 @@ function toTemplate(stack: Stack): any { if (!Stage.isStage(root)) { throw new Error('unexpected: all stacks must be part of a Stage or an App'); } + const assembly = root.synth(); if (stack.nestedStackParent) { // if this is a nested stack (it has a parent), then just read the template as a string diff --git a/packages/@aws-cdk/assertions/package.json b/packages/@aws-cdk/assertions/package.json index 5e0b386147418..3c2533d04ae9d 100644 --- a/packages/@aws-cdk/assertions/package.json +++ b/packages/@aws-cdk/assertions/package.json @@ -65,10 +65,10 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", "@types/fs-extra": "^9.0.13", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "constructs": "^3.3.69", - "jest": "^27.4.5", - "ts-jest": "^27.1.2" + "jest": "^27.5.1", + "ts-jest": "^27.1.3" }, "dependencies": { "@aws-cdk/cloud-assembly-schema": "0.0.0", diff --git a/packages/@aws-cdk/assertions/rewrite.toml b/packages/@aws-cdk/assertions/rewrite.toml new file mode 100644 index 0000000000000..66459d1a6991f --- /dev/null +++ b/packages/@aws-cdk/assertions/rewrite.toml @@ -0,0 +1,115 @@ +# comby -config ~/rewrite.toml -f .ts -d test -in-place -timeout 10 + +[000_import] +match="import '@aws-cdk/assert-internal/jest'" +rewrite="import { Template } from '@aws-cdk/assertions'" + +[000_import2] +match="import :[_] from '@aws-cdk/assert-internal'" +rewrite="import { Template } from '@aws-cdk/assertions'" + +[100_jest_toHaveResourceLike_CompleteDefinition] +match="expect(:[stack]).toHaveResourceLike(:[args], ResourcePart.CompleteDefinition)" +rewrite="Template.fromStack(:[stack]).hasResource(:[args])" + +[100_assert_toHaveResourceLike_CompleteDefinition] +match=":[[expect]](:[stack]).to(haveResourceLike(:[args], ResourcePart.CompleteDefinition))" +rewrite="Template.fromStack(:[stack]).hasResource(:[args])" +rule='''where match :[expect] { + | "expect" -> true + | "cdkExpect" -> true + | ":[_]" -> false +}''' + +[100_jest_toHaveResource_CompleteDefinition] +match="expect(:[stack]).toHaveResource(:[args], ResourcePart.CompleteDefinition)" +rewrite="Template.fromStack(:[stack]).hasResource(:[args])" + +[100_assert_toHaveResource_CompleteDefinition] +match=":[[expect]](:[stack]).to(haveResource(:[args], ResourcePart.CompleteDefinition))" +rewrite="Template.fromStack(:[stack]).hasResource(:[args])" +rule='''where match :[expect] { + | "expect" -> true + | "cdkExpect" -> true + | ":[_]" -> false +}''' + +[200_jest_toHaveResourceLike] +match="expect(:[stack]).toHaveResourceLike(:[args])" +rewrite="Template.fromStack(:[stack]).hasResourceProperties(:[args])" + +[200_assert_toHaveResourceLike] +match=":[[expect]](:[stack]).to(haveResourceLike(:[args]))" +rewrite="Template.fromStack(:[stack]).hasResourceProperties(:[args])" +rule='''where match :[expect] { + | "expect" -> true + | "cdkExpect" -> true + | ":[_]" -> false +}''' + +[200_jest_toHaveResource] +match="expect(:[stack]).toHaveResource(:[args])" +rewrite="Template.fromStack(:[stack]).hasResourceProperties(:[args])" + +[200_assert_toHaveResource] +match=":[[expect]](:[stack]).to(haveResource(:[args]))" +rewrite="Template.fromStack(:[stack]).hasResourceProperties(:[args])" +rule='''where match :[expect] { + | "expect" -> true + | "cdkExpect" -> true + | ":[_]" -> false +}''' + +[200_jest_toCountResources] +match="expect(:[stack]).toCountResources" +rewrite="Template.fromStack(:[stack]).resourceCountIs" + +[200_assert_toCountResources2] +match=":[[expect]](:[stack]).to(countResources(:[args]))" +rewrite="Template.fromStack(:[stack]).resourceCountIs(:[args])" +rule='''where match :[expect] { + | "expect" -> true + | "cdkExpect" -> true + | ":[_]" -> false +}''' + +[200_jest_toMatchTemplate] +match="expect(:[stack]).toMatchTemplate" +rewrite="Template.fromStack(:[stack]).templateMatches" + +[200_assert_toMatchTemplate] +match=":[[expect]](:[stack]).toMatchTemplate" +rewrite="Template.fromStack(:[stack]).templateMatches" +rule='''where match :[expect] { + | "expect" -> true + | "cdkExpect" -> true + | ":[_]" -> false +}''' + +[300_notToHaveResourceLike] +match="expect(:[stack]).not.toHaveResourceLike(:[args])" +rewrite="Template.fromStack(:[stack]).resourceCountIs(:[args], 0)" + +[300_notToHaveResource] +match="expect(:[stack]).not.toHaveResource(:[args])" +rewrite="Template.fromStack(:[stack]).resourceCountIs(:[args], 0)" + +[arrayWith] +match="arrayWith(:[args])" +rewrite="Match.arrayWith([:[args]])" + +[objectLike] +match="objectLike" +rewrite="Match.objectLike" + +[absent] +match="ABSENT" +rewrite="Match.absent()" + +[400_synthutils_template] +match="SynthUtils.synthesize(:[stack]).template" +rewrite="Template.fromStack(:[stack]).toJSON()" + +[401_synthutils_assembly] +match="SynthUtils.synthesize(:[stack])" +rewrite="App.of(:[stack]).synth()" \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/rosetta/default.ts-fixture b/packages/@aws-cdk/assertions/rosetta/default.ts-fixture index fa862da5c609a..32d751f8c38fe 100644 --- a/packages/@aws-cdk/assertions/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/assertions/rosetta/default.ts-fixture @@ -1,7 +1,7 @@ // Fixture with packages imported, but nothing else import { Construct } from 'constructs'; -import { Stack } from '@aws-cdk/core'; -import { Capture, Match, Template } from '@aws-cdk/assertions'; +import { Aspects, CfnResource, Stack } from '@aws-cdk/core'; +import { Annotations, Capture, Match, Template } from '@aws-cdk/assertions'; class Fixture extends Stack { constructor(scope: Construct, id: string) { diff --git a/packages/@aws-cdk/assertions/test/annotations.test.ts b/packages/@aws-cdk/assertions/test/annotations.test.ts new file mode 100644 index 0000000000000..2b1c0c0274ead --- /dev/null +++ b/packages/@aws-cdk/assertions/test/annotations.test.ts @@ -0,0 +1,207 @@ +import { Annotations, Aspects, CfnResource, IAspect, Stack } from '@aws-cdk/core'; +import { IConstruct } from 'constructs'; +import { Annotations as _Annotations, Match } from '../lib'; + +describe('Messages', () => { + let stack: Stack; + let annotations: _Annotations; + beforeAll(() => { + stack = new Stack(); + new CfnResource(stack, 'Foo', { + type: 'Foo::Bar', + properties: { + Fred: 'Thud', + }, + }); + + new CfnResource(stack, 'Bar', { + type: 'Foo::Bar', + properties: { + Baz: 'Qux', + }, + }); + + new CfnResource(stack, 'Fred', { + type: 'Baz::Qux', + properties: { + Foo: 'Bar', + }, + }); + + new CfnResource(stack, 'Qux', { + type: 'Fred::Thud', + properties: { + Fred: 'Bar', + }, + }); + + Aspects.of(stack).add(new MyAspect()); + annotations = _Annotations.fromStack(stack); + }); + + describe('hasError', () => { + test('match', () => { + annotations.hasError('/Default/Foo', 'this is an error'); + }); + + test('no match', () => { + expect(() => annotations.hasError('/Default/Fred', Match.anyValue())) + .toThrowError(/Stack has 1 messages, but none match as expected./); + }); + }); + + describe('findError', () => { + test('match', () => { + const result = annotations.findError('*', Match.anyValue()); + expect(result.length).toEqual(2); + }); + + test('no match', () => { + const result = annotations.findError('*', 'no message looks like this'); + expect(result.length).toEqual(0); + }); + }); + + describe('hasWarning', () => { + test('match', () => { + annotations.hasWarning('/Default/Fred', 'this is a warning'); + }); + + test('no match', () => { + expect(() => annotations.hasWarning('/Default/Foo', Match.anyValue())).toThrowError(/Stack has 1 messages, but none match as expected./); + }); + }); + + describe('findWarning', () => { + test('match', () => { + const result = annotations.findWarning('*', Match.anyValue()); + expect(result.length).toEqual(1); + }); + + test('no match', () => { + const result = annotations.findWarning('*', 'no message looks like this'); + expect(result.length).toEqual(0); + }); + }); + + describe('hasInfo', () => { + test('match', () => { + annotations.hasInfo('/Default/Qux', 'this is an info'); + }); + + test('no match', () => { + expect(() => annotations.hasInfo('/Default/Qux', 'this info is incorrect')).toThrowError(/Stack has 1 messages, but none match as expected./); + }); + }); + + describe('findInfo', () => { + test('match', () => { + const result = annotations.findInfo('/Default/Qux', 'this is an info'); + expect(result.length).toEqual(1); + }); + + test('no match', () => { + const result = annotations.findInfo('*', 'no message looks like this'); + expect(result.length).toEqual(0); + }); + }); + + describe('with matchers', () => { + test('anyValue', () => { + const result = annotations.findError('*', Match.anyValue()); + expect(result.length).toEqual(2); + }); + + test('not', () => { + expect(() => annotations.hasError('/Default/Foo', Match.not('this is an error'))) + .toThrowError(/Found unexpected match: "this is an error" at \/entry\/data/); + }); + + test('stringLikeRegEx', () => { + annotations.hasError('/Default/Foo', Match.stringLikeRegexp('.*error')); + }); + }); +}); + +describe('Multiple Messages on the Resource', () => { + let stack: Stack; + let annotations: _Annotations; + beforeAll(() => { + stack = new Stack(); + new CfnResource(stack, 'Foo', { + type: 'Foo::Bar', + properties: { + Fred: 'Thud', + }, + }); + + const bar = new CfnResource(stack, 'Bar', { + type: 'Foo::Bar', + properties: { + Baz: 'Qux', + }, + }); + bar.node.setContext('disable-stack-trace', false); + + Aspects.of(stack).add(new MultipleAspectsPerNode()); + annotations = _Annotations.fromStack(stack); + }); + + test('succeeds on hasXxx APIs', () => { + annotations.hasError('/Default/Foo', 'error: this is an error'); + annotations.hasError('/Default/Foo', 'error: unsupported type Foo::Bar'); + annotations.hasWarning('/Default/Foo', 'warning: Foo::Bar is deprecated'); + }); + + test('succeeds on findXxx APIs', () => { + const result1 = annotations.findError('*', Match.stringLikeRegexp('error:.*')); + expect(result1.length).toEqual(4); + const result2 = annotations.findError('/Default/Bar', Match.stringLikeRegexp('error:.*')); + expect(result2.length).toEqual(2); + const result3 = annotations.findWarning('/Default/Bar', 'warning: Foo::Bar is deprecated'); + expect(result3[0].entry.data).toEqual('warning: Foo::Bar is deprecated'); + }); +}); +class MyAspect implements IAspect { + public visit(node: IConstruct): void { + if (node instanceof CfnResource) { + if (node.cfnResourceType === 'Foo::Bar') { + this.error(node, 'this is an error'); + } else if (node.cfnResourceType === 'Baz::Qux') { + this.warn(node, 'this is a warning'); + } else { + this.info(node, 'this is an info'); + } + } + }; + + protected warn(node: IConstruct, message: string): void { + Annotations.of(node).addWarning(message); + } + + protected error(node: IConstruct, message: string): void { + Annotations.of(node).addError(message); + } + + protected info(node: IConstruct, message: string): void { + Annotations.of(node).addInfo(message); + } +} + +class MultipleAspectsPerNode implements IAspect { + public visit(node: IConstruct): void { + if (node instanceof CfnResource) { + this.error(node, 'error: this is an error'); + this.error(node, `error: unsupported type ${node.cfnResourceType}`); + this.warn(node, `warning: ${node.cfnResourceType} is deprecated`); + } + } + + protected warn(node: IConstruct, message: string): void { + Annotations.of(node).addWarning(message); + } + + protected error(node: IConstruct, message: string): void { + Annotations.of(node).addError(message); + } +} \ 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 0b1ce784f9023..92c3646e044a7 100644 --- a/packages/@aws-cdk/assertions/test/match.test.ts +++ b/packages/@aws-cdk/assertions/test/match.test.ts @@ -176,7 +176,7 @@ describe('Matchers', () => { expectPass(matcher, { foo: 'bar', baz: { fred: 'waldo', wobble: 'flob' } }); }); - test('nested with ArrayMatch', () => { + test('ArrayMatch nested inside ObjectMatch', () => { matcher = Match.objectLike({ foo: Match.arrayWith(['bar']), }); @@ -184,6 +184,23 @@ describe('Matchers', () => { expectFailure(matcher, { foo: ['baz'], fred: 'waldo' }, [/Missing element \[bar\] at pattern index 0 at \/foo/]); }); + test('Partiality is maintained throughout arrays', () => { + // Before this fix: + // + // - objectLike({ x: { LITERAL }) ==> LITERAL would be matched partially as well + // - objectLike({ xs: [ { LITERAL } ] }) ==> but here LITERAL would be matched fully + // + // That passing through an array resets the partial matching to full is a + // surprising inconsistency. + // + matcher = Match.objectLike({ + foo: [{ bar: 'bar' }], + }); + expectPass(matcher, { foo: [{ bar: 'bar' }] }); // Trivially true + expectPass(matcher, { boo: 'boo', foo: [{ bar: 'bar' }] }); // Additional members at top level okay + expectPass(matcher, { foo: [{ bar: 'bar', boo: 'boo' }] }); // Additional members at inner level okay + }); + test('absent', () => { matcher = Match.objectLike({ foo: Match.absent() }); expectPass(matcher, { bar: 'baz' }); @@ -384,12 +401,28 @@ describe('Matchers', () => { expectPass(matcher, {}); }); }); + + describe('stringLikeRegexp', () => { + let matcher: Matcher; + + test('simple', () => { + matcher = Match.stringLikeRegexp('.*includeHeaders = true.*'); + expectFailure(matcher, 'const includeHeaders = false;', [/did not match pattern/]); + expectPass(matcher, 'const includeHeaders = true;'); + }); + + test('nested in object', () => { + matcher = Match.objectLike({ foo: Match.stringLikeRegexp('.*includeHeaders = true.*') }); + expectFailure(matcher, { foo: 'const includeHeaders = false;' }, [/did not match pattern/]); + expectPass(matcher, { foo: 'const includeHeaders = true;' }); + }); + }); }); function expectPass(matcher: Matcher, target: any): void { const result = matcher.test(target); if (result.hasFailed()) { - fail(result.toHumanStrings()); // eslint-disable-line jest/no-jasmine-globals + throw new Error(result.toHumanStrings().join('\n')); // eslint-disable-line jest/no-jasmine-globals } } diff --git a/packages/@aws-cdk/assertions/test/template.test.ts b/packages/@aws-cdk/assertions/test/template.test.ts index f5068cede6265..dcdb73e61da71 100644 --- a/packages/@aws-cdk/assertions/test/template.test.ts +++ b/packages/@aws-cdk/assertions/test/template.test.ts @@ -1,15 +1,15 @@ -import { App, CfnMapping, CfnOutput, CfnResource, NestedStack, Stack } from '@aws-cdk/core'; +import { App, CfnCondition, CfnMapping, CfnOutput, CfnParameter, CfnResource, Fn, NestedStack, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Capture, Match, Template } from '../lib'; describe('Template', () => { test('fromString', () => { const template = Template.fromString(`{ - "Resources": { - "Foo": { + "Resources": { + "Foo": { "Type": "Baz::Qux", "Properties": { "Fred": "Waldo" } - } + } } }`); @@ -79,11 +79,11 @@ describe('Template', () => { describe('fromString', () => { test('default', () => { const assertions = Template.fromString(`{ - "Resources": { - "Foo": { + "Resources": { + "Foo": { "Type": "Baz::Qux", "Properties": { "Fred": "Waldo" } - } + } } }`); assertions.resourceCountIs('Baz::Qux', 1); @@ -708,6 +708,156 @@ describe('Template', () => { }); }); + describe('findParameters', () => { + test('matching', () => { + const stack = new Stack(); + new CfnParameter(stack, 'p1', { + type: 'String', + description: 'string parameter', + }); + new CfnParameter(stack, 'p2', { + type: 'Number', + description: 'number parameter', + }); + + const inspect = Template.fromStack(stack); + const result = inspect.findParameters('*', { Type: 'String' }); + expect(result).toEqual({ + p1: { + Description: 'string parameter', + Type: 'String', + }, + }); + }); + + test('not matching', () => { + const stack = new Stack(); + new CfnParameter(stack, 'p1', { + type: 'String', + description: 'string parameter', + }); + + const inspect = Template.fromStack(stack); + const result = inspect.findParameters('*', { Type: 'Number' }); + expect(Object.keys(result).length).toEqual(0); + }); + + test('matching with specific parameter name', () => { + const stack = new Stack(); + new CfnParameter(stack, 'p1', { + type: 'String', + description: 'string parameter', + }); + new CfnParameter(stack, 'p2', { + type: 'Number', + description: 'number parameter', + }); + + const inspect = Template.fromStack(stack); + const result = inspect.findParameters('p1', { Type: 'String' }); + expect(result).toEqual({ + p1: { + Description: 'string parameter', + Type: 'String', + }, + }); + }); + + test('not matching specific parameter name', () => { + const stack = new Stack(); + new CfnParameter(stack, 'p1', { + type: 'String', + description: 'string parameter', + }); + new CfnParameter(stack, 'p2', { + type: 'Number', + description: 'number parameter', + }); + + const inspect = Template.fromStack(stack); + const result = inspect.findParameters('p3', { Type: 'String' }); + expect(Object.keys(result).length).toEqual(0); + }); + }); + + describe('hasParameter', () => { + test('matching', () => { + const stack = new Stack(); + new CfnParameter(stack, 'p1', { + type: 'String', + description: 'string parameter', + }); + new CfnParameter(stack, 'p2', { + type: 'Number', + description: 'number parameter', + }); + + const inspect = Template.fromStack(stack); + expect(() => inspect.findParameters('p3', { Type: 'String' })).not.toThrow(); + }); + + test('not matching', (done) => { + const stack = new Stack(); + new CfnParameter(stack, 'p1', { + type: 'String', + description: 'string parameter', + }); + new CfnParameter(stack, 'p2', { + type: 'Number', + description: 'number parameter', + }); + + const inspect = Template.fromStack(stack); + expectToThrow( + () => inspect.hasParameter('*', { Type: 'CommaDelimitedList' }), + [ + /2 parameters/, + /Expected CommaDelimitedList but received String/, + ], + done, + ); + done(); + }); + + test('matching specific parameter name', () => { + const stack = new Stack(); + new CfnParameter(stack, 'p1', { + type: 'String', + description: 'string parameter', + }); + new CfnParameter(stack, 'p2', { + type: 'Number', + description: 'number parameter', + }); + + const inspect = Template.fromStack(stack); + expect(() => inspect.findParameters('p1', { Type: 'String' })).not.toThrow(); + }); + + test('not matching specific parameter name', (done) => { + const stack = new Stack(); + new CfnParameter(stack, 'p1', { + type: 'String', + description: 'string parameter', + }); + new CfnParameter(stack, 'p2', { + type: 'Number', + description: 'number parameter', + }); + + const inspect = Template.fromStack(stack); + expectToThrow( + () => inspect.hasParameter('p2', { Type: 'CommaDelimitedList' }), + [ + /1 parameter/, + /Expected CommaDelimitedList but received Number/, + ], + done, + ); + done(); + }); + }); + describe('findMappings', () => { test('matching', () => { const stack = new Stack(); @@ -790,6 +940,169 @@ describe('Template', () => { expect(Object.keys(result).length).toEqual(0); }); }); + + describe('hasCondition', () => { + test('matching', () => { + const stack = new Stack(); + new CfnCondition(stack, 'Foo', { + expression: Fn.conditionEquals('Bar', 'Baz'), + }); + + const inspect = Template.fromStack(stack); + expect(() => inspect.hasCondition('*', { 'Fn::Equals': ['Bar', 'Baz'] })).not.toThrow(); + }); + + test('not matching', (done) => { + const stack = new Stack(); + new CfnCondition(stack, 'Foo', { + expression: Fn.conditionEquals('Bar', 'Baz'), + }); + + new CfnCondition(stack, 'Qux', { + expression: Fn.conditionNot(Fn.conditionEquals('Quux', 'Quuz')), + }); + + const inspect = Template.fromStack(stack); + expectToThrow( + () => inspect.hasCondition('*', { + 'Fn::Equals': ['Baz', 'Bar'], + }), + [ + /2 conditions/, + /Missing key/, + ], + done, + ); + done(); + }); + + test('matching specific outputName', () => { + const stack = new Stack(); + new CfnCondition(stack, 'Foo', { + expression: Fn.conditionEquals('Bar', 'Baz'), + }); + + const inspect = Template.fromStack(stack); + expect(() => inspect.hasCondition('Foo', { 'Fn::Equals': ['Bar', 'Baz'] })).not.toThrow(); + }); + + test('not matching specific outputName', (done) => { + const stack = new Stack(); + new CfnCondition(stack, 'Foo', { + expression: Fn.conditionEquals('Baz', 'Bar'), + }); + + const inspect = Template.fromStack(stack); + expectToThrow( + () => inspect.hasCondition('Foo', { + 'Fn::Equals': ['Bar', 'Baz'], + }), + [ + /1 conditions/, + /Expected Baz but received Bar/, + ], + done, + ); + done(); + }); + }); + + describe('findConditions', () => { + test('matching', () => { + const stack = new Stack(); + new CfnCondition(stack, 'Foo', { + expression: Fn.conditionEquals('Bar', 'Baz'), + }); + + new CfnCondition(stack, 'Qux', { + expression: Fn.conditionNot(Fn.conditionEquals('Quux', 'Quuz')), + }); + + const inspect = Template.fromStack(stack); + const firstCondition = inspect.findConditions('Foo'); + expect(firstCondition).toEqual({ + Foo: { + 'Fn::Equals': [ + 'Bar', + 'Baz', + ], + }, + }); + + const secondCondition = inspect.findConditions('Qux'); + expect(secondCondition).toEqual({ + Qux: { + 'Fn::Not': [ + { + 'Fn::Equals': [ + 'Quux', + 'Quuz', + ], + }, + ], + }, + }); + }); + + test('not matching', () => { + const stack = new Stack(); + new CfnCondition(stack, 'Foo', { + expression: Fn.conditionEquals('Bar', 'Baz'), + }); + + const inspect = Template.fromStack(stack); + const result = inspect.findMappings('Bar'); + expect(Object.keys(result).length).toEqual(0); + }); + + test('matching with specific outputName', () => { + const stack = new Stack(); + new CfnCondition(stack, 'Foo', { + expression: Fn.conditionEquals('Bar', 'Baz'), + }); + + const inspect = Template.fromStack(stack); + const result = inspect.findConditions('Foo', { 'Fn::Equals': ['Bar', 'Baz'] }); + expect(result).toEqual({ + Foo: { + 'Fn::Equals': [ + 'Bar', + 'Baz', + ], + }, + }); + }); + + test('not matching specific output name', () => { + const stack = new Stack(); + new CfnCondition(stack, 'Foo', { + expression: Fn.conditionEquals('Bar', 'Baz'), + }); + + const inspect = Template.fromStack(stack); + const result = inspect.findConditions('Foo', { 'Fn::Equals': ['Bar', 'Qux'] }); + expect(Object.keys(result).length).toEqual(0); + }); + }); + + test('throws when given a template with cyclic dependencies', () => { + expect(() => { + Template.fromJSON({ + Resources: { + Res1: { + Type: 'Foo', + Properties: { + Thing: { Ref: 'Res2' }, + }, + }, + Res2: { + Type: 'Foo', + DependsOn: ['Res1'], + }, + }, + }); + }).toThrow(/dependency cycle/); + }); }); function expectToThrow(fn: () => void, msgs: (RegExp | string)[], done: jest.DoneCallback): void { diff --git a/packages/@aws-cdk/assets/package.json b/packages/@aws-cdk/assets/package.json index 85a3265b08b2c..30adc5c82cfcb 100644 --- a/packages/@aws-cdk/assets/package.json +++ b/packages/@aws-cdk/assets/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -69,14 +76,14 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "@types/sinon": "^9.0.11", "aws-cdk": "0.0.0", - "jest": "^27.4.5", + "jest": "^27.5.1", "sinon": "^9.2.4", "ts-mock-imports": "^1.3.8" }, diff --git a/packages/@aws-cdk/assets/test/compat.test.ts b/packages/@aws-cdk/assets/test/compat.test.ts index 25afe6dd9411b..dfb3c3afd2daa 100644 --- a/packages/@aws-cdk/assets/test/compat.test.ts +++ b/packages/@aws-cdk/assets/test/compat.test.ts @@ -1,5 +1,4 @@ import { SymlinkFollowMode } from '@aws-cdk/core'; -import '@aws-cdk/assert-internal/jest'; import { FollowMode } from '../lib'; import { toSymlinkFollow } from '../lib/compat'; diff --git a/packages/@aws-cdk/assets/test/staging.test.ts b/packages/@aws-cdk/assets/test/staging.test.ts index bd924e434207b..8893c6451f2c1 100644 --- a/packages/@aws-cdk/assets/test/staging.test.ts +++ b/packages/@aws-cdk/assets/test/staging.test.ts @@ -3,7 +3,6 @@ import * as path from 'path'; import { describeDeprecated } from '@aws-cdk/cdk-build-tools'; import { App, Stack } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import '@aws-cdk/assert-internal/jest'; import { Staging } from '../lib'; describeDeprecated('staging', () => { diff --git a/packages/@aws-cdk/aws-accessanalyzer/package.json b/packages/@aws-cdk/aws-accessanalyzer/package.json index 6ecb0ec375882..ed8871901f393 100644 --- a/packages/@aws-cdk/aws-accessanalyzer/package.json +++ b/packages/@aws-cdk/aws-accessanalyzer/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-acmpca/README.md b/packages/@aws-cdk/aws-acmpca/README.md index 66f15be5be271..d6f2c754f808e 100644 --- a/packages/@aws-cdk/aws-acmpca/README.md +++ b/packages/@aws-cdk/aws-acmpca/README.md @@ -14,7 +14,7 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. -```ts +```ts nofixture import * as acmpca from '@aws-cdk/aws-acmpca'; ``` @@ -62,6 +62,8 @@ If you need to pass the higher-level `ICertificateAuthority` somewhere, you can get it from the lower-level `CfnCertificateAuthority` using the same `fromCertificateAuthorityArn` method: ```ts +declare const cfnCertificateAuthority: acmpca.CfnCertificateAuthority; + const certificateAuthority = acmpca.CertificateAuthority.fromCertificateAuthorityArn(this, 'CertificateAuthority', cfnCertificateAuthority.attrArn); ``` diff --git a/packages/@aws-cdk/aws-acmpca/package.json b/packages/@aws-cdk/aws-acmpca/package.json index e63b1a1157bc5..8522c051e20b0 100644 --- a/packages/@aws-cdk/aws-acmpca/package.json +++ b/packages/@aws-cdk/aws-acmpca/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -74,11 +81,11 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-acmpca/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-acmpca/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..16526213b2c62 --- /dev/null +++ b/packages/@aws-cdk/aws-acmpca/rosetta/default.ts-fixture @@ -0,0 +1,11 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as acmpca from '@aws-cdk/aws-acmpca'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} diff --git a/packages/@aws-cdk/aws-acmpca/test/acmpca.test.ts b/packages/@aws-cdk/aws-acmpca/test/acmpca.test.ts index c4505ad966984..b7175f106094e 100644 --- a/packages/@aws-cdk/aws-acmpca/test/acmpca.test.ts +++ b/packages/@aws-cdk/aws-acmpca/test/acmpca.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import {} from '../lib'; test('No tests are specified for this package', () => { diff --git a/packages/@aws-cdk/aws-amazonmq/package.json b/packages/@aws-cdk/aws-amazonmq/package.json index 4d2faba7936fa..56d6e296c4ed0 100644 --- a/packages/@aws-cdk/aws-amazonmq/package.json +++ b/packages/@aws-cdk/aws-amazonmq/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -77,7 +84,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-amplify/README.md b/packages/@aws-cdk/aws-amplify/README.md index c015bd38494e8..fffaf2ac97bcf 100644 --- a/packages/@aws-cdk/aws-amplify/README.md +++ b/packages/@aws-cdk/aws-amplify/README.md @@ -29,37 +29,36 @@ To set up an Amplify Console app, define an `App`: ```ts import * as codebuild from '@aws-cdk/aws-codebuild'; -import * as amplify from '@aws-cdk/aws-amplify'; -import * as cdk from '@aws-cdk/core'; const amplifyApp = new amplify.App(this, 'MyApp', { sourceCodeProvider: new amplify.GitHubSourceCodeProvider({ owner: '', repository: '', - oauthToken: cdk.SecretValue.secretsManager('my-github-token') + oauthToken: SecretValue.secretsManager('my-github-token'), }), - buildSpec: codebuild.BuildSpec.fromObjectToYaml({ // Alternatively add a `amplify.yml` to the repo + buildSpec: codebuild.BuildSpec.fromObjectToYaml({ + // Alternatively add a `amplify.yml` to the repo version: '1.0', frontend: { phases: { preBuild: { commands: [ - 'yarn' - ] + 'yarn', + ], }, build: { commands: [ - 'yarn build' - ] - } + 'yarn build', + ], + }, }, artifacts: { baseDirectory: 'public', files: - - '**/*' - } - } - }) + - '**/*', + }, + }, + }), }); ``` @@ -70,20 +69,22 @@ const amplifyApp = new amplify.App(this, 'MyApp', { sourceCodeProvider: new amplify.GitLabSourceCodeProvider({ owner: '', repository: '', - oauthToken: cdk.SecretValue.secretsManager('my-gitlab-token') - }) + oauthToken: SecretValue.secretsManager('my-gitlab-token'), + }), }); ``` To connect your `App` to CodeCommit, use the `CodeCommitSourceCodeProvider`: ```ts +import * as codecommit from '@aws-cdk/aws-codecommit'; + const repository = new codecommit.Repository(this, 'Repo', { - repositoryName: 'my-repo' + repositoryName: 'my-repo', }); const amplifyApp = new amplify.App(this, 'App', { - sourceCodeProvider: new amplify.CodeCommitSourceCodeProvider({ repository }) + sourceCodeProvider: new amplify.CodeCommitSourceCodeProvider({ repository }), }); ``` @@ -93,8 +94,12 @@ to pull the CodeCommit repository. Add branches: ```ts +declare const amplifyApp: amplify.App; + const master = amplifyApp.addBranch('master'); // `id` will be used as repo branch name -const dev = amplifyApp.addBranch('dev'); +const dev = amplifyApp.addBranch('dev', { + performanceMode: true, // optional, enables performance mode +}); dev.addEnvironment('STAGE', 'dev'); ``` @@ -103,10 +108,11 @@ Auto build and pull request preview are enabled by default. Add custom rules for redirection: ```ts +declare const amplifyApp: amplify.App; amplifyApp.addCustomRule({ source: '/docs/specific-filename.html', target: '/documents/different-filename.html', - status: amplify.RedirectStatus.TEMPORARY_REDIRECT + status: amplify.RedirectStatus.TEMPORARY_REDIRECT, }); ``` @@ -117,12 +123,18 @@ file extensions: css, gif, ico, jpg, js, png, txt, svg, woff, ttf, map, json, webmanifest. ```ts +declare const mySinglePageApp: amplify.App; + mySinglePageApp.addCustomRule(amplify.CustomRule.SINGLE_PAGE_APPLICATION_REDIRECT); ``` Add a domain and map sub domains to branches: ```ts +declare const amplifyApp: amplify.App; +declare const master: amplify.Branch; +declare const dev: amplify.Branch; + const domain = amplifyApp.addDomain('example.com', { enableAutoSubdomain: true, // in case subdomains should be auto registered for branches autoSubdomainCreationPatterns: ['*', 'pr*'], // regex for branches that should auto register subdomains @@ -140,9 +152,12 @@ Use `BasicAuth.fromCredentials` when referencing an existing secret: ```ts const amplifyApp = new amplify.App(this, 'MyApp', { - repository: 'https://github.com//', - oauthToken: cdk.SecretValue.secretsManager('my-github-token'), - basicAuth: amplify.BasicAuth.fromCredentials('username', cdk.SecretValue.secretsManager('my-github-token')) + sourceCodeProvider: new amplify.GitHubSourceCodeProvider({ + owner: '', + repository: '', + oauthToken: SecretValue.secretsManager('my-github-token'), + }), + basicAuth: amplify.BasicAuth.fromCredentials('username', SecretValue.secretsManager('my-github-token')), }); ``` @@ -150,17 +165,21 @@ Use `BasicAuth.fromGeneratedPassword` to generate a password in Secrets Manager: ```ts const amplifyApp = new amplify.App(this, 'MyApp', { - repository: 'https://github.com//', - oauthToken: cdk.SecretValue.secretsManager('my-github-token'), - basicAuth: amplify.BasicAuth.fromGeneratedPassword('username') + sourceCodeProvider: new amplify.GitHubSourceCodeProvider({ + owner: '', + repository: '', + oauthToken: SecretValue.secretsManager('my-github-token'), + }), + basicAuth: amplify.BasicAuth.fromGeneratedPassword('username'), }); ``` Basic auth can be added to specific branches: ```ts -app.addBranch('feature/next', { - basicAuth: amplify.BasicAuth.fromGeneratedPassword('username') +declare const amplifyApp: amplify.App; +amplifyApp.addBranch('feature/next', { + basicAuth: amplify.BasicAuth.fromGeneratedPassword('username'), }); ``` @@ -171,11 +190,14 @@ of branches: ```ts const amplifyApp = new amplify.App(this, 'MyApp', { - repository: 'https://github.com//', - oauthToken: cdk.SecretValue.secretsManager('my-github-token'), + sourceCodeProvider: new amplify.GitHubSourceCodeProvider({ + owner: '', + repository: '', + oauthToken: SecretValue.secretsManager('my-github-token'), + }), autoBranchCreation: { // Automatically connect branches that match a pattern set - patterns: ['feature/*', 'test/*'] - } + patterns: ['feature/*', 'test/*'], + }, autoBranchDeletion: true, // Automatically disconnect a branch when you delete a branch from your repository }); ``` @@ -185,11 +207,11 @@ const amplifyApp = new amplify.App(this, 'MyApp', { Use the `customResponseHeaders` prop to configure custom response headers for an Amplify app: ```ts -const amplifyApp = new amplify.App(stack, 'App', { +const amplifyApp = new amplify.App(this, 'App', { sourceCodeProvider: new amplify.GitHubSourceCodeProvider({ owner: '', repository: '', - oauthToken: cdk.SecretValue.secretsManager('my-github-token') + oauthToken: SecretValue.secretsManager('my-github-token'), }), customResponseHeaders: [ { @@ -214,7 +236,9 @@ const amplifyApp = new amplify.App(stack, 'App', { `sourceCodeProvider` is optional; when this is not specified the Amplify app can be deployed to using `.zip` packages. The `asset` property can be used to deploy S3 assets to Amplify as part of the CDK: ```ts -const asset = new assets.Asset(this, "SampleAsset", {}); -const amplifyApp = new amplify.App(this, 'MyApp', {}); +import * as assets from '@aws-cdk/aws-s3-assets'; + +declare const asset: assets.Asset; +declare const amplifyApp: amplify.App; const branch = amplifyApp.addBranch("dev", { asset: asset }); ``` diff --git a/packages/@aws-cdk/aws-amplify/lib/app.ts b/packages/@aws-cdk/aws-amplify/lib/app.ts index 030dc58059a6e..bedec6e9e58ac 100644 --- a/packages/@aws-cdk/aws-amplify/lib/app.ts +++ b/packages/@aws-cdk/aws-amplify/lib/app.ts @@ -28,7 +28,7 @@ export interface SourceCodeProviderConfig { /** * The repository for the application. Must use the `HTTPS` protocol. * - * @example https://github.com/aws/aws-cdk + * For example, `https://github.com/aws/aws-cdk`. */ readonly repository: string; diff --git a/packages/@aws-cdk/aws-amplify/lib/branch.ts b/packages/@aws-cdk/aws-amplify/lib/branch.ts index 3298f7a246056..40c1bdb3d0671 100644 --- a/packages/@aws-cdk/aws-amplify/lib/branch.ts +++ b/packages/@aws-cdk/aws-amplify/lib/branch.ts @@ -114,6 +114,17 @@ export interface BranchOptions { * @default - no asset */ readonly asset?: Asset + + /** + * Enables performance mode for the branch. + * + * Performance mode optimizes for faster hosting performance by keeping content cached at the edge + * for a longer interval. When performance mode is enabled, hosting configuration or code changes + * can take up to 10 minutes to roll out. + * + * @default false + */ + readonly performanceMode?: boolean; } /** @@ -168,6 +179,7 @@ export class Branch extends Resource implements IBranch { environmentVariables: Lazy.any({ produce: () => renderEnvironmentVariables(this.environmentVariables) }, { omitEmptyArray: true }), pullRequestEnvironmentName: props.pullRequestEnvironmentName, stage: props.stage, + enablePerformanceMode: props.performanceMode, }); this.arn = branch.attrArn; diff --git a/packages/@aws-cdk/aws-amplify/package.json b/packages/@aws-cdk/aws-amplify/package.json index f5903f7a8ca74..7a221bfd747f5 100644 --- a/packages/@aws-cdk/aws-amplify/package.json +++ b/packages/@aws-cdk/aws-amplify/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -79,7 +86,7 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "@types/yaml": "1.9.6", "aws-sdk": "^2.848.0" }, diff --git a/packages/@aws-cdk/aws-amplify/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-amplify/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..922b7902f805d --- /dev/null +++ b/packages/@aws-cdk/aws-amplify/rosetta/default.ts-fixture @@ -0,0 +1,11 @@ +// Fixture with packages imported, but nothing else +import { SecretValue, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as amplify from '@aws-cdk/aws-amplify'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} diff --git a/packages/@aws-cdk/aws-amplify/test/branch.test.ts b/packages/@aws-cdk/aws-amplify/test/branch.test.ts index ba8e517205beb..888a92117fb9c 100644 --- a/packages/@aws-cdk/aws-amplify/test/branch.test.ts +++ b/packages/@aws-cdk/aws-amplify/test/branch.test.ts @@ -162,3 +162,15 @@ test('with asset deployment', () => { }, }); }); + +test('with performance mode', () => { + // WHEN + app.addBranch('dev', { + performanceMode: true, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Amplify::Branch', { + EnablePerformanceMode: true, + }); +}); diff --git a/packages/@aws-cdk/aws-apigateway/lib/api-key.ts b/packages/@aws-cdk/aws-apigateway/lib/api-key.ts index c6dd3e8c44dbd..1852ab0a42ad1 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/api-key.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/api-key.ts @@ -165,7 +165,7 @@ export class ApiKey extends ApiKeyBase { const resource = new CfnApiKey(this, 'Resource', { customerId: props.customerId, description: props.description, - enabled: props.enabled || true, + enabled: props.enabled ?? true, generateDistinctId: props.generateDistinctId, name: this.physicalName, stageKeys: this.renderStageKeys(props.resources), diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts index a354c8a4b3196..f7be4f954d7e8 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts @@ -1,6 +1,6 @@ import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; -import { Duration, Lazy, Names, Stack } from '@aws-cdk/core'; +import { Arn, ArnFormat, Duration, Lazy, Names, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnAuthorizer } from '../apigateway.generated'; import { Authorizer, IAuthorizer } from '../authorizer'; @@ -254,5 +254,6 @@ export class RequestAuthorizer extends LambdaAuthorizer { * constructs the authorizerURIArn. */ function lambdaAuthorizerArn(handler: lambda.IFunction) { - return `arn:${Stack.of(handler).partition}:apigateway:${Stack.of(handler).region}:lambda:path/2015-03-31/functions/${handler.functionArn}/invocations`; + const { region, partition } = Arn.split( handler.functionArn, ArnFormat.COLON_RESOURCE_NAME); + return `arn:${partition}:apigateway:${region}:lambda:path/2015-03-31/functions/${handler.functionArn}/invocations`; } diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index 714e99bce7a0b..f843ee1b5e25a 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -311,8 +311,8 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc const template = new Array(); - template.push('#set($origin = $input.params("Origin"))'); - template.push('#if($origin == "") #set($origin = $input.params("origin")) #end'); + template.push('#set($origin = $input.params().header.get("Origin"))'); + template.push('#if($origin == "") #set($origin = $input.params().header.get("origin")) #end'); const condition = origins.map(o => `$origin.matches("${o}")`).join(' || '); diff --git a/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts b/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts index 4db28eb936e80..2f10071259517 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts @@ -6,7 +6,7 @@ import { CfnUsagePlan, CfnUsagePlanKey } from './apigateway.generated'; import { Method } from './method'; import { IRestApi } from './restapi'; import { Stage } from './stage'; -import { validateInteger } from './util'; +import { validateDouble, validateInteger } from './util'; /** * Container for defining throttling parameters to API stages or methods. @@ -316,7 +316,7 @@ export class UsagePlan extends UsagePlanBase { const burstLimit = props.burstLimit; validateInteger(burstLimit, 'Throttle burst limit'); const rateLimit = props.rateLimit; - validateInteger(rateLimit, 'Throttle rate limit'); + validateDouble(rateLimit, 'Throttle rate limit'); ret = { burstLimit: burstLimit, diff --git a/packages/@aws-cdk/aws-apigateway/lib/util.ts b/packages/@aws-cdk/aws-apigateway/lib/util.ts index 7facdc35bc2a0..550250edcd432 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/util.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/util.ts @@ -78,6 +78,12 @@ export function validateInteger(property: number | undefined, messagePrefix: str } } +export function validateDouble(property: number | undefined, messagePrefix: string) { + if (property && isNaN(property) && isNaN(parseFloat(property.toString()))) { + throw new Error(`${messagePrefix} should be an double`); + } +} + export class JsonSchemaMapper { /** * Transforms naming of some properties to prefix with a $, where needed diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index 1c6e564c1496b..3243e168f0919 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -79,12 +79,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-certificatemanager": "0.0.0", diff --git a/packages/@aws-cdk/aws-apigateway/test/access-log.test.ts b/packages/@aws-cdk/aws-apigateway/test/access-log.test.ts index 6509cc24b614b..3262207317c38 100644 --- a/packages/@aws-cdk/aws-apigateway/test/access-log.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/access-log.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import * as apigateway from '../lib'; describe('access log', () => { diff --git a/packages/@aws-cdk/aws-apigateway/test/api-definition.test.ts b/packages/@aws-cdk/aws-apigateway/test/api-definition.test.ts index c4af056038451..a74b3664dae26 100644 --- a/packages/@aws-cdk/aws-apigateway/test/api-definition.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/api-definition.test.ts @@ -1,6 +1,5 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as path from 'path'; -import { ResourcePart } from '@aws-cdk/assert-internal'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; @@ -84,12 +83,12 @@ describe('api definition', () => { apiDefinition: assetApiDefinition, }); - expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Template.fromStack(stack).hasResource('AWS::ApiGateway::RestApi', { Metadata: { 'aws:asset:path': 'asset.68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fb.yaml', 'aws:asset:property': 'BodyS3Location', }, - }, ResourcePart.CompleteDefinition); + }); }); }); diff --git a/packages/@aws-cdk/aws-apigateway/test/api-key.test.ts b/packages/@aws-cdk/aws-apigateway/test/api-key.test.ts index a929519d39c5a..46620128f7977 100644 --- a/packages/@aws-cdk/aws-apigateway/test/api-key.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/api-key.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as apigateway from '../lib'; @@ -13,14 +12,38 @@ describe('api key', () => { new apigateway.ApiKey(stack, 'my-api-key'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', undefined, ResourcePart.CompleteDefinition); - // should have an api key with no props defined. + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::ApiKey', { + Enabled: true, + }); + }); + + + test('enabled flag is respected', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new apigateway.ApiKey(stack, 'my-api-key', { + enabled: false, + value: 'arandomstringwithmorethantwentycharacters', + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::ApiKey', { + Enabled: false, + Value: 'arandomstringwithmorethantwentycharacters', + }); }); + test('specify props for apiKey', () => { // GIVEN const stack = new cdk.Stack(); - const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: true, deployOptions: { stageName: 'test' } }); + const api = new apigateway.RestApi(stack, 'test-api', { + cloudWatchRole: false, + deploy: true, + deployOptions: { stageName: 'test' }, + }); api.root.addMethod('GET'); // api must have atleast one method. // WHEN @@ -30,7 +53,7 @@ describe('api key', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::ApiKey', { CustomerId: 'test-customer', StageKeys: [ { @@ -53,7 +76,7 @@ describe('api key', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::ApiKey', { Description: 'The most secret api key', }); }); @@ -61,7 +84,11 @@ describe('api key', () => { test('use an imported api key', () => { // GIVEN const stack = new cdk.Stack(); - const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: true, deployOptions: { stageName: 'test' } }); + const api = new apigateway.RestApi(stack, 'test-api', { + cloudWatchRole: false, + deploy: true, + deployOptions: { stageName: 'test' }, + }); api.root.addMethod('GET'); // api must have atleast one method. // WHEN @@ -70,7 +97,7 @@ describe('api key', () => { usagePlan.addApiKey(importedKey); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::UsagePlanKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::UsagePlanKey', { KeyId: 'KeyIdabc', KeyType: 'API_KEY', UsagePlanId: { @@ -83,7 +110,11 @@ describe('api key', () => { // GIVEN const stack = new cdk.Stack(); const user = new iam.User(stack, 'User'); - const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: true, deployOptions: { stageName: 'test' } }); + const api = new apigateway.RestApi(stack, 'test-api', { + cloudWatchRole: false, + deploy: true, + deployOptions: { stageName: 'test' }, + }); api.root.addMethod('GET'); // api must have atleast one method. // WHEN @@ -94,7 +125,7 @@ describe('api key', () => { apiKey.grantRead(user); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -130,7 +161,11 @@ describe('api key', () => { // GIVEN const stack = new cdk.Stack(); const user = new iam.User(stack, 'User'); - const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: true, deployOptions: { stageName: 'test' } }); + const api = new apigateway.RestApi(stack, 'test-api', { + cloudWatchRole: false, + deploy: true, + deployOptions: { stageName: 'test' }, + }); api.root.addMethod('GET'); // api must have atleast one method. // WHEN @@ -141,7 +176,7 @@ describe('api key', () => { apiKey.grantWrite(user); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -182,7 +217,11 @@ describe('api key', () => { // GIVEN const stack = new cdk.Stack(); const user = new iam.User(stack, 'User'); - const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: true, deployOptions: { stageName: 'test' } }); + const api = new apigateway.RestApi(stack, 'test-api', { + cloudWatchRole: false, + deploy: true, + deployOptions: { stageName: 'test' }, + }); api.root.addMethod('GET'); // api must have atleast one method. // WHEN @@ -193,7 +232,7 @@ describe('api key', () => { apiKey.grantReadWrite(user); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -243,17 +282,21 @@ describe('api key', () => { // THEN // should have an api key with no props defined. - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', undefined, ResourcePart.CompleteDefinition); + Template.fromStack(stack).hasResource('AWS::ApiGateway::ApiKey', Match.anyValue()); // should not have a usage plan. - expect(stack).not.toHaveResource('AWS::ApiGateway::UsagePlan'); + Template.fromStack(stack).resourceCountIs('AWS::ApiGateway::UsagePlan', 0); // should not have a usage plan key. - expect(stack).not.toHaveResource('AWS::ApiGateway::UsagePlanKey'); + Template.fromStack(stack).resourceCountIs('AWS::ApiGateway::UsagePlanKey', 0); }); test('only api key is created when rate limiting properties are not provided', () => { // GIVEN const stack = new cdk.Stack(); - const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: true, deployOptions: { stageName: 'test' } }); + const api = new apigateway.RestApi(stack, 'test-api', { + cloudWatchRole: false, + deploy: true, + deployOptions: { stageName: 'test' }, + }); api.root.addMethod('GET'); // api must have atleast one method. // WHEN @@ -263,7 +306,7 @@ describe('api key', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::ApiKey', { CustomerId: 'test-customer', StageKeys: [ { @@ -273,15 +316,19 @@ describe('api key', () => { ], }); // should not have a usage plan. - expect(stack).not.toHaveResource('AWS::ApiGateway::UsagePlan'); + Template.fromStack(stack).resourceCountIs('AWS::ApiGateway::UsagePlan', 0); // should not have a usage plan key. - expect(stack).not.toHaveResource('AWS::ApiGateway::UsagePlanKey'); + Template.fromStack(stack).resourceCountIs('AWS::ApiGateway::UsagePlanKey', 0); }); test('api key and usage plan are created and linked when rate limiting properties are provided', () => { // GIVEN const stack = new cdk.Stack(); - const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: true, deployOptions: { stageName: 'test' } }); + const api = new apigateway.RestApi(stack, 'test-api', { + cloudWatchRole: false, + deploy: true, + deployOptions: { stageName: 'test' }, + }); api.root.addMethod('GET'); // api must have atleast one method. // WHEN @@ -296,7 +343,7 @@ describe('api key', () => { // THEN // should have an api key - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::ApiKey', { CustomerId: 'test-customer', StageKeys: [ { @@ -306,14 +353,14 @@ describe('api key', () => { ], }); // should have a usage plan with specified quota. - expect(stack).toHaveResource('AWS::ApiGateway::UsagePlan', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::UsagePlan', { Quota: { Limit: 10000, Period: 'MONTH', }, - }, ResourcePart.Properties); + }); // should have a usage plan key linking the api key and usage plan - expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::UsagePlanKey', { KeyId: { Ref: 'testapikey998028B6', }, @@ -321,7 +368,7 @@ describe('api key', () => { UsagePlanId: { Ref: 'testapikeyUsagePlanResource66DB63D6', }, - }, ResourcePart.Properties); + }); }); }); }); diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/cognito.test.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/cognito.test.ts index 1956c69dbb149..906f772a8505b 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/cognito.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/cognito.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cognito from '@aws-cdk/aws-cognito'; import { Duration, Stack } from '@aws-cdk/core'; import { AuthorizationType, CognitoUserPoolsAuthorizer, RestApi } from '../../lib'; @@ -21,7 +21,7 @@ describe('Cognito Authorizer', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Authorizer', { Type: 'COGNITO_USER_POOLS', RestApiId: stack.resolve(restApi.restApiId), IdentitySource: 'method.request.header.Authorization', @@ -52,7 +52,7 @@ describe('Cognito Authorizer', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Authorizer', { Type: 'COGNITO_USER_POOLS', Name: 'myauthorizer', RestApiId: stack.resolve(restApi.restApiId), diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.lit.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.lit.expected.json index 9768e9692a548..981f02ebed888 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.lit.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.lit.expected.json @@ -253,11 +253,37 @@ [ "arn:", { - "Ref": "AWS::Partition" + "Fn::Select": [ + 1, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "MyAuthorizerFunction70F1223E", + "Arn" + ] + } + ] + } + ] }, ":apigateway:", { - "Ref": "AWS::Region" + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "MyAuthorizerFunction70F1223E", + "Arn" + ] + } + ] + } + ] }, ":lambda:path/2015-03-31/functions/", { diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json index b3d35baa2e42c..eda922f948d66 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json @@ -122,11 +122,37 @@ [ "arn:", { - "Ref": "AWS::Partition" + "Fn::Select": [ + 1, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "MyAuthorizerFunction70F1223E", + "Arn" + ] + } + ] + } + ] }, ":apigateway:", { - "Ref": "AWS::Region" + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "MyAuthorizerFunction70F1223E", + "Arn" + ] + } + ] + } + ] }, ":lambda:path/2015-03-31/functions/", { diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.lit.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.lit.expected.json index f36705d28f193..237a238eefcaa 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.lit.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.lit.expected.json @@ -253,11 +253,37 @@ [ "arn:", { - "Ref": "AWS::Partition" + "Fn::Select": [ + 1, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "MyAuthorizerFunction70F1223E", + "Arn" + ] + } + ] + } + ] }, ":apigateway:", { - "Ref": "AWS::Region" + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "MyAuthorizerFunction70F1223E", + "Arn" + ] + } + ] + } + ] }, ":lambda:path/2015-03-31/functions/", { diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/lambda.test.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/lambda.test.ts index fdaa20af10cd3..a4eea0f56892d 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/lambda.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/lambda.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import { Duration, Stack } from '@aws-cdk/core'; @@ -25,7 +24,7 @@ describe('lambda authorizer', () => { authorizationType: AuthorizationType.CUSTOM, }); - expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Authorizer', { Type: 'TOKEN', RestApiId: stack.resolve(restApi.restApiId), IdentitySource: 'method.request.header.Authorization', @@ -35,11 +34,31 @@ describe('lambda authorizer', () => { [ 'arn:', { - Ref: 'AWS::Partition', + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + ], + }, + ], }, ':apigateway:', { - Ref: 'AWS::Region', + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + ], + }, + ], }, ':lambda:path/2015-03-31/functions/', { @@ -51,7 +70,7 @@ describe('lambda authorizer', () => { }, }); - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', Principal: 'apigateway.amazonaws.com', }); @@ -80,7 +99,7 @@ describe('lambda authorizer', () => { authorizationType: AuthorizationType.CUSTOM, }); - expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Authorizer', { Type: 'REQUEST', RestApiId: stack.resolve(restApi.restApiId), AuthorizerUri: { @@ -89,11 +108,31 @@ describe('lambda authorizer', () => { [ 'arn:', { - Ref: 'AWS::Partition', + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + ], + }, + ], }, ':apigateway:', { - Ref: 'AWS::Region', + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + ], + }, + ], }, ':lambda:path/2015-03-31/functions/', { @@ -105,7 +144,7 @@ describe('lambda authorizer', () => { }, }); - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', Principal: 'apigateway.amazonaws.com', }); @@ -154,7 +193,7 @@ describe('lambda authorizer', () => { authorizationType: AuthorizationType.CUSTOM, }); - expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Authorizer', { Type: 'TOKEN', RestApiId: stack.resolve(restApi.restApiId), IdentitySource: 'method.request.header.whoami', @@ -167,11 +206,31 @@ describe('lambda authorizer', () => { [ 'arn:', { - Ref: 'AWS::Partition', + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + ], + }, + ], }, ':apigateway:', { - Ref: 'AWS::Region', + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + ], + }, + ], }, ':lambda:path/2015-03-31/functions/', { @@ -206,7 +265,7 @@ describe('lambda authorizer', () => { authorizationType: AuthorizationType.CUSTOM, }); - expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Authorizer', { Type: 'REQUEST', RestApiId: stack.resolve(restApi.restApiId), IdentitySource: 'method.request.header.whoami', @@ -218,11 +277,31 @@ describe('lambda authorizer', () => { [ 'arn:', { - Ref: 'AWS::Partition', + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + ], + }, + ], }, ':apigateway:', { - Ref: 'AWS::Region', + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + ], + }, + ], }, ':lambda:path/2015-03-31/functions/', { @@ -260,7 +339,7 @@ describe('lambda authorizer', () => { authorizationType: AuthorizationType.CUSTOM, }); - expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Authorizer', { Type: 'TOKEN', RestApiId: stack.resolve(restApi.restApiId), AuthorizerUri: { @@ -269,11 +348,31 @@ describe('lambda authorizer', () => { [ 'arn:', { - Ref: 'AWS::Partition', + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + ], + }, + ], }, ':apigateway:', { - Ref: 'AWS::Region', + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + ], + }, + ], }, ':lambda:path/2015-03-31/functions/', { @@ -285,9 +384,9 @@ describe('lambda authorizer', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Role'); + Template.fromStack(stack).hasResource('AWS::IAM::Role', Match.anyValue()); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { Roles: [ stack.resolve(role.roleName), ], @@ -300,9 +399,9 @@ describe('lambda authorizer', () => { }, ], }, - }, ResourcePart.Properties); + }); - expect(stack).not.toHaveResource('AWS::Lambda::Permission'); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 0); }); test('request authorizer with assume role', () => { @@ -332,7 +431,7 @@ describe('lambda authorizer', () => { authorizationType: AuthorizationType.CUSTOM, }); - expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Authorizer', { Type: 'REQUEST', RestApiId: stack.resolve(restApi.restApiId), AuthorizerUri: { @@ -341,11 +440,31 @@ describe('lambda authorizer', () => { [ 'arn:', { - Ref: 'AWS::Partition', + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + ], + }, + ], }, ':apigateway:', { - Ref: 'AWS::Region', + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + ], + }, + ], }, ':lambda:path/2015-03-31/functions/', { @@ -357,9 +476,9 @@ describe('lambda authorizer', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Role'); + Template.fromStack(stack).hasResource('AWS::IAM::Role', Match.anyValue()); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { Roles: [ stack.resolve(role.roleName), ], @@ -372,9 +491,9 @@ describe('lambda authorizer', () => { }, ], }, - }, ResourcePart.Properties); + }); - expect(stack).not.toHaveResource('AWS::Lambda::Permission'); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 0); }); test('token authorizer throws when not attached to a rest api', () => { diff --git a/packages/@aws-cdk/aws-apigateway/test/base-path-mapping.test.ts b/packages/@aws-cdk/aws-apigateway/test/base-path-mapping.test.ts index 6e57ce68d1394..a886b3c957dad 100644 --- a/packages/@aws-cdk/aws-apigateway/test/base-path-mapping.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/base-path-mapping.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as acm from '@aws-cdk/aws-certificatemanager'; import * as cdk from '@aws-cdk/core'; import * as apigw from '../lib'; @@ -22,7 +22,7 @@ describe('BasePathMapping', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { DomainName: { Ref: 'MyDomainE4943FBC' }, RestApiId: { Ref: 'MyApi49610EDF' }, }); @@ -47,7 +47,7 @@ describe('BasePathMapping', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { BasePath: 'My_B45E-P4th', }); }); @@ -100,7 +100,7 @@ describe('BasePathMapping', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { Stage: { Ref: 'MyStage572B0482' }, }); }); diff --git a/packages/@aws-cdk/aws-apigateway/test/cors.test.ts b/packages/@aws-cdk/aws-apigateway/test/cors.test.ts index 42879b562df18..c573e6302589e 100644 --- a/packages/@aws-cdk/aws-apigateway/test/cors.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/cors.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as lambda from '@aws-cdk/aws-lambda'; import { Duration, Stack } from '@aws-cdk/core'; import * as apigw from '../lib'; @@ -16,7 +16,7 @@ describe('cors', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceD5CDB490' }, Integration: { @@ -63,7 +63,7 @@ describe('cors', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceD5CDB490' }, Integration: { @@ -112,7 +112,7 @@ describe('cors', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceD5CDB490' }, Integration: { @@ -159,7 +159,7 @@ describe('cors', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceD5CDB490' }, Integration: { @@ -219,7 +219,7 @@ describe('cors', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceD5CDB490' }, Integration: { @@ -277,7 +277,7 @@ describe('cors', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceD5CDB490' }, Integration: { @@ -290,7 +290,7 @@ describe('cors', () => { 'method.response.header.Access-Control-Allow-Methods': "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", }, ResponseTemplates: { - 'application/json': '#set($origin = $input.params("Origin"))\n#if($origin == "") #set($origin = $input.params("origin")) #end\n#if($origin.matches("https://amazon.com") || $origin.matches("https://aws.amazon.com"))\n #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin)\n#end', + 'application/json': '#set($origin = $input.params().header.get("Origin"))\n#if($origin == "") #set($origin = $input.params().header.get("origin")) #end\n#if($origin.matches("https://amazon.com") || $origin.matches("https://aws.amazon.com"))\n #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin)\n#end', }, StatusCode: '204', }, @@ -327,7 +327,7 @@ describe('cors', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceD5CDB490' }, Integration: { @@ -376,7 +376,7 @@ describe('cors', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceD5CDB490' }, Integration: { @@ -439,7 +439,7 @@ describe('cors', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceD5CDB490' }, Integration: { @@ -489,12 +489,12 @@ describe('cors', () => { resource.addResource('MyChildResource'); // THEN - expect(stack).toCountResources('AWS::ApiGateway::Method', 2); // on both resources - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).resourceCountIs('AWS::ApiGateway::Method', 2); // on both resources + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceD5CDB490' }, }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceMyChildResource2DC010C5' }, }); @@ -515,15 +515,15 @@ describe('cors', () => { child1.addResource('child2'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { 'Fn::GetAtt': ['apiC8550315', 'RootResourceId'] }, }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apichild1841A5840' }, }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apichild1child26A9A7C47' }, }); @@ -548,7 +548,7 @@ describe('cors', () => { }); // THENB - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { ResourceId: { Ref: 'apiAllowAll2F5BC564', }, @@ -579,7 +579,7 @@ describe('cors', () => { }, ], }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { ResourceId: { Ref: 'apiAllowSpecific77DD8AF1', }, @@ -646,8 +646,8 @@ describe('cors', () => { }); // THEN - expect(stack).toCountResources('AWS::ApiGateway::Method', 4); // two ANY and two OPTIONS resources - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).resourceCountIs('AWS::ApiGateway::Method', 4); // two ANY and two OPTIONS resources + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { 'Fn::GetAtt': [ @@ -656,7 +656,7 @@ describe('cors', () => { ], }, }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'lambdarestapiproxyE3AE07E3', @@ -676,7 +676,7 @@ describe('cors', () => { api.root.addProxy(); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', }); }); diff --git a/packages/@aws-cdk/aws-apigateway/test/deployment.test.ts b/packages/@aws-cdk/aws-apigateway/test/deployment.test.ts index 3f431bfb29f2a..ef537a848c070 100644 --- a/packages/@aws-cdk/aws-apigateway/test/deployment.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/deployment.test.ts @@ -1,6 +1,5 @@ import * as path from 'path'; -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart, SynthUtils } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as lambda from '@aws-cdk/aws-lambda'; import { CfnResource, Lazy, Stack } from '@aws-cdk/core'; import * as apigateway from '../lib'; @@ -16,7 +15,7 @@ describe('deployment', () => { new apigateway.Deployment(stack, 'deployment', { api }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { apiGETECF0BD67: { Type: 'AWS::ApiGateway::Method', @@ -68,7 +67,7 @@ describe('deployment', () => { new apigateway.Deployment(stack, 'deployment', { api, retainDeployments: true }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { apiGETECF0BD67: { Type: 'AWS::ApiGateway::Method', @@ -122,39 +121,54 @@ describe('deployment', () => { new apigateway.Deployment(stack, 'deployment', { api, description: 'this is my deployment' }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Deployment', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Deployment', { Description: 'this is my deployment', }); }); - test('logical ID of the deployment resource is salted', () => { - // GIVEN - const stack = new Stack(); - const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); - const deployment = new apigateway.Deployment(stack, 'deployment', { api }); - api.root.addMethod('GET'); + describe('logical ID of the deployment resource is salted', () => { + test('before salting', () => { + // GIVEN + const stack = new Stack(); + const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); + new apigateway.Deployment(stack, 'deployment', { api }); + api.root.addMethod('GET'); + + const resources = Template.fromStack(stack).findResources('AWS::ApiGateway::Deployment'); + expect(resources.deployment33381975bba46c5132329b81e7befcbbba5a0e75).toBeDefined(); + }); + + test('after salting with a resolved value', () => { + const stack = new Stack(); + const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); + const deployment = new apigateway.Deployment(stack, 'deployment', { api }); + api.root.addMethod('GET'); - const resources = synthesize().Resources; - expect(resources.deployment33381975bba46c5132329b81e7befcbbba5a0e75).toBeDefined(); + // adding some salt + deployment.addToLogicalId({ foo: 123 }); // add some data to the logical ID - // adding some salt - deployment.addToLogicalId({ foo: 123 }); // add some data to the logical ID + // the logical ID changed + const template = Template.fromStack(stack).findResources('AWS::ApiGateway::Deployment'); + expect(template.deployment33381975bba46c5132329b81e7befcbbba5a0e75).toBeUndefined(); + expect(template.deployment333819758aa4cdb9d204502b959c4903f4d5d29f).toBeDefined(); + }); - // the logical ID changed - const template = synthesize(); - expect(template.Resources.deployment33381975bba46c5132329b81e7befcbbba5a0e75).toBeUndefined(); - expect(template.Resources.deployment333819758aa4cdb9d204502b959c4903f4d5d29f).toBeDefined(); + test('after salting with a resolved value and a token', () => { + const stack = new Stack(); + const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); + const deployment = new apigateway.Deployment(stack, 'deployment', { api }); + api.root.addMethod('GET'); - // tokens supported, and are resolved upon synthesis - const value = 'hello hello'; - deployment.addToLogicalId({ foo: Lazy.string({ produce: () => value }) }); + // adding some salt + deployment.addToLogicalId({ foo: 123 }); // add some data to the logical ID - const template2 = synthesize(); - expect(template2.Resources.deployment333819758d91bed959c6bd6268ba84f6d33e888e).toBeDefined(); + // tokens supported, and are resolved upon synthesis + const value = 'hello hello'; + deployment.addToLogicalId({ foo: Lazy.string({ produce: () => value }) }); - function synthesize() { - return SynthUtils.synthesize(stack).template; - } + const template = Template.fromStack(stack).findResources('AWS::ApiGateway::Deployment'); + expect(template.deployment333819758d91bed959c6bd6268ba84f6d33e888e).toBeDefined(); + }); }); test('"addDependency" can be used to add a resource as a dependency', () => { @@ -169,12 +183,12 @@ describe('deployment', () => { // WHEN deployment.node.addDependency(dep); - expect(stack).toHaveResource('AWS::ApiGateway::Deployment', { + Template.fromStack(stack).hasResource('AWS::ApiGateway::Deployment', { DependsOn: [ 'apiGETECF0BD67', 'MyResource', ], - }, ResourcePart.CompleteDefinition); + }); }); @@ -205,10 +219,10 @@ describe('deployment', () => { api2.root.addMethod('GET'); // THEN - expect(stack1).toHaveResource('AWS::ApiGateway::Stage', { + Template.fromStack(stack1).hasResourceProperties('AWS::ApiGateway::Stage', { DeploymentId: { Ref: 'myapiDeploymentB7EF8EB74c5295c27fa87ff13f4d04e13f67662d' }, }); - expect(stack2).toHaveResource('AWS::ApiGateway::Stage', { + Template.fromStack(stack2).hasResourceProperties('AWS::ApiGateway::Stage', { DeploymentId: { Ref: 'myapiDeploymentB7EF8EB7b50d305057ba109c118e4aafd4509355' }, }); @@ -233,12 +247,12 @@ describe('deployment', () => { const resource = restapi.root.addResource('myresource'); resource.addMethod('GET'); - expect(stack).toHaveResource('AWS::ApiGateway::Deployment', { + Template.fromStack(stack).hasResource('AWS::ApiGateway::Deployment', { DependsOn: [ 'myapiGET9B7CD29E', 'myapimyresourceGET732851A5', 'myapiPOST23417BD2', ], - }, ResourcePart.CompleteDefinition); + }); }); }); diff --git a/packages/@aws-cdk/aws-apigateway/test/domains.test.ts b/packages/@aws-cdk/aws-apigateway/test/domains.test.ts index 31c553ad9c973..7e054c83d7102 100644 --- a/packages/@aws-cdk/aws-apigateway/test/domains.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/domains.test.ts @@ -1,5 +1,4 @@ -import { ABSENT } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as acm from '@aws-cdk/aws-certificatemanager'; import { Bucket } from '@aws-cdk/aws-s3'; import { Stack } from '@aws-cdk/core'; @@ -27,13 +26,13 @@ describe('domains', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'example.com', 'EndpointConfiguration': { 'Types': ['REGIONAL'] }, 'RegionalCertificateArn': { 'Ref': 'Cert5C9FAEC1' }, }); - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'example.com', 'EndpointConfiguration': { 'Types': ['EDGE'] }, 'CertificateArn': { 'Ref': 'Cert5C9FAEC1' }, @@ -57,7 +56,7 @@ describe('domains', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'example.com', 'EndpointConfiguration': { 'Types': ['REGIONAL'] }, 'RegionalCertificateArn': { 'Ref': 'Cert5C9FAEC1' }, @@ -88,25 +87,25 @@ describe('domains', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'old.example.com', 'EndpointConfiguration': { 'Types': ['REGIONAL'] }, 'RegionalCertificateArn': { 'Ref': 'Cert5C9FAEC1' }, 'SecurityPolicy': 'TLS_1_0', }); - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'new.example.com', 'EndpointConfiguration': { 'Types': ['REGIONAL'] }, 'RegionalCertificateArn': { 'Ref': 'Cert5C9FAEC1' }, 'SecurityPolicy': 'TLS_1_2', }); - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'default.example.com', 'EndpointConfiguration': { 'Types': ['REGIONAL'] }, 'RegionalCertificateArn': { 'Ref': 'Cert5C9FAEC1' }, - 'SecurityPolicy': ABSENT, + 'SecurityPolicy': Match.absent(), }); }); @@ -125,7 +124,7 @@ describe('domains', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'DomainName': { 'Ref': 'Domain66AC69E0', }, @@ -156,7 +155,7 @@ describe('domains', () => { domain.addBasePathMapping(api2, { basePath: 'api2' }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'DomainName': { 'Ref': 'mydomain592C948B', }, @@ -169,7 +168,7 @@ describe('domains', () => { }, }); - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'DomainName': { 'Ref': 'mydomain592C948B', }, @@ -197,7 +196,7 @@ describe('domains', () => { api.root.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'my.domain.com', 'EndpointConfiguration': { 'Types': [ @@ -208,7 +207,7 @@ describe('domains', () => { 'Ref': 'cert56CA94EB', }, }); - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'DomainName': { 'Ref': 'apiCustomDomain64773C4F', }, @@ -235,7 +234,7 @@ describe('domains', () => { api.addDomainName('domainId', { domainName, certificate }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': domainName, 'EndpointConfiguration': { 'Types': [ @@ -246,7 +245,7 @@ describe('domains', () => { 'Ref': 'cert56CA94EB', }, }); - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'DomainName': { 'Ref': 'apidomainId102F8DAA', }, @@ -274,7 +273,7 @@ describe('domains', () => { api.addDomainName('domainId', { domainName, certificate, basePath }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'BasePath': 'users', 'RestApiId': { 'Ref': 'apiC8550315', @@ -300,13 +299,13 @@ describe('domains', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'BasePath': 'users', 'RestApiId': { 'Ref': 'apiC8550315', }, }); - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'BasePath': 'books', 'RestApiId': { 'Ref': 'apiC8550315', @@ -361,7 +360,7 @@ describe('domains', () => { expect(api.domainName).toEqual(domainName1); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'my.domain.com', 'EndpointConfiguration': { 'Types': [ @@ -372,7 +371,7 @@ describe('domains', () => { 'Ref': 'cert56CA94EB', }, }); - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'your.domain.com', 'EndpointConfiguration': { 'Types': [ @@ -383,7 +382,7 @@ describe('domains', () => { 'Ref': 'cert56CA94EB', }, }); - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'our.domain.com', 'EndpointConfiguration': { 'Types': [ @@ -394,7 +393,7 @@ describe('domains', () => { 'Ref': 'cert56CA94EB', }, }); - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'DomainName': { 'Ref': 'apidomainId102F8DAA', }, @@ -433,7 +432,7 @@ describe('domains', () => { domain.addBasePathMapping(api2, { basePath: 'api2' }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'DomainName': { 'Ref': 'mydomain592C948B', }, @@ -444,7 +443,7 @@ describe('domains', () => { 'Stage': stack.resolve(testStage.stageName), }); - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'DomainName': { 'Ref': 'mydomain592C948B', }, @@ -471,7 +470,7 @@ describe('domains', () => { certificate: acm.Certificate.fromCertificateArn(stack, 'cert', 'arn:aws:acm:us-east-1:1111111:certificate/11-3336f1-44483d-adc7-9cd375c5169d'), }); - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'example.com', 'EndpointConfiguration': { 'Types': ['REGIONAL'] }, 'RegionalCertificateArn': 'arn:aws:acm:us-east-1:1111111:certificate/11-3336f1-44483d-adc7-9cd375c5169d', @@ -492,7 +491,7 @@ describe('domains', () => { version: 'version', }, }); - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'example.com', 'EndpointConfiguration': { 'Types': ['REGIONAL'] }, 'RegionalCertificateArn': 'arn:aws:acm:us-east-1:1111111:certificate/11-3336f1-44483d-adc7-9cd375c5169d', @@ -512,7 +511,7 @@ describe('domains', () => { }).root.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'DomainName': { 'Ref': 'restApiWithStageCustomDomainC4749625', }, @@ -543,7 +542,7 @@ describe('domains', () => { }).root.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'DomainName': { 'Ref': 'specRestApiWithStageCustomDomain8A36A5C9', }, diff --git a/packages/@aws-cdk/aws-apigateway/test/gateway-response.test.ts b/packages/@aws-cdk/aws-apigateway/test/gateway-response.test.ts index 7a02ca17b6ca0..80093aba04de1 100644 --- a/packages/@aws-cdk/aws-apigateway/test/gateway-response.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/gateway-response.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import { ResponseType, RestApi } from '../lib'; @@ -20,12 +19,12 @@ describe('gateway response', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::GatewayResponse', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::GatewayResponse', { ResponseType: 'ACCESS_DENIED', RestApiId: stack.resolve(api.restApiId), - StatusCode: ABSENT, - ResponseParameters: ABSENT, - ResponseTemplates: ABSENT, + StatusCode: Match.absent(), + ResponseParameters: Match.absent(), + ResponseTemplates: Match.absent(), }); }); @@ -50,7 +49,7 @@ describe('gateway response', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::GatewayResponse', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::GatewayResponse', { ResponseType: 'AUTHORIZER_FAILURE', RestApiId: stack.resolve(api.restApiId), StatusCode: '500', @@ -58,7 +57,7 @@ describe('gateway response', () => { 'gatewayresponse.header.Access-Control-Allow-Origin': 'test.com', 'gatewayresponse.header.test-key': 'test-value', }, - ResponseTemplates: ABSENT, + ResponseTemplates: Match.absent(), }); }); @@ -82,11 +81,11 @@ describe('gateway response', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::GatewayResponse', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::GatewayResponse', { ResponseType: 'AUTHORIZER_FAILURE', RestApiId: stack.resolve(api.restApiId), StatusCode: '500', - ResponseParameters: ABSENT, + ResponseParameters: Match.absent(), ResponseTemplates: { 'application/json': '{ "message": $context.error.messageString, "statusCode": "488" }', }, diff --git a/packages/@aws-cdk/aws-apigateway/test/http.test.ts b/packages/@aws-cdk/aws-apigateway/test/http.test.ts index 15714ca988087..4d687da1546b7 100644 --- a/packages/@aws-cdk/aws-apigateway/test/http.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/http.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as apigateway from '../lib'; @@ -14,7 +14,7 @@ describe('http integration', () => { api.root.addMethod('GET', integ); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { IntegrationHttpMethod: 'GET', Type: 'HTTP_PROXY', @@ -40,7 +40,7 @@ describe('http integration', () => { api.root.addMethod('GET', integ); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { CacheNamespace: 'hey', IntegrationHttpMethod: 'POST', diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json index 22b5c45a87b53..02d757e898afe 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json @@ -51,7 +51,7 @@ "corsapitest8682546E" ] }, - "corsapitestDeployment2BF1633A228079ea05e5799220dd4ca13512b92d": { + "corsapitestDeployment2BF1633A51392cbce1ac2785bd0e53063423e203": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { @@ -74,7 +74,7 @@ "Ref": "corsapitest8682546E" }, "DeploymentId": { - "Ref": "corsapitestDeployment2BF1633A228079ea05e5799220dd4ca13512b92d" + "Ref": "corsapitestDeployment2BF1633A51392cbce1ac2785bd0e53063423e203" }, "StageName": "prod" }, @@ -472,7 +472,7 @@ "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" }, "ResponseTemplates": { - "application/json": "#set($origin = $input.params(\"Origin\"))\n#if($origin == \"\") #set($origin = $input.params(\"origin\")) #end\n#if($origin.matches(\"https://www.test-cors.org\"))\n #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin)\n#end" + "application/json": "#set($origin = $input.params().header.get(\"Origin\"))\n#if($origin == \"\") #set($origin = $input.params().header.get(\"origin\")) #end\n#if($origin.matches(\"https://www.test-cors.org\"))\n #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin)\n#end" }, "StatusCode": "204" } diff --git a/packages/@aws-cdk/aws-apigateway/test/integration.test.ts b/packages/@aws-cdk/aws-apigateway/test/integration.test.ts index f89a1fdf042ed..0ae46986bcc93 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integration.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integration.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as iam from '@aws-cdk/aws-iam'; @@ -52,7 +51,7 @@ describe('integration', () => { }); api.root.addMethod('GET', integration); - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { Uri: { 'Fn::Join': [ @@ -133,7 +132,7 @@ describe('integration', () => { })).toThrow(/cannot set 'vpcLink' where 'connectionType' is INTERNET/); }); - test('connectionType is ABSENT when vpcLink is not specified', () => { + test('connectionType is Match.absent() when vpcLink is not specified', () => { // GIVEN const stack = new cdk.Stack(); const api = new apigw.RestApi(stack, 'restapi'); @@ -146,10 +145,10 @@ describe('integration', () => { api.root.addMethod('ANY', integration); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'ANY', Integration: { - ConnectionType: ABSENT, + ConnectionType: Match.absent(), }, }); }); @@ -177,7 +176,7 @@ describe('integration', () => { api.root.addMethod('ANY', integration); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'ANY', Integration: { ConnectionType: 'VPC_LINK', @@ -221,7 +220,7 @@ describe('integration', () => { api.root.addMethod('ANY', integration); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'ANY', Integration: { TimeoutInMillis: 1000, diff --git a/packages/@aws-cdk/aws-apigateway/test/integrations/lambda.test.ts b/packages/@aws-cdk/aws-apigateway/test/integrations/lambda.test.ts index 7c5b9e60e85d6..187e15e2b09cf 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integrations/lambda.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integrations/lambda.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; import * as apigateway from '../../lib'; @@ -19,7 +19,7 @@ describe('lambda', () => { api.root.addMethod('GET', integ); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { IntegrationHttpMethod: 'POST', Type: 'AWS_PROXY', @@ -66,7 +66,7 @@ describe('lambda', () => { api.root.addMethod('GET', integ); // THEN - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { SourceArn: { 'Fn::Join': [ '', @@ -78,7 +78,7 @@ describe('lambda', () => { }, }); - expect(stack).not.toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', Match.not({ SourceArn: { 'Fn::Join': [ '', @@ -95,7 +95,7 @@ describe('lambda', () => { ], ], }, - }); + })); }); test('"allowTestInvoke" set to true allows calling the API from the test UI', () => { @@ -114,7 +114,7 @@ describe('lambda', () => { api.root.addMethod('GET', integ); // THEN - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { SourceArn: { 'Fn::Join': [ '', @@ -150,7 +150,7 @@ describe('lambda', () => { api.root.addMethod('GET', integ); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { Type: 'AWS', }, @@ -171,7 +171,7 @@ describe('lambda', () => { api.root.addMethod('ANY', target); - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { SourceArn: { 'Fn::Join': [ '', @@ -190,7 +190,7 @@ describe('lambda', () => { }, }); - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { SourceArn: { 'Fn::Join': [ '', @@ -235,7 +235,7 @@ describe('lambda', () => { api.root.addMethod('ANY', new apigateway.LambdaIntegration(handler)); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { RestApiId: 'imported-rest-api-id', ResourceId: 'imported-root-resource-id', HttpMethod: 'ANY', diff --git a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts index c803fa974f7f6..830ca2a8e2000 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { stringLike, anything } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import { StateMachine, StateMachineType } from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; @@ -16,7 +15,7 @@ describe('StepFunctionsIntegration', () => { api.root.addMethod('GET', integ); //THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { ResourceId: { 'Fn::GetAtt': [ 'myrestapiBAC2BF45', @@ -74,16 +73,16 @@ describe('StepFunctionsIntegration', () => { const integ = apigw.StepFunctionsIntegration.startExecution(stateMachine); api.root.addMethod('GET', integ); - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { RequestTemplates: { 'application/json': { 'Fn::Join': [ '', [ - stringLike('*includeHeaders = false*'), + Match.stringLikeRegexp('includeHeaders = false'), { Ref: 'StateMachine2E01A3A5' }, - anything(), + Match.anyValue(), ], ], }, @@ -102,16 +101,16 @@ describe('StepFunctionsIntegration', () => { }); api.root.addMethod('GET', integ); - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { RequestTemplates: { 'application/json': { 'Fn::Join': [ '', [ - stringLike('*#set($includeHeaders = true)*'), + Match.stringLikeRegexp('#set\\(\\$includeHeaders = true\\)'), { Ref: 'StateMachine2E01A3A5' }, - anything(), + Match.anyValue(), ], ], }, @@ -128,16 +127,16 @@ describe('StepFunctionsIntegration', () => { const integ = apigw.StepFunctionsIntegration.startExecution(stateMachine); api.root.addMethod('GET', integ); - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { RequestTemplates: { 'application/json': { 'Fn::Join': [ '', [ - stringLike('*#set($includeQueryString = true)*'), + Match.stringLikeRegexp('#set\\(\\$includeQueryString = true\\)'), { Ref: 'StateMachine2E01A3A5' }, - anything(), + Match.anyValue(), ], ], }, @@ -145,16 +144,16 @@ describe('StepFunctionsIntegration', () => { }, }); - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { RequestTemplates: { 'application/json': { 'Fn::Join': [ '', [ - stringLike('*#set($includePath = true)*'), + Match.stringLikeRegexp('#set\\(\\$includePath = true\\)'), { Ref: 'StateMachine2E01A3A5' }, - anything(), + Match.anyValue(), ], ], }, @@ -174,16 +173,16 @@ describe('StepFunctionsIntegration', () => { }); api.root.addMethod('GET', integ); - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { RequestTemplates: { 'application/json': { 'Fn::Join': [ '', [ - stringLike('*#set($includeQueryString = false)*'), + Match.stringLikeRegexp('#set\\(\\$includeQueryString = false\\)'), { Ref: 'StateMachine2E01A3A5' }, - anything(), + Match.anyValue(), ], ], }, @@ -191,16 +190,16 @@ describe('StepFunctionsIntegration', () => { }, }); - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { RequestTemplates: { 'application/json': { 'Fn::Join': [ '', [ - stringLike('*#set($includePath = false)*'), + Match.stringLikeRegexp('#set\\(\\$includePath = false\\)'), { Ref: 'StateMachine2E01A3A5' }, - anything(), + Match.anyValue(), ], ], }, @@ -217,16 +216,16 @@ describe('StepFunctionsIntegration', () => { const integ = apigw.StepFunctionsIntegration.startExecution(stateMachine, {}); api.root.addMethod('GET', integ); - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { RequestTemplates: { 'application/json': { 'Fn::Join': [ '', [ - anything(), + Match.anyValue(), { Ref: 'StateMachine2E01A3A5' }, - stringLike('*#set($requestContext = \"\")*'), + Match.stringLikeRegexp('#set\\(\\$requestContext = \"\"\\)'), ], ], }, @@ -247,16 +246,16 @@ describe('StepFunctionsIntegration', () => { }); api.root.addMethod('GET', integ); - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { RequestTemplates: { 'application/json': { 'Fn::Join': [ '', [ - anything(), + Match.anyValue(), { Ref: 'StateMachine2E01A3A5' }, - stringLike('*#set($requestContext = \"{@@accountId@@:@@$context.identity.accountId@@}\"*'), + Match.stringLikeRegexp('#set\\(\\$requestContext = \"{@@accountId@@:@@\\$context.identity.accountId@@}\"'), ], ], }, @@ -283,7 +282,7 @@ describe('StepFunctionsIntegration', () => { api.root.addMethod('ANY', apigw.StepFunctionsIntegration.startExecution(stateMachine)); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { ResourceId: 'imported-root-resource-id', RestApiId: 'imported-rest-api-id', }); diff --git a/packages/@aws-cdk/aws-apigateway/test/lambda-api.test.ts b/packages/@aws-cdk/aws-apigateway/test/lambda-api.test.ts index 7e412c549e897..314225d3afe34 100644 --- a/packages/@aws-cdk/aws-apigateway/test/lambda-api.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/lambda-api.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; import * as apigw from '../lib'; @@ -23,11 +23,11 @@ describe('lambda api', () => { }).toThrow(); // THEN -- template proxies everything - expect(stack).toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: '{proxy+}', }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'ANY', ResourceId: { Ref: 'lambdarestapiproxyE3AE07E3', @@ -91,11 +91,11 @@ describe('lambda api', () => { }).toThrow(); // THEN -- template proxies everything - expect(stack).toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: '{proxy+}', }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'ANY', ResourceId: { Ref: 'lambdarestapiproxyE3AE07E3', @@ -149,20 +149,20 @@ describe('lambda api', () => { tasks.addMethod('POST'); // THEN - expect(stack).not.toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', Match.not({ PathPart: '{proxy+}', - }); + })); - expect(stack).toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: 'tasks', }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET', ResourceId: { Ref: 'lambdarestapitasks224418C8' }, }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'POST', ResourceId: { Ref: 'lambdarestapitasks224418C8' }, }); @@ -209,7 +209,7 @@ describe('lambda api', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'lambdarestapiproxyE3AE07E3' }, Integration: { diff --git a/packages/@aws-cdk/aws-apigateway/test/method.test.ts b/packages/@aws-cdk/aws-apigateway/test/method.test.ts index f1037e550e23a..de41b0dfe363f 100644 --- a/packages/@aws-cdk/aws-apigateway/test/method.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/method.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; @@ -24,7 +23,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'POST', AuthorizationType: 'NONE', Integration: { @@ -51,7 +50,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { ApiKeyRequired: true, OperationName: 'MyOperation', }); @@ -72,7 +71,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { IntegrationHttpMethod: 'POST', Type: 'AWS', @@ -104,7 +103,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { IntegrationHttpMethod: 'POST', Type: 'AWS', @@ -133,7 +132,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { IntegrationHttpMethod: 'GET', }, @@ -159,7 +158,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { Type: 'HTTP_PROXY', Uri: 'https://amazon.com', @@ -325,7 +324,7 @@ describe('method', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { Credentials: { 'Fn::GetAtt': ['MyRoleF48FFE04', 'Arn'] }, }, @@ -347,7 +346,7 @@ describe('method', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { Credentials: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::*:user/*']] }, }, @@ -386,7 +385,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET', MethodResponses: [{ StatusCode: '200', @@ -435,7 +434,7 @@ describe('method', () => { })); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { IntegrationHttpMethod: 'POST', IntegrationResponses: [ @@ -467,9 +466,9 @@ describe('method', () => { api.root.addMethod('PUT'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { HttpMethod: 'POST' }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { HttpMethod: 'GET' }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { HttpMethod: 'PUT' }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'POST' }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET' }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'PUT' }); }); @@ -499,7 +498,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET', RequestModels: { 'application/json': { Ref: stack.getLogicalId(model.node.findChild('Resource') as cdk.CfnElement) }, @@ -549,7 +548,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET', MethodResponses: [{ StatusCode: '200', @@ -593,10 +592,10 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { RequestValidatorId: { Ref: stack.getLogicalId(validator.node.findChild('Resource') as cdk.CfnElement) }, }); - expect(stack).toHaveResource('AWS::ApiGateway::RequestValidator', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RequestValidator', { RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, ValidateRequestBody: true, ValidateRequestParameters: false, @@ -626,7 +625,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { OperationName: 'defaultRequestParameters', RequestParameters: { 'method.request.path.proxy': true, @@ -644,7 +643,7 @@ describe('method', () => { authorizer: DUMMY_AUTHORIZER, }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'ANY', AuthorizationType: 'CUSTOM', AuthorizerId: DUMMY_AUTHORIZER.authorizerId, @@ -674,7 +673,7 @@ describe('method', () => { }); restApi.root.addMethod('ANY'); - expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Authorizer', { Name: 'myauthorizer1', Type: 'TOKEN', RestApiId: stack.resolve(restApi.restApiId), @@ -732,7 +731,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { ApiKeyRequired: true, AuthorizationScopes: ['AuthScope1', 'AuthScope2'], }); @@ -761,7 +760,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { OperationName: 'defaultAuthScopes', AuthorizationScopes: ['DefaultAuth'], }); @@ -791,7 +790,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { ApiKeyRequired: true, AuthorizationScopes: ['MethodAuthScope'], }); @@ -817,9 +816,9 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { OperationName: 'authScopesAbsent', - AuthorizationScopes: ABSENT, + AuthorizationScopes: Match.absent(), }); @@ -844,7 +843,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::RequestValidator', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RequestValidator', { RestApiId: stack.resolve(api.restApiId), ValidateRequestBody: true, ValidateRequestParameters: false, @@ -866,8 +865,8 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { - RequestValidatorId: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { + RequestValidatorId: Match.absent(), }); diff --git a/packages/@aws-cdk/aws-apigateway/test/model.test.ts b/packages/@aws-cdk/aws-apigateway/test/model.test.ts index 62602ef353959..4318f3c5b0093 100644 --- a/packages/@aws-cdk/aws-apigateway/test/model.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/model.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as apigw from '../lib'; @@ -24,7 +24,7 @@ describe('model', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Model', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Model', { RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, Schema: { $schema: 'http://json-schema.org/draft-04/schema#', @@ -57,7 +57,7 @@ describe('model', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Model', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Model', { RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, Schema: { $schema: 'http://json-schema.org/draft-04/schema#', diff --git a/packages/@aws-cdk/aws-apigateway/test/requestvalidator.test.ts b/packages/@aws-cdk/aws-apigateway/test/requestvalidator.test.ts index ec29752b47b6a..061c7853d3392 100644 --- a/packages/@aws-cdk/aws-apigateway/test/requestvalidator.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/requestvalidator.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as apigateway from '../lib'; @@ -20,7 +20,7 @@ describe('request validator', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::RequestValidator', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RequestValidator', { RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, ValidateRequestBody: true, ValidateRequestParameters: false, @@ -45,7 +45,7 @@ describe('request validator', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::RequestValidator', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RequestValidator', { RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, Name: 'my-model', ValidateRequestBody: false, diff --git a/packages/@aws-cdk/aws-apigateway/test/resource.test.ts b/packages/@aws-cdk/aws-apigateway/test/resource.test.ts index b118c328073cb..3c97221b56339 100644 --- a/packages/@aws-cdk/aws-apigateway/test/resource.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/resource.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import * as apigw from '../lib'; @@ -16,7 +16,7 @@ describe('resource', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { 'ParentId': { 'Fn::GetAtt': [ 'apiC8550315', @@ -29,7 +29,7 @@ describe('resource', () => { }, }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { 'HttpMethod': 'ANY', 'ResourceId': { 'Ref': 'proxy3A1DA9C7', @@ -60,9 +60,9 @@ describe('resource', () => { proxy.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Resource'); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { 'HttpMethod': 'GET' }); - expect(stack).not.toHaveResource('AWS::ApiGateway::Method', { 'HttpMethod': 'ANY' }); + Template.fromStack(stack).resourceCountIs('AWS::ApiGateway::Resource', 1); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { 'HttpMethod': 'GET' }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', Match.not({ 'HttpMethod': 'ANY' })); }); @@ -78,7 +78,7 @@ describe('resource', () => { const v2 = api.root.addResource('v2'); v2.addProxy(); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'apiC8550315': { 'Type': 'AWS::ApiGateway::RestApi', @@ -154,7 +154,7 @@ describe('resource', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'DELETE', ResourceId: { Ref: 'apiproxy4EA44110' }, Integration: { @@ -164,7 +164,7 @@ describe('resource', () => { OperationName: 'DeleteMe', }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'DELETE', ResourceId: { 'Fn::GetAtt': ['apiC8550315', 'RootResourceId'] }, Integration: { @@ -251,7 +251,7 @@ describe('resource', () => { imported.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET', ResourceId: resourceId, }); diff --git a/packages/@aws-cdk/aws-apigateway/test/restapi.test.ts b/packages/@aws-cdk/aws-apigateway/test/restapi.test.ts index 7a9f117afe126..f873c6c643f5d 100644 --- a/packages/@aws-cdk/aws-apigateway/test/restapi.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/restapi.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart, SynthUtils } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import { GatewayVpcEndpoint } from '@aws-cdk/aws-ec2'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import { App, CfnElement, CfnResource, Stack } from '@aws-cdk/core'; @@ -15,7 +14,7 @@ describe('restapi', () => { api.root.addMethod('GET'); // must have at least one method or an API definition // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { myapi4C7BF186: { Type: 'AWS::ApiGateway::RestApi', @@ -132,7 +131,7 @@ describe('restapi', () => { api.root.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RestApi', { Name: 'restapi', }); }); @@ -168,17 +167,17 @@ describe('restapi', () => { foo.addResource('{hello}'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: 'foo', ParentId: { 'Fn::GetAtt': ['restapiC5611D27', 'RootResourceId'] }, }); - expect(stack).toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: 'bar', ParentId: { 'Fn::GetAtt': ['restapiC5611D27', 'RootResourceId'] }, }); - expect(stack).toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: '{hello}', ParentId: { Ref: 'restapifooF697E056' }, }); @@ -198,7 +197,7 @@ describe('restapi', () => { proxy.addMethod('ANY'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: '{proxy+}', ParentId: { 'Fn::GetAtt': ['restapiC5611D27', 'RootResourceId'] }, }); @@ -216,7 +215,7 @@ describe('restapi', () => { r1.addMethod('POST'); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { restapiC5611D27: { Type: 'AWS::ApiGateway::RestApi', @@ -330,8 +329,8 @@ describe('restapi', () => { api.root.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::IAM::Role'); - expect(stack).toHaveResource('AWS::ApiGateway::Account'); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + Template.fromStack(stack).resourceCountIs('AWS::ApiGateway::Account', 1); }); test('"url" and "urlForPath" return the URL endpoints of the deployed API', () => { @@ -462,7 +461,7 @@ describe('restapi', () => { api.root.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RestApi', { EndpointConfiguration: { Types: [ 'EDGE', @@ -486,7 +485,7 @@ describe('restapi', () => { api.root.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RestApi', { EndpointConfiguration: { Types: ['EDGE', 'PRIVATE'], }, @@ -511,7 +510,7 @@ describe('restapi', () => { api.root.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RestApi', { EndpointConfiguration: { Types: [ 'EDGE', @@ -551,7 +550,7 @@ describe('restapi', () => { api.root.addMethod('GET'); - expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RestApi', { CloneFrom: 'foobar', Name: 'api', }); @@ -568,7 +567,7 @@ describe('restapi', () => { resource.node.addDependency(api); // THEN - expect(stack).toHaveResource('My::Resource', { + Template.fromStack(stack).hasResource('My::Resource', { DependsOn: [ 'myapiAccountC3A4750C', 'myapiCloudWatchRoleEB425128', @@ -577,7 +576,7 @@ describe('restapi', () => { 'myapiDeploymentStageprod329F21FF', 'myapi162F20B8', ], - }, ResourcePart.CompleteDefinition); + }); }); test('defaultIntegration and defaultMethodOptions can be used at any level', () => { @@ -624,7 +623,7 @@ describe('restapi', () => { // THEN // CASE #1 - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET', ResourceId: { 'Fn::GetAtt': ['myapi162F20B8', 'RootResourceId'] }, Integration: { Type: 'AWS' }, @@ -633,7 +632,7 @@ describe('restapi', () => { }); // CASE #2 - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'POST', ResourceId: { Ref: 'myapichildA0A65412' }, Integration: { Type: 'AWS' }, @@ -642,7 +641,7 @@ describe('restapi', () => { }); // CASE #3 - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'DELETE', Integration: { Type: 'MOCK' }, AuthorizerId: 'AUTHID2', @@ -650,7 +649,7 @@ describe('restapi', () => { }); // CASE #4 - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'PUT', Integration: { Type: 'AWS' }, AuthorizerId: 'AUTHID2', @@ -671,7 +670,7 @@ describe('restapi', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::ApiKey', { Enabled: true, Name: 'myApiKey1', StageKeys: [ @@ -701,7 +700,7 @@ describe('restapi', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Model', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Model', { RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as CfnElement) }, Schema: { $schema: 'http://json-schema.org/draft-04/schema#', @@ -731,14 +730,14 @@ describe('restapi', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::RequestValidator', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RequestValidator', { RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as CfnElement) }, Name: 'Parameters', ValidateRequestBody: false, ValidateRequestParameters: true, }); - expect(stack).toHaveResource('AWS::ApiGateway::RequestValidator', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RequestValidator', { RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as CfnElement) }, Name: 'Body', ValidateRequestBody: true, @@ -755,7 +754,8 @@ describe('restapi', () => { api.root.addMethod('GET'); // THEN - expect(SynthUtils.toCloudFormation(stack).Outputs).toEqual({ + const outputs = Template.fromStack(stack).findOutputs('myapiEndpoint8EB17201'); + expect(outputs).toEqual({ myapiEndpoint8EB17201: { Value: { 'Fn::Join': [ @@ -787,7 +787,8 @@ describe('restapi', () => { api.root.addMethod('GET'); // THEN - expect(SynthUtils.toCloudFormation(stack).Outputs).toEqual({ + const outputs = Template.fromStack(stack).findOutputs('myapiEndpoint8EB17201'); + expect(outputs).toEqual({ myapiEndpoint8EB17201: { Value: { 'Fn::Join': [ @@ -864,11 +865,11 @@ describe('restapi', () => { resource.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: 'pets', ParentId: stack.resolve(imported.restApiRootResourceId), }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET', ResourceId: stack.resolve(resource.resourceId), }); @@ -888,11 +889,11 @@ describe('restapi', () => { resource.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: 'pets', ParentId: stack.resolve(api.restApiRootResourceId), }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET', ResourceId: stack.resolve(resource.resourceId), }); @@ -911,7 +912,7 @@ describe('restapi', () => { api.root.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RestApi', { EndpointConfiguration: { Types: [ 'EDGE', @@ -936,7 +937,7 @@ describe('restapi', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::ApiKey', { Enabled: true, Name: 'myApiKey1', StageKeys: [ @@ -1081,7 +1082,7 @@ describe('restapi', () => { api.root.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RestApi', { DisableExecuteApiEndpoint: true, }); }); diff --git a/packages/@aws-cdk/aws-apigateway/test/stage.test.ts b/packages/@aws-cdk/aws-apigateway/test/stage.test.ts index a9b030ebba07d..76d07c6e2b4b3 100644 --- a/packages/@aws-cdk/aws-apigateway/test/stage.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/stage.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as logs from '@aws-cdk/aws-logs'; import * as cdk from '@aws-cdk/core'; import * as apigateway from '../lib'; @@ -16,7 +15,7 @@ describe('stage', () => { new apigateway.Stage(stack, 'my-stage', { deployment }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { testapiD6451F70: { Type: 'AWS::ApiGateway::RestApi', @@ -80,9 +79,9 @@ describe('stage', () => { // WHEN new apigateway.Stage(stack, 'my-stage', { deployment }); - expect(stack).toHaveResourceLike('AWS::ApiGateway::Stage', { + Template.fromStack(stack).hasResource('AWS::ApiGateway::Stage', { DependsOn: ['testapiAccount9B907665'], - }, ResourcePart.CompleteDefinition); + }); }); test('common method settings can be set at the stage level', () => { @@ -100,7 +99,7 @@ describe('stage', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Stage', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Stage', { MethodSettings: [ { DataTraceEnabled: false, @@ -165,7 +164,7 @@ describe('stage', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Stage', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Stage', { MethodSettings: [ { DataTraceEnabled: false, @@ -198,7 +197,7 @@ describe('stage', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Stage', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Stage', { CacheClusterEnabled: true, CacheClusterSize: '0.5', }); @@ -218,7 +217,7 @@ describe('stage', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Stage', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Stage', { CacheClusterEnabled: true, CacheClusterSize: '0.5', }); @@ -253,7 +252,7 @@ describe('stage', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Stage', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Stage', { CacheClusterEnabled: true, CacheClusterSize: '0.5', MethodSettings: [ @@ -298,7 +297,7 @@ describe('stage', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Stage', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Stage', { AccessLogSetting: { DestinationArn: { 'Fn::GetAtt': [ @@ -329,7 +328,7 @@ describe('stage', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Stage', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Stage', { AccessLogSetting: { DestinationArn: { 'Fn::GetAtt': [ diff --git a/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts index 30a1769b8965b..51426a4ffe6c3 100644 --- a/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import { StateMachine } from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; @@ -17,7 +17,7 @@ describe('Step Functions api', () => { }).toThrow(); //THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'ANY', MethodResponses: getMethodResponse(), AuthorizationType: 'NONE', diff --git a/packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts b/packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts index 195e17a0b7fbf..26a63641abc4e 100644 --- a/packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { testFutureBehavior } from '@aws-cdk/cdk-build-tools/lib/feature-flag'; @@ -22,19 +21,19 @@ describe('usage plan', () => { }); // THEN - expect(stack).toHaveResource(RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(RESOURCE_TYPE, { UsagePlanName: usagePlanName, Description: usagePlanDescription, - }, ResourcePart.Properties); + }); }); - test('usage plan with throttling limits', () => { + test('usage plan with integer throttling limits', () => { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'my-api', { cloudWatchRole: false, deploy: true, deployOptions: { stageName: 'test' } }); const method: apigateway.Method = api.root.addMethod('GET'); // Need at least one method on the api const usagePlanName = 'Basic'; - const usagePlanDescription = 'Basic Usage Plan with throttling limits'; + const usagePlanDescription = 'Basic Usage Plan with integer throttling limits'; // WHEN new apigateway.UsagePlan(stack, 'my-usage-plan', { @@ -57,7 +56,7 @@ describe('usage plan', () => { }); // THEN - expect(stack).toHaveResource(RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(RESOURCE_TYPE, { UsagePlanName: usagePlanName, Description: usagePlanDescription, ApiStages: [ @@ -76,7 +75,58 @@ describe('usage plan', () => { }, }, ], - }, ResourcePart.Properties); + }); + }); + + test('usage plan with integer and float throttling limits', () => { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'my-api', { cloudWatchRole: false, deploy: true, deployOptions: { stageName: 'test' } }); + const method: apigateway.Method = api.root.addMethod('GET'); // Need at least one method on the api + const usagePlanName = 'Basic'; + const usagePlanDescription = 'Basic Usage Plan with integer and float throttling limits'; + + // WHEN + new apigateway.UsagePlan(stack, 'my-usage-plan', { + name: usagePlanName, + description: usagePlanDescription, + apiStages: [ + { + stage: api.deploymentStage, + throttle: [ + { + method, + throttle: { + burstLimit: 20, + rateLimit: 10.5, + }, + }, + ], + }, + ], + }); + + // THEN + Template.fromStack(stack).hasResourceProperties(RESOURCE_TYPE, { + UsagePlanName: usagePlanName, + Description: usagePlanDescription, + ApiStages: [ + { + ApiId: { + Ref: 'myapi4C7BF186', + }, + Stage: { + Ref: 'myapiDeploymentStagetest4A4AB65E', + }, + Throttle: { + '//GET': { + BurstLimit: 20, + RateLimit: 10.5, + }, + }, + }, + ], + }); }); test('usage plan with blocked methods', () => { @@ -108,7 +158,7 @@ describe('usage plan', () => { }); // THEN - expect(stack).toHaveResource(RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(RESOURCE_TYPE, { UsagePlanName: usagePlanName, Description: usagePlanDescription, ApiStages: [ @@ -127,7 +177,7 @@ describe('usage plan', () => { }, }, ], - }, ResourcePart.Properties); + }); }); test('usage plan with quota limits', () => { @@ -143,12 +193,12 @@ describe('usage plan', () => { }); // THEN - expect(stack).toHaveResource(RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(RESOURCE_TYPE, { Quota: { Limit: 10000, Period: 'MONTH', }, - }, ResourcePart.Properties); + }); }); describe('UsagePlanKey', () => { @@ -165,7 +215,7 @@ describe('usage plan', () => { usagePlan.addApiKey(apiKey); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::UsagePlanKey', { KeyId: { Ref: 'myapikey1B052F70', }, @@ -173,7 +223,7 @@ describe('usage plan', () => { UsagePlanId: { Ref: 'myusageplan23AA1E32', }, - }, ResourcePart.Properties); + }); }); @@ -187,13 +237,13 @@ describe('usage plan', () => { usagePlan.addApiKey(apiKey); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::UsagePlanKey', { KeyId: { Ref: 'myapikey1B052F70', }, KeyType: 'API_KEY', UsagePlanId: 'imported-id', - }, ResourcePart.Properties); + }); }); test('multiple keys', () => { @@ -212,22 +262,22 @@ describe('usage plan', () => { usagePlan.addApiKey(apiKey2); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::ApiKey', { Name: 'my-api-key-1', - }, ResourcePart.Properties); - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { + }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::ApiKey', { Name: 'my-api-key-2', - }, ResourcePart.Properties); - expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', { + }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::UsagePlanKey', { KeyId: { Ref: 'myapikey11F723FC7', }, - }, ResourcePart.Properties); - expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', { + }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::UsagePlanKey', { KeyId: { Ref: 'myapikey2ABDEF012', }, - }, ResourcePart.Properties); + }); }); test('overrideLogicalId', () => { diff --git a/packages/@aws-cdk/aws-apigateway/test/util.test.ts b/packages/@aws-cdk/aws-apigateway/test/util.test.ts index 07c8a2cef379d..30861de05dad1 100644 --- a/packages/@aws-cdk/aws-apigateway/test/util.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/util.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import { JsonSchema, JsonSchemaType } from '../lib'; import { JsonSchemaMapper, parseAwsApiCall, parseMethodOptionsPath } from '../lib/util'; diff --git a/packages/@aws-cdk/aws-apigateway/test/vpc-link.test.ts b/packages/@aws-cdk/aws-apigateway/test/vpc-link.test.ts index ba2b82464141d..b41e1a4d19471 100644 --- a/packages/@aws-cdk/aws-apigateway/test/vpc-link.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/vpc-link.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as cdk from '@aws-cdk/core'; @@ -20,7 +20,7 @@ describe('vpc link', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::VpcLink', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::VpcLink', { Name: 'MyLink', TargetArns: [{ Ref: 'NLB55158F82' }], }); @@ -43,7 +43,7 @@ describe('vpc link', () => { link.addTargets(nlb3); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::VpcLink', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::VpcLink', { Name: 'VpcLink', TargetArns: [ { Ref: 'NLB03D178991' }, @@ -62,7 +62,7 @@ describe('vpc link', () => { apigateway.VpcLink.fromVpcLinkId(stack, 'ImportedVpcLink', 'vpclink-id'); // THEN - expect(stack).not.toHaveResource('AWS::ApiGateway::VpcLink'); + Template.fromStack(stack).resourceCountIs('AWS::ApiGateway::VpcLink', 0); }); test('validation error if vpc link is created and no targets are added', () => { diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/websocket/lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/websocket/lambda.ts index 2e60cbdd7b547..8b5b5c6d3fc43 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/websocket/lambda.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/websocket/lambda.ts @@ -28,7 +28,11 @@ export interface WebSocketLambdaAuthorizerProps { /** * The identity source for which authorization is requested. * - * @default ['$request.header.Authorization'] + * Request parameter match `'route.request.querystring|header.[a-zA-z0-9._-]+'`. + * Staged variable match `'stageVariables.[a-zA-Z0-9._-]+'`. + * Context parameter match `'context.[a-zA-Z0-9._-]+'`. + * + * @default ['route.request.header.Authorization'] */ readonly identitySource?: string[]; } @@ -56,7 +60,7 @@ export class WebSocketLambdaAuthorizer implements IWebSocketRouteAuthorizer { this.authorizer = new WebSocketAuthorizer(options.scope, this.id, { webSocketApi: options.route.webSocketApi, identitySource: this.props.identitySource ?? [ - '$request.header.Authorization', + 'route.request.header.Authorization', ], type: WebSocketAuthorizerType.LAMBDA, authorizerName: this.props.authorizerName ?? this.id, diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json b/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json index 6eb99a2d3202a..dd8a9418e6e2c 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json @@ -85,8 +85,8 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/aws-lambda": "^8.10.89", - "@types/jest": "^27.0.3" + "@types/aws-lambda": "^8.10.92", + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-apigatewayv2": "0.0.0", diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.iam.expected.json b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.iam.expected.json index ab33666949d9b..be1c18a7e49f7 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.iam.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.iam.expected.json @@ -67,7 +67,7 @@ ] } }, - "UserAccess": { + "UserAccessEC42ADF7": { "Type": "AWS::IAM::AccessKey", "Properties": { "UserName": { @@ -184,13 +184,13 @@ }, "TESTACCESSKEYID": { "Value": { - "Ref": "UserAccess" + "Ref": "UserAccessEC42ADF7" } }, "TESTSECRETACCESSKEY": { "Value": { "Fn::GetAtt": [ - "UserAccess", + "UserAccessEC42ADF7", "SecretAccessKey" ] } diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.iam.ts b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.iam.ts index a010e6c0b990e..6ae3c42bc8421 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.iam.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.iam.ts @@ -17,8 +17,8 @@ class ExampleComIntegration extends apigatewayv2.HttpRouteIntegration { const app = new cdk.App(); const stack = new cdk.Stack(app, 'IntegApiGatewayV2Iam'); const user = new iam.User(stack, 'User'); -const userAccessKey = new iam.CfnAccessKey(stack, 'UserAccess', { - userName: user.userName, +const userAccessKey = new iam.AccessKey(stack, 'UserAccess', { + user, }); const httpApi = new apigatewayv2.HttpApi(stack, 'HttpApi', { @@ -44,11 +44,11 @@ new cdk.CfnOutput(stack, 'API', { }); new cdk.CfnOutput(stack, 'TESTACCESSKEYID', { - value: userAccessKey.ref, + value: userAccessKey.accessKeyId, }); new cdk.CfnOutput(stack, 'TESTSECRETACCESSKEY', { - value: userAccessKey.attrSecretAccessKey, + value: userAccessKey.secretAccessKey.toString(), }); new cdk.CfnOutput(stack, 'TESTREGION', { diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/websocket/lambda.test.ts b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/websocket/lambda.test.ts index c171247801911..8a62d5731ac58 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/websocket/lambda.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/websocket/lambda.test.ts @@ -35,7 +35,7 @@ describe('WebSocketLambdaAuthorizer', () => { Name: 'default-authorizer', AuthorizerType: 'REQUEST', IdentitySource: [ - '$request.header.Authorization', + 'route.request.header.Authorization', ], }); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts index 6bdce918195cf..2417fffe1610d 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts @@ -63,10 +63,10 @@ export class HttpLambdaIntegration extends HttpRouteIntegration { }); return { - type: HttpIntegrationType.LAMBDA_PROXY, + type: HttpIntegrationType.AWS_PROXY, uri: this.handler.functionArn, payloadFormatVersion: this.props.payloadFormatVersion ?? PayloadFormatVersion.VERSION_2_0, parameterMapping: this.props.parameterMapping, }; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/index.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/index.ts index 04a64da0c7540..9c6035e3957d4 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/index.ts @@ -1 +1,2 @@ export * from './lambda'; +export * from './mock'; diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/mock.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/mock.ts new file mode 100644 index 0000000000000..9c7a83ece4538 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/mock.ts @@ -0,0 +1,27 @@ +import { + WebSocketRouteIntegration, + WebSocketIntegrationType, + WebSocketRouteIntegrationConfig, + WebSocketRouteIntegrationBindOptions, +} from '@aws-cdk/aws-apigatewayv2'; + +/** + * Mock WebSocket Integration + */ +export class WebSocketMockIntegration extends WebSocketRouteIntegration { + + /** + * @param id id of the underlying integration construct + */ + constructor(id: string) { + super(id); + } + + bind(options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig { + options; + return { + type: WebSocketIntegrationType.MOCK, + uri: '', + }; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json index 87b5a43c1f9cb..749d5fb08b479 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json @@ -81,7 +81,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-apigatewayv2": "0.0.0", diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.mock.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.mock.expected.json new file mode 100644 index 0000000000000..dede3af2298b4 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.mock.expected.json @@ -0,0 +1,108 @@ +{ + "Resources": { + "mywsapi32E6CE11": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "mywsapi", + "ProtocolType": "WEBSOCKET", + "RouteSelectionExpression": "$request.body.action" + } + }, + "mywsapidefaultRouteDefaultIntegrationFFCB3BA9": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationType": "MOCK", + "IntegrationUri": "" + } + }, + "mywsapidefaultRouteE9382DF8": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "RouteKey": "$default", + "AuthorizationType": "NONE", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mywsapidefaultRouteDefaultIntegrationFFCB3BA9" + } + ] + ] + } + } + }, + "mywsapisendmessageRouteSendMessageIntegrationD29E12F9": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationType": "MOCK", + "IntegrationUri": "" + } + }, + "mywsapisendmessageRouteAE873328": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "RouteKey": "sendmessage", + "AuthorizationType": "NONE", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mywsapisendmessageRouteSendMessageIntegrationD29E12F9" + } + ] + ] + } + } + }, + "mystage114C35EC": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "StageName": "dev", + "AutoDeploy": true + } + } + }, + "Outputs": { + "ApiEndpoint": { + "Value": { + "Fn::Join": [ + "", + [ + "wss://", + { + "Ref": "mywsapi32E6CE11" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/dev" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.mock.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.mock.ts new file mode 100644 index 0000000000000..672378b42d375 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.mock.ts @@ -0,0 +1,24 @@ +import { WebSocketApi, WebSocketStage } from '@aws-cdk/aws-apigatewayv2'; +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { WebSocketMockIntegration } from '../../lib'; + +/* + * Stack verification steps: + * 1. Verify manually that the integration has type "MOCK" + */ + +const app = new App(); +const stack = new Stack(app, 'integ-mock-websocket-integration'); + +const webSocketApi = new WebSocketApi(stack, 'mywsapi', { + defaultRouteOptions: { integration: new WebSocketMockIntegration('DefaultIntegration') }, +}); +const stage = new WebSocketStage(stack, 'mystage', { + webSocketApi, + stageName: 'dev', + autoDeploy: true, +}); + +webSocketApi.addRoute('sendmessage', { integration: new WebSocketMockIntegration('SendMessageIntegration') }); + +new CfnOutput(stack, 'ApiEndpoint', { value: stage.url }); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/mock.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/mock.test.ts new file mode 100644 index 0000000000000..4bd7eccd9fc7b --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/mock.test.ts @@ -0,0 +1,23 @@ +import { Template } from '@aws-cdk/assertions'; +import { WebSocketApi } from '@aws-cdk/aws-apigatewayv2'; +import { Stack } from '@aws-cdk/core'; +import { WebSocketMockIntegration } from '../../lib'; + + +describe('MockWebSocketIntegration', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new WebSocketApi(stack, 'Api', { + defaultRouteOptions: { integration: new WebSocketMockIntegration('DefaultIntegration') }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'MOCK', + IntegrationUri: '', + }); + }); +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index a09d015dc87a2..040ee20d0bf6a 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -79,7 +79,7 @@ configures all other HTTP method calls to `/books` to a lambda proxy. ```ts import { HttpUrlIntegration, HttpLambdaIntegration } from '@aws-cdk/aws-apigatewayv2-integrations'; -const getBooksIntegration = new HttpUrlIntegration('GetBooksIntegration' 'https://get-books-proxy.myproxy.internal'); +const getBooksIntegration = new HttpUrlIntegration('GetBooksIntegration', 'https://get-books-proxy.myproxy.internal'); declare const booksDefaultFn: lambda.Function; const booksDefaultIntegration = new HttpLambdaIntegration('BooksIntegration', booksDefaultFn); @@ -261,19 +261,21 @@ Mutual TLS can be configured to limit access to your API based by using client c ```ts import * as s3 from '@aws-cdk/aws-s3'; +import * as acm from '@aws-cdk/aws-certificatemanager'; + const certArn = 'arn:aws:acm:us-east-1:111111111111:certificate'; const domainName = 'example.com'; -const bucket = new s3.Bucket.fromBucketName(stack, 'TrustStoreBucket', ...); +declare const bucket: s3.Bucket; -new DomainName(stack, 'DomainName', { +new apigwv2.DomainName(this, 'DomainName', { domainName, - certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), + certificate: acm.Certificate.fromCertificateArn(this, 'cert', certArn), mtls: { bucket, key: 'someca.pem', version: 'version', }, -}) +}); ``` Instructions for configuring your trust store can be found [here](https://aws.amazon.com/blogs/compute/introducing-mutual-tls-authentication-for-amazon-api-gateway/) @@ -402,23 +404,29 @@ webSocketApi.addRoute('sendmessage', { }); ``` +To import an existing WebSocketApi: + +```ts +const webSocketApi = apigwv2.WebSocketApi.fromWebSocketApiAttributes(this, 'mywsapi', { webSocketId: 'api-1234' }); +``` + ### Manage Connections Permission Grant permission to use API Gateway Management API of a WebSocket API by calling the `grantManageConnections` API. You can use Management API to send a callback message to a connected client, get connection information, or disconnect the client. Learn more at [Use @connections commands in your backend service](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-how-to-call-websocket-api-connections.html). ```ts -const lambda = new lambda.Function(this, 'lambda', { /* ... */ }); +declare const fn: lambda.Function; -const webSocketApi = new WebSocketApi(stack, 'mywsapi'); -const stage = new WebSocketStage(stack, 'mystage', { +const webSocketApi = new apigwv2.WebSocketApi(this, 'mywsapi'); +const stage = new apigwv2.WebSocketStage(this, 'mystage', { webSocketApi, stageName: 'dev', }); // per stage permission -stage.grantManageConnections(lambda); +stage.grantManagementApiAccess(fn); // for all the stages permission -webSocketApi.grantManageConnections(lambda); +webSocketApi.grantManageConnections(fn); ``` ### Managing access to WebSocket APIs @@ -426,3 +434,16 @@ webSocketApi.grantManageConnections(lambda); API Gateway supports multiple mechanisms for [controlling and managing access to a WebSocket API](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-control-access.html) through authorizers. These authorizers can be found in the [APIGatewayV2-Authorizers](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-apigatewayv2-authorizers-readme.html) constructs library. + +### API Keys + +Websocket APIs also support usage of API Keys. An API Key is a key that is used to grant access to an API. These are useful for controlling and tracking access to an API, when used together with [usage plans](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-usage-plans.html). These together allow you to configure controls around API access such as quotas and throttling, along with per-API Key metrics on usage. + +To require an API Key when accessing the Websocket API: + +```ts +const webSocketApi = new apigwv2.WebSocketApi(this, 'mywsapi',{ + apiKeySelectionExpression: apigwv2.WebSocketApiKeySelectionExpression.HEADER_X_API_KEY, +}); +``` + diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts index 857ef57657736..58e9c9a60879a 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts @@ -1,3 +1,4 @@ +import { IRole } from '@aws-cdk/aws-iam'; import { Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnIntegration } from '../apigatewayv2.generated'; @@ -23,15 +24,96 @@ export interface IHttpIntegration extends IIntegration { */ export enum HttpIntegrationType { /** - * Integration type is a Lambda proxy - * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html + * Integration type is an HTTP proxy. + * + * For integrating the route or method request with an HTTP endpoint, with the + * client request passed through as-is. This is also referred to as HTTP proxy + * integration. For HTTP API private integrations, use an HTTP_PROXY integration. + * + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-http.html */ - LAMBDA_PROXY = 'AWS_PROXY', + HTTP_PROXY = 'HTTP_PROXY', + /** - * Integration type is an HTTP proxy + * Integration type is an AWS proxy. + * + * For integrating the route or method request with a Lambda function or other + * AWS service action. This integration is also referred to as a Lambda proxy + * integration. + * + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-aws-services.html * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html */ - HTTP_PROXY = 'HTTP_PROXY', + AWS_PROXY = 'AWS_PROXY', +} + +/** + * Supported integration subtypes + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-aws-services-reference.html + */ +export enum HttpIntegrationSubtype { + /** + * EventBridge PutEvents integration + */ + EVENTBRIDGE_PUT_EVENTS = 'EventBridge-PutEvents', + /** + * SQS SendMessage integration + */ + SQS_SEND_MESSAGE = 'SQS-SendMessage', + /** + * SQS ReceiveMessage integration, + */ + SQS_RECEIVE_MESSAGE = 'SQS-ReceiveMessage', + /** + * SQS DeleteMessage integration, + */ + SQS_DELETE_MESSAGE = 'SQS-DeleteMessage', + /** + * SQS PurgeQueue integration + */ + SQS_PURGE_QUEUE = 'SQS-PurgeQueue', + /** + * AppConfig GetConfiguration integration + */ + APPCONFIG_GET_CONFIGURATION = 'AppConfig-GetConfiguration', + /** + * Kinesis PutRecord integration + */ + KINESIS_PUT_RECORD = 'Kinesis-PutRecord', + /** + * Step Functions StartExecution integration + */ + STEPFUNCTIONS_START_EXECUTION = 'StepFunctions-StartExecution', + /** + * Step Functions StartSyncExecution integration + */ + STEPFUNCTIONS_START_SYNC_EXECUTION = 'StepFunctions-StartSyncExecution', + /** + * Step Functions StopExecution integration + */ + STEPFUNCTIONS_STOP_EXECUTION = 'StepFunctions-StopExecution', +} + +/** + * Credentials used for AWS Service integrations. + */ +export abstract class IntegrationCredentials { + /** + * Use the specified role for integration requests + */ + public static fromRole(role: IRole): IntegrationCredentials { + return { credentialsArn: role.roleArn }; + } + + /** Use the calling user's identity to call the integration */ + public static useCallerIdentity(): IntegrationCredentials { + return { credentialsArn: 'arn:aws:iam::*:user/*' }; + } + + /** + * The ARN of the credentials + */ + public abstract readonly credentialsArn: string; } /** @@ -88,12 +170,23 @@ export interface HttpIntegrationProps { */ readonly integrationType: HttpIntegrationType; + /** + * Integration subtype. + * + * Used for AWS Service integrations, specifies the target of the integration. + * + * @default - none, required if no `integrationUri` is defined. + */ + readonly integrationSubtype?: HttpIntegrationSubtype; + /** * Integration URI. - * This will be the function ARN in the case of `HttpIntegrationType.LAMBDA_PROXY`, + * This will be the function ARN in the case of `HttpIntegrationType.AWS_PROXY`, * or HTTP URL in the case of `HttpIntegrationType.HTTP_PROXY`. + * + * @default - none, required if no `integrationSubtype` is defined. */ - readonly integrationUri: string; + readonly integrationUri?: string; /** * The HTTP method to use when calling the underlying HTTP proxy @@ -118,7 +211,7 @@ export interface HttpIntegrationProps { /** * The version of the payload format * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html - * @default - defaults to latest in the case of HttpIntegrationType.LAMBDA_PROXY`, irrelevant otherwise. + * @default - defaults to latest in the case of HttpIntegrationType.AWS_PROXY`, irrelevant otherwise. */ readonly payloadFormatVersion?: PayloadFormatVersion; @@ -135,6 +228,13 @@ export interface HttpIntegrationProps { * @default undefined requests are sent to the backend unmodified */ readonly parameterMapping?: ParameterMapping; + + /** + * The credentials with which to invoke the integration. + * + * @default - no credentials, use resource-based permissions on supported AWS services + */ + readonly credentials?: IntegrationCredentials; } /** @@ -148,15 +248,22 @@ export class HttpIntegration extends Resource implements IHttpIntegration { constructor(scope: Construct, id: string, props: HttpIntegrationProps) { super(scope, id); + + if (!props.integrationSubtype && !props.integrationUri) { + throw new Error('Either `integrationSubtype` or `integrationUri` must be specified.'); + } + const integ = new CfnIntegration(this, 'Resource', { apiId: props.httpApi.apiId, integrationType: props.integrationType, + integrationSubtype: props.integrationSubtype, integrationUri: props.integrationUri, integrationMethod: props.method, connectionId: props.connectionId, connectionType: props.connectionType, payloadFormatVersion: props.payloadFormatVersion?.version, requestParameters: props.parameterMapping?.mappings, + credentialsArn: props.credentials?.credentialsArn, }); if (props.secureServerName) { @@ -214,6 +321,7 @@ export abstract class HttpRouteIntegration { this.integration = new HttpIntegration(options.scope, this.id, { httpApi: options.route.httpApi, integrationType: config.type, + integrationSubtype: config.subtype, integrationUri: config.uri, method: config.method, connectionId: config.connectionId, @@ -221,6 +329,7 @@ export abstract class HttpRouteIntegration { payloadFormatVersion: config.payloadFormatVersion, secureServerName: config.secureServerName, parameterMapping: config.parameterMapping, + credentials: config.credentials, }); } return { integrationId: this.integration.integrationId }; @@ -241,10 +350,19 @@ export interface HttpRouteIntegrationConfig { */ readonly type: HttpIntegrationType; + /** + * Integration subtype. + * + * @default - none, required if no `integrationUri` is defined. + */ + readonly subtype?: HttpIntegrationSubtype; + /** * Integration URI + * + * @default - none, required if no `integrationSubtype` is defined. */ - readonly uri: string; + readonly uri?: string; /** * The HTTP method that must be used to invoke the underlying proxy. @@ -287,4 +405,11 @@ export interface HttpRouteIntegrationConfig { * @default undefined requests are sent to the backend unmodified */ readonly parameterMapping?: ParameterMapping; + + /** + * The credentials with which to invoke the integration. + * + * @default - no credentials, use resource-based permissions on supported AWS services + */ + readonly credentials?: IntegrationCredentials; } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/parameter-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/parameter-mapping.ts index deb967d572de2..5b92a50d9ca61 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/parameter-mapping.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/parameter-mapping.ts @@ -71,6 +71,18 @@ export class MappingValue implements IMappingValue { * Represents a Parameter Mapping. */ export class ParameterMapping { + + /** + * Creates a mapping from an object. + */ + public static fromObject(obj: { [key: string]: MappingValue }): ParameterMapping { + const mapping = new ParameterMapping(); + for (const [k, m] of Object.entries(obj)) { + mapping.custom(k, m.value); + } + return mapping; + } + /** * Represents all created parameter mappings. */ @@ -142,4 +154,4 @@ export class ParameterMapping { this.mappings[key] = value; return this; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts index d78d16842e295..f81eb7b899f1f 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts @@ -12,6 +12,29 @@ import { WebSocketRoute, WebSocketRouteOptions } from './route'; export interface IWebSocketApi extends IApi { } +/** + * Represents the currently available API Key Selection Expressions + */ +export class WebSocketApiKeySelectionExpression { + + /** + * The API will extract the key value from the `x-api-key` header in the user request. + */ + public static readonly HEADER_X_API_KEY = new WebSocketApiKeySelectionExpression('$request.header.x-api-key'); + + /** + * The API will extract the key value from the `usageIdentifierKey` attribute in the `context` map, + * returned by the Lambda Authorizer. + * See https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html + */ + public static readonly AUTHORIZER_USAGE_IDENTIFIER_KEY = new WebSocketApiKeySelectionExpression('$context.authorizer.usageIdentifierKey'); + + /** + * @param customApiKeySelector The expression used by API Gateway + */ + public constructor(public readonly customApiKeySelector: string) {} +} + /** * Props for WebSocket API */ @@ -22,6 +45,12 @@ export interface WebSocketApiProps { */ readonly apiName?: string; + /** + * An API key selection expression. Providing this option will require an API Key be provided to access the API. + * @default - Key is not required to access these APIs + */ + readonly apiKeySelectionExpression?: WebSocketApiKeySelectionExpression + /** * The description of the API. * @default - none @@ -56,11 +85,47 @@ export interface WebSocketApiProps { readonly defaultRouteOptions?: WebSocketRouteOptions; } +/** + * Attributes for importing a WebSocketApi into the CDK + */ +export interface WebSocketApiAttributes { + /** + * The identifier of the WebSocketApi + */ + readonly webSocketId: string; + + /** + * The endpoint URL of the WebSocketApi + * @default - throw san error if apiEndpoint is accessed. + */ + readonly apiEndpoint?: string; +} + + /** * Create a new API Gateway WebSocket API endpoint. * @resource AWS::ApiGatewayV2::Api */ export class WebSocketApi extends ApiBase implements IWebSocketApi { + /** + * Import an existing WebSocket API into this CDK app. + */ + public static fromWebSocketApiAttributes(scope: Construct, id: string, attrs: WebSocketApiAttributes): IWebSocketApi { + class Import extends ApiBase { + public readonly apiId = attrs.webSocketId; + public readonly websocketApiId = attrs.webSocketId; + private readonly _apiEndpoint = attrs.apiEndpoint; + + public get apiEndpoint(): string { + if (!this._apiEndpoint) { + throw new Error('apiEndpoint is not configured on the imported WebSocketApi.'); + } + return this._apiEndpoint; + } + } + return new Import(scope, id); + } + public readonly apiId: string; public readonly apiEndpoint: string; @@ -76,6 +141,7 @@ export class WebSocketApi extends ApiBase implements IWebSocketApi { const resource = new CfnApi(this, 'Resource', { name: this.webSocketApiName, + apiKeySelectionExpression: props?.apiKeySelectionExpression?.customApiKeySelector, protocolType: 'WEBSOCKET', description: props?.description, routeSelectionExpression: props?.routeSelectionExpression ?? '$request.body.action', @@ -120,7 +186,7 @@ export class WebSocketApi extends ApiBase implements IWebSocketApi { return Grant.addToPrincipal({ grantee: identity, actions: ['execute-api:ManageConnections'], - resourceArns: [`${arn}/*/POST/@connections/*`], + resourceArns: [`${arn}/*/*/@connections/*`], }); } } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts index b5366be83f2ba..028dfd07b7a97 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts @@ -24,7 +24,11 @@ export enum WebSocketIntegrationType { /** * AWS Proxy Integration Type */ - AWS_PROXY = 'AWS_PROXY' + AWS_PROXY = 'AWS_PROXY', + /** + * Mock Integration Type + */ + MOCK = 'MOCK' } /** diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts index 0aaa93587015c..923311a2b524e 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts @@ -52,6 +52,12 @@ export interface WebSocketRouteProps extends WebSocketRouteOptions { * The key to this route. */ readonly routeKey: string; + + /** + * Whether the route requires an API Key to be provided + * @default false + */ + readonly apiKeyRequired?: boolean; } /** @@ -91,6 +97,7 @@ export class WebSocketRoute extends Resource implements IWebSocketRoute { const route = new CfnRoute(this, 'Resource', { apiId: props.webSocketApi.apiId, + apiKeyRequired: props.apiKeyRequired, routeKey: props.routeKey, target: `integrations/${config.integrationId}`, authorizerId: authBindResult.authorizerId, diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts index 6d5cc8527fef0..685850a746f4e 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts @@ -131,7 +131,7 @@ export class WebSocketStage extends StageBase implements IWebSocketStage { return Grant.addToPrincipal({ grantee: identity, actions: ['execute-api:ManageConnections'], - resourceArns: [`${arn}/${this.stageName}/POST/@connections/*`], + resourceArns: [`${arn}/${this.stageName}/*/@connections/*`], }); } } diff --git a/packages/@aws-cdk/aws-apigatewayv2/package.json b/packages/@aws-cdk/aws-apigatewayv2/package.json index 6e12d9613fa27..088009ce5a483 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -32,7 +32,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -82,7 +89,7 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-certificatemanager": "0.0.0", diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts index 0f0d4d01fd1c5..7f64176446928 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts @@ -1,5 +1,5 @@ import { Template } from '@aws-cdk/assertions'; -import { AccountPrincipal, Role } from '@aws-cdk/aws-iam'; +import { AccountPrincipal, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import { Stack, App } from '@aws-cdk/core'; import { HttpApi, HttpAuthorizer, HttpAuthorizerType, HttpConnectionType, HttpIntegrationType, HttpMethod, HttpRoute, @@ -7,6 +7,8 @@ import { MappingValue, ParameterMapping, PayloadFormatVersion, + HttpIntegrationSubtype, + IntegrationCredentials, } from '../../lib'; describe('HttpRoute', () => { @@ -249,6 +251,56 @@ describe('HttpRoute', () => { Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::VpcLink', 0); }); + test('configures AWS service integration correctly', () => { + // GIVEN + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'HttpApi'); + const role = new Role(stack, 'Role', { + assumedBy: new ServicePrincipal('apigateway.amazonaws.com'), + }); + + class SqsSendMessageIntegration extends HttpRouteIntegration { + public bind(): HttpRouteIntegrationConfig { + return { + method: HttpMethod.ANY, + payloadFormatVersion: PayloadFormatVersion.VERSION_1_0, + type: HttpIntegrationType.AWS_PROXY, + subtype: HttpIntegrationSubtype.SQS_SEND_MESSAGE, + credentials: IntegrationCredentials.fromRole(role), + parameterMapping: ParameterMapping.fromObject({ + QueueUrl: MappingValue.requestHeader('queueUrl'), + MessageBody: MappingValue.requestBody('message'), + }), + }; + } + } + + // WHEN + new HttpRoute(stack, 'HttpRoute', { + httpApi, + integration: new SqsSendMessageIntegration('SqsSendMessageIntegration'), + routeKey: HttpRouteKey.with('/sqs', HttpMethod.POST), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationSubtype: 'SQS-SendMessage', + IntegrationMethod: 'ANY', + PayloadFormatVersion: '1.0', + CredentialsArn: { + 'Fn::GetAtt': [ + 'Role1ABCC5F0', + 'Arn', + ], + }, + RequestParameters: { + QueueUrl: '$request.header.queueUrl', + MessageBody: '$request.body.message', + }, + }); + }); + test('can create route with an authorizer attached', () => { const stack = new Stack(); const httpApi = new HttpApi(stack, 'HttpApi'); @@ -632,4 +684,4 @@ class SomeAuthorizerType implements IHttpRouteAuthorizer { authorizationType: this.authorizationType, }; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts index 50e973d445731..f9fb740c0e190 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts @@ -2,8 +2,12 @@ import { Match, Template } from '@aws-cdk/assertions'; import { User } from '@aws-cdk/aws-iam'; import { Stack } from '@aws-cdk/core'; import { - WebSocketRouteIntegration, WebSocketApi, WebSocketIntegrationType, - WebSocketRouteIntegrationBindOptions, WebSocketRouteIntegrationConfig, + WebSocketRouteIntegration, + WebSocketApi, + WebSocketApiKeySelectionExpression, + WebSocketIntegrationType, + WebSocketRouteIntegrationBindOptions, + WebSocketRouteIntegrationConfig, } from '../../lib'; describe('WebSocketApi', () => { @@ -25,6 +29,27 @@ describe('WebSocketApi', () => { Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::Integration', 0); }); + test('apiKeySelectionExpression: given a value', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new WebSocketApi(stack, 'api', { + apiKeySelectionExpression: WebSocketApiKeySelectionExpression.AUTHORIZER_USAGE_IDENTIFIER_KEY, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Api', { + ApiKeySelectionExpression: '$context.authorizer.usageIdentifierKey', + Name: 'api', + ProtocolType: 'WEBSOCKET', + }); + + Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::Stage', 0); + Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::Route', 0); + Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::Integration', 0); + }); + test('addRoute: adds a route with passed key', () => { // GIVEN const stack = new Stack(); @@ -82,6 +107,25 @@ describe('WebSocketApi', () => { }); }); + test('import', () => { + // GIVEN + const stack = new Stack(); + const imported = WebSocketApi.fromWebSocketApiAttributes(stack, 'imported', { webSocketId: 'ws-1234', apiEndpoint: 'api-endpoint' }); + + // THEN + expect(imported.apiId).toEqual('ws-1234'); + expect(imported.apiEndpoint).toEqual('api-endpoint'); + }); + + test('apiEndpoint for imported', () => { + // GIVEN + const stack = new Stack(); + const api = WebSocketApi.fromWebSocketApiAttributes(stack, 'imported', { webSocketId: 'api-1234' }); + + // THEN + expect(() => api.apiEndpoint).toThrow(/apiEndpoint is not configured/); + }); + describe('grantManageConnections', () => { test('adds an IAM policy to the principal', () => { // GIVEN @@ -116,7 +160,7 @@ describe('WebSocketApi', () => { { Ref: 'apiC8550315', }, - '/*/POST/@connections/*', + '/*/*/@connections/*', ]], }, }]), diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-apikey.expected.json b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-apikey.expected.json new file mode 100644 index 0000000000000..bc0b6f740acc8 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-apikey.expected.json @@ -0,0 +1,13 @@ +{ + "Resources": { + "MyWebsocketApiEBAC53DF": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "ApiKeySelectionExpression": "$request.header.x-api-key", + "Name": "MyWebsocketApi", + "ProtocolType": "WEBSOCKET", + "RouteSelectionExpression": "$request.body.action" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-apikey.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-apikey.ts new file mode 100644 index 0000000000000..1c5482bd3848e --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-apikey.ts @@ -0,0 +1,14 @@ +#!/usr/bin/env node +import * as cdk from '@aws-cdk/core'; +import * as apigw from '../../lib'; +import { WebSocketApiKeySelectionExpression } from '../../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-aws-apigatewayv2-websockets'); + +new apigw.WebSocketApi(stack, 'MyWebsocketApi', { + apiKeySelectionExpression: WebSocketApiKeySelectionExpression.HEADER_X_API_KEY, +}); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts index 655390f31165f..94c4e969a08b6 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts @@ -42,6 +42,45 @@ describe('WebSocketRoute', () => { }); }); + test('Api Key is required for route when apiKeyIsRequired is true', () => { + // GIVEN + const stack = new Stack(); + const webSocketApi = new WebSocketApi(stack, 'Api'); + + // WHEN + new WebSocketRoute(stack, 'Route', { + webSocketApi, + integration: new DummyIntegration(), + routeKey: 'message', + apiKeyRequired: true, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(webSocketApi.apiId), + ApiKeyRequired: true, + RouteKey: 'message', + Target: { + 'Fn::Join': [ + '', + [ + 'integrations/', + { + Ref: 'RouteDummyIntegrationE40E82B4', + }, + ], + ], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + ApiId: stack.resolve(webSocketApi.apiId), + IntegrationType: 'AWS_PROXY', + IntegrationUri: 'some-uri', + }); + }); + + test('integration cannot be used across WebSocketApis', () => { // GIVEN const integration = new DummyIntegration(); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts index b873f7fa74efa..b1af6af2e59bc 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts @@ -99,7 +99,7 @@ describe('WebSocketStage', () => { { Ref: 'ApiF70053CD', }, - `/${defaultStage.stageName}/POST/@connections/*`, + `/${defaultStage.stageName}/*/@connections/*`, ]], }, }]), diff --git a/packages/@aws-cdk/aws-appconfig/package.json b/packages/@aws-cdk/aws-appconfig/package.json index 453ee2d7ee87b..59a35b9f2f241 100644 --- a/packages/@aws-cdk/aws-appconfig/package.json +++ b/packages/@aws-cdk/aws-appconfig/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-appflow/package.json b/packages/@aws-cdk/aws-appflow/package.json index d662bbcb347cb..e401c2c7c98de 100644 --- a/packages/@aws-cdk/aws-appflow/package.json +++ b/packages/@aws-cdk/aws-appflow/package.json @@ -28,6 +28,13 @@ "Framework :: AWS CDK :: 1" ] } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -75,7 +82,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-appintegrations/package.json b/packages/@aws-cdk/aws-appintegrations/package.json index 6a3ec67dcdea2..d0615ae38682c 100644 --- a/packages/@aws-cdk/aws-appintegrations/package.json +++ b/packages/@aws-cdk/aws-appintegrations/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-appintegrations", "module": "aws_cdk.aws_appintegrations" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/target-tracking-scaling-policy.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/target-tracking-scaling-policy.ts index c974a13d4ec8a..e0252b377ddc9 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/target-tracking-scaling-policy.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/target-tracking-scaling-policy.ts @@ -136,6 +136,11 @@ export class TargetTrackingScalingPolicy extends CoreConstruct { super(scope, id); + // replace dummy value in DYNAMODB_WRITE_CAPACITY_UTILIZATION due to a jsii bug (https://github.com/aws/jsii/issues/2782) + const predefinedMetric = props.predefinedMetric === PredefinedMetric.DYNAMODB_WRITE_CAPACITY_UTILIZATION ? + PredefinedMetric.DYANMODB_WRITE_CAPACITY_UTILIZATION : + props.predefinedMetric; + const resource = new CfnScalingPolicy(this, 'Resource', { policyName: props.policyName || cdk.Names.uniqueId(this), policyType: 'TargetTrackingScaling', @@ -143,8 +148,8 @@ export class TargetTrackingScalingPolicy extends CoreConstruct { targetTrackingScalingPolicyConfiguration: { customizedMetricSpecification: renderCustomMetric(props.customMetric), disableScaleIn: props.disableScaleIn, - predefinedMetricSpecification: props.predefinedMetric !== undefined ? { - predefinedMetricType: props.predefinedMetric, + predefinedMetricSpecification: predefinedMetric !== undefined ? { + predefinedMetricType: predefinedMetric, resourceLabel: props.resourceLabel, } : undefined, scaleInCooldown: props.scaleInCooldown && props.scaleInCooldown.toSeconds(), @@ -183,9 +188,20 @@ export enum PredefinedMetric { * @see https://docs.aws.amazon.com/autoscaling/application/APIReference/API_PredefinedMetricSpecification.html */ DYNAMODB_READ_CAPACITY_UTILIZATION = 'DynamoDBReadCapacityUtilization', + /** + * DYNAMODB_WRITE_CAPACITY_UTILIZATION + * + * Suffix `dummy` is necessary due to jsii bug (https://github.com/aws/jsii/issues/2782). + * Duplicate values will be dropped, so this suffix is added as a workaround. + * The value will be replaced when this enum is used. + * + * @see https://docs.aws.amazon.com/autoscaling/application/APIReference/API_PredefinedMetricSpecification.html + */ + DYNAMODB_WRITE_CAPACITY_UTILIZATION = 'DynamoDBWriteCapacityUtilization-dummy', /** * DYANMODB_WRITE_CAPACITY_UTILIZATION * @see https://docs.aws.amazon.com/autoscaling/application/APIReference/API_PredefinedMetricSpecification.html + * @deprecated use `PredefinedMetric.DYNAMODB_WRITE_CAPACITY_UTILIZATION` */ DYANMODB_WRITE_CAPACITY_UTILIZATION = 'DynamoDBWriteCapacityUtilization', /** diff --git a/packages/@aws-cdk/aws-applicationautoscaling/package.json b/packages/@aws-cdk/aws-applicationautoscaling/package.json index 292f2d3b0684d..95e7c514ff6f7 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/package.json +++ b/packages/@aws-cdk/aws-applicationautoscaling/package.json @@ -79,13 +79,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "fast-check": "^2.20.0", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "fast-check": "^2.22.0", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-autoscaling-common": "0.0.0", @@ -126,6 +126,7 @@ "docs-public-apis:@aws-cdk/aws-applicationautoscaling.StepScalingPolicyProps", "docs-public-apis:@aws-cdk/aws-applicationautoscaling.PredefinedMetric.ECS_SERVICE_AVERAGE_CPU_UTILIZATION", "docs-public-apis:@aws-cdk/aws-applicationautoscaling.PredefinedMetric.DYNAMODB_READ_CAPACITY_UTILIZATION", + "docs-public-apis:@aws-cdk/aws-applicationautoscaling.PredefinedMetric.DYNAMODB_WRITE_CAPACITY_UTILIZATION", "docs-public-apis:@aws-cdk/aws-applicationautoscaling.PredefinedMetric.DYANMODB_WRITE_CAPACITY_UTILIZATION", "docs-public-apis:@aws-cdk/aws-applicationautoscaling.PredefinedMetric.ALB_REQUEST_COUNT_PER_TARGET", "docs-public-apis:@aws-cdk/aws-applicationautoscaling.PredefinedMetric.RDS_READER_AVERAGE_CPU_UTILIZATION", diff --git a/packages/@aws-cdk/aws-applicationautoscaling/test/scalable-target.test.ts b/packages/@aws-cdk/aws-applicationautoscaling/test/scalable-target.test.ts index 036fd608a5e6d..bb66486113531 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/test/scalable-target.test.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/test/scalable-target.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; @@ -20,7 +20,7 @@ describe('scalable target', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { ServiceNamespace: 'dynamodb', ScalableDimension: 'test:TestCount', ResourceId: 'test:this/test', @@ -43,7 +43,7 @@ describe('scalable target', () => { }); // THEN: no exception - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { ServiceNamespace: 'dynamodb', ScalableDimension: 'test:TestCount', ResourceId: 'test:this/test', @@ -65,7 +65,7 @@ describe('scalable target', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { ScheduledActions: [ { ScalableTargetAction: { @@ -104,11 +104,11 @@ describe('scalable target', () => { }); // THEN - expect(stack).not.toHaveResource('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', Match.not({ Period: 60, - }); + })); - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ComparisonOperator: 'LessThanOrEqualToThreshold', EvaluationPeriods: 1, Metrics: [ @@ -203,7 +203,7 @@ describe('scalable target', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { ServiceNamespace: 'dynamodb', ScalableDimension: 'test:TestCount', ResourceId: 'test:this/test', diff --git a/packages/@aws-cdk/aws-applicationautoscaling/test/step-scaling-policy.test.ts b/packages/@aws-cdk/aws-applicationautoscaling/test/step-scaling-policy.test.ts index ca1011881ec14..4e10b53ae9ce9 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/test/step-scaling-policy.test.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/test/step-scaling-policy.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { SynthUtils } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as cdk from '@aws-cdk/core'; import * as fc from 'fast-check'; @@ -132,7 +131,7 @@ describe('step scaling policy', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'StepScaling', ScalingTargetId: { Ref: 'Target3191CF44', @@ -169,14 +168,14 @@ describe('step scaling policy', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'StepScaling', StepScalingPolicyConfiguration: { AdjustmentType: 'ChangeInCapacity', MetricAggregationType: 'Average', }, }); - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 1, AlarmActions: [ @@ -209,14 +208,14 @@ describe('step scaling policy', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'StepScaling', StepScalingPolicyConfiguration: { AdjustmentType: 'ChangeInCapacity', MetricAggregationType: 'Maximum', }, }); - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 10, ExtendedStatistic: 'p99', @@ -247,14 +246,14 @@ describe('step scaling policy', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'StepScaling', StepScalingPolicyConfiguration: { AdjustmentType: 'ChangeInCapacity', MetricAggregationType: 'Maximum', }, }); - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 10, DatapointsToAlarm: 6, @@ -297,7 +296,7 @@ function setupStepScaling(intervals: appscaling.ScalingInterval[]) { scalingSteps: intervals, }); - return new ScalingStackTemplate(SynthUtils.synthesize(stack).template); + return new ScalingStackTemplate(Template.fromStack(stack).toJSON()); } class ScalingStackTemplate { diff --git a/packages/@aws-cdk/aws-applicationautoscaling/test/target-tracking.test.ts b/packages/@aws-cdk/aws-applicationautoscaling/test/target-tracking.test.ts index 2ec9f8dd07a26..fb9f50e5722c7 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/test/target-tracking.test.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/test/target-tracking.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as cdk from '@aws-cdk/core'; import * as appscaling from '../lib'; @@ -17,7 +17,7 @@ describe('target tracking', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'EC2SpotFleetRequestAverageCPUUtilization' }, @@ -41,7 +41,7 @@ describe('target tracking', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'LambdaProvisionedConcurrencyUtilization' }, @@ -53,6 +53,26 @@ describe('target tracking', () => { }); + test('test setup target tracking on predefined metric for DYNAMODB_WRITE_CAPACITY_UTILIZATION', () => { + // GIVEN + const stack = new cdk.Stack(); + const target = createScalableTarget(stack); + + // WHEN + target.scaleToTrackMetric('Tracking', { + predefinedMetric: appscaling.PredefinedMetric.DYNAMODB_WRITE_CAPACITY_UTILIZATION, + targetValue: 0.9, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { + TargetTrackingScalingPolicyConfiguration: { + PredefinedMetricSpecification: { PredefinedMetricType: 'DynamoDBWriteCapacityUtilization' }, + TargetValue: 0.9, + }, + }); + }); + test('test setup target tracking on custom metric', () => { // GIVEN const stack = new cdk.Stack(); @@ -65,7 +85,7 @@ describe('target tracking', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { CustomizedMetricSpecification: { diff --git a/packages/@aws-cdk/aws-applicationinsights/package.json b/packages/@aws-cdk/aws-applicationinsights/package.json index c41c3964d45c3..70ecad632f81f 100644 --- a/packages/@aws-cdk/aws-applicationinsights/package.json +++ b/packages/@aws-cdk/aws-applicationinsights/package.json @@ -28,6 +28,13 @@ "Framework :: AWS CDK :: 1" ] } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index 54bc79fade4c3..d168d56c73a7e 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -236,6 +236,24 @@ The `backends` property can be added with `node.addBackend()`. In the example, w The `backendDefaults` property is added to the node while creating the virtual node. These are the virtual node's default settings for all backends. +The `VirtualNode.addBackend()` method is especially useful if you want to create a circular traffic flow by having a Virtual Service as a backend whose provider is that same Virtual Node: + +```ts +declare const mesh: appmesh.Mesh; + +const node = new appmesh.VirtualNode(this, 'node', { + mesh, + serviceDiscovery: appmesh.ServiceDiscovery.dns('node'), +}); + +const virtualService = new appmesh.VirtualService(this, 'service-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.virtualNode(node), + virtualServiceName: 'service1.domain.local', +}); + +node.addBackend(appmesh.Backend.virtualService(virtualService)); +``` + ### Adding TLS to a listener The `tls` property specifies TLS configuration when creating a listener for a virtual node or a virtual gateway. diff --git a/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts b/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts index 8e672ac535bdc..7598ac959e98b 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts @@ -238,7 +238,14 @@ class VirtualServiceBackend extends Backend { return { virtualServiceBackend: { virtualService: { - virtualServiceName: this.virtualService.virtualServiceName, + /** + * We want to use the name of the Virtual Service here directly instead of + * a `{ 'Fn::GetAtt' }` CFN expression. This avoids a circular dependency in + * the case where this Virtual Node is the Virtual Service's provider. + */ + virtualServiceName: cdk.Token.isUnresolved(this.virtualService.virtualServiceName) + ? (this.virtualService as any).physicalName + : this.virtualService.virtualServiceName, clientPolicy: this.tlsClientPolicy ? { tls: renderTlsClientPolicy(scope, this.tlsClientPolicy), diff --git a/packages/@aws-cdk/aws-appmesh/package.json b/packages/@aws-cdk/aws-appmesh/package.json index 5d9afd2e2b8db..81b3eff008d13 100644 --- a/packages/@aws-cdk/aws-appmesh/package.json +++ b/packages/@aws-cdk/aws-appmesh/package.json @@ -84,13 +84,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-acmpca": "0.0.0", diff --git a/packages/@aws-cdk/aws-appmesh/test/gateway-route.test.ts b/packages/@aws-cdk/aws-appmesh/test/gateway-route.test.ts index 6ae344e7ff297..63516490d37a8 100644 --- a/packages/@aws-cdk/aws-appmesh/test/gateway-route.test.ts +++ b/packages/@aws-cdk/aws-appmesh/test/gateway-route.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as appmesh from '../lib'; @@ -50,9 +49,9 @@ describe('gateway route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'gateway-http-route', - MeshOwner: ABSENT, + MeshOwner: Match.absent(), Spec: { HttpRoute: { Action: { @@ -70,7 +69,7 @@ describe('gateway route', () => { }, }, }); - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'gateway-http2-route', Spec: { Http2Route: { @@ -89,7 +88,7 @@ describe('gateway route', () => { }, }, }); - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'gateway-grpc-route', Spec: { GrpcRoute: { @@ -163,11 +162,9 @@ describe('gateway route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { MeshOwner: meshEnv.account, }); - - }); }); @@ -214,7 +211,7 @@ describe('gateway route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'gateway-http-route', Spec: { HttpRoute: { @@ -229,7 +226,7 @@ describe('gateway route', () => { }, }); - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'gateway-grpc-route', Spec: { GrpcRoute: { @@ -243,8 +240,6 @@ describe('gateway route', () => { }, }, }); - - }); }); @@ -279,10 +274,8 @@ describe('gateway route', () => { }), gatewayRouteName: 'gateway-http2-route', }); - - // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'gateway-http2-route', Spec: { Http2Route: { @@ -296,8 +289,6 @@ describe('gateway route', () => { }, }, }); - - }); }); @@ -355,7 +346,7 @@ describe('gateway route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'gateway-http-route', Spec: { HttpRoute: { @@ -370,7 +361,7 @@ describe('gateway route', () => { }, }); - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'gateway-http2-route', Spec: { Http2Route: { @@ -385,18 +376,16 @@ describe('gateway route', () => { }, }); - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'gateway-http2-route-1', Spec: { Http2Route: { Action: { - Rewrite: ABSENT, + Rewrite: Match.absent(), }, }, }, }); - - }); test("should throw an error if the prefix match does not start and end with '/'", () => { @@ -429,8 +418,6 @@ describe('gateway route', () => { gatewayRouteName: 'gateway-http-route', }); }).toThrow(/Prefix path for the match must start with \'\/\', got: test\//); - - expect(() => { virtualGateway.addGatewayRoute('gateway-http2-route', { routeSpec: appmesh.GatewayRouteSpec.http2({ @@ -442,8 +429,6 @@ describe('gateway route', () => { gatewayRouteName: 'gateway-http2-route', }); }).toThrow(/When prefix path for the rewrite is specified, prefix path for the match must end with \'\/\', got: \/test/); - - }); test("should throw an error if the custom prefix does not start and end with '/'", () => { @@ -488,8 +473,6 @@ describe('gateway route', () => { gatewayRouteName: 'gateway-http2-route', }); }).toThrow(/Prefix path for the rewrite must start and end with \'\/\', got: \/rewrittenUri/); - - }); }); @@ -535,7 +518,7 @@ describe('gateway route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'gateway-http-route', Spec: { HttpRoute: { @@ -545,13 +528,13 @@ describe('gateway route', () => { }, }, Action: { - Rewrite: ABSENT, + Rewrite: Match.absent(), }, }, }, }); - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'gateway-grpc-route', Spec: { GrpcRoute: { @@ -563,8 +546,6 @@ describe('gateway route', () => { }, }, }); - - }); }); @@ -610,7 +591,7 @@ describe('gateway route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'gateway-grpc-route', Spec: { GrpcRoute: { @@ -681,8 +662,6 @@ describe('gateway route', () => { }, }, }); - - }); test('should throw an error if the array length is invalid', () => { @@ -742,8 +721,6 @@ describe('gateway route', () => { gatewayRouteName: 'gateway-grpc-route', }); }).toThrow(/Number of metadata provided for matching must be between 1 and 10/); - - }); }); @@ -790,7 +767,7 @@ describe('gateway route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'gateway-http-route', Spec: { HttpRoute: { @@ -861,8 +838,6 @@ describe('gateway route', () => { }, }, }); - - }); }); @@ -898,7 +873,7 @@ describe('gateway route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'gateway-http-route', Spec: { HttpRoute: { @@ -908,8 +883,6 @@ describe('gateway route', () => { }, }, }); - - }); }); @@ -956,7 +929,7 @@ describe('gateway route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'gateway-http-route', Spec: { HttpRoute: { @@ -966,13 +939,13 @@ describe('gateway route', () => { }, }, Action: { - Rewrite: ABSENT, + Rewrite: Match.absent(), }, }, }, }); - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'gateway-http2-route', Spec: { Http2Route: { @@ -984,8 +957,6 @@ describe('gateway route', () => { }, }, }); - - }); test('should throw an error if empty string is passed', () => { @@ -1019,8 +990,6 @@ describe('gateway route', () => { gatewayRouteName: 'gateway-http-route', }); }).toThrow(/Exact Path for the rewrite cannot be empty. Unlike startsWith\(\) method, no automatic rewrite on whole path match/); - - }); }); @@ -1058,7 +1027,7 @@ describe('gateway route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'gateway-http-route', Spec: { HttpRoute: { @@ -1075,8 +1044,6 @@ describe('gateway route', () => { }, }, }); - - }); }); }); @@ -1112,7 +1079,7 @@ describe('gateway route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'gateway-http-route', Spec: { HttpRoute: { @@ -1157,7 +1124,7 @@ describe('gateway route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { Spec: { Priority: 100, }, @@ -1197,7 +1164,7 @@ describe('gateway route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { Spec: { Priority: 500, }, diff --git a/packages/@aws-cdk/aws-appmesh/test/health-check.test.ts b/packages/@aws-cdk/aws-appmesh/test/health-check.test.ts index 7abb6ed92f249..ebc78039d8cc4 100644 --- a/packages/@aws-cdk/aws-appmesh/test/health-check.test.ts +++ b/packages/@aws-cdk/aws-appmesh/test/health-check.test.ts @@ -1,5 +1,4 @@ import * as cdk from '@aws-cdk/core'; - import * as appmesh from '../lib'; let idCounter = 0; @@ -29,8 +28,6 @@ describe('health check', () => { expect(() => toThrow(max)).not.toThrow(); expect(() => toThrow(min - 1)).toThrow(/interval must be between 5 seconds and 300 seconds/); expect(() => toThrow(max + 1)).toThrow(/interval must be between 5 seconds and 300 seconds/); - - }); test('timeout', () => { // GIVEN @@ -48,8 +45,6 @@ describe('health check', () => { expect(() => toThrow(max)).not.toThrow(); expect(() => toThrow(min - 1)).toThrow(/timeout must be between 2 seconds and 60 seconds/); expect(() => toThrow(max + 1)).toThrow(/timeout must be between 2 seconds and 60 seconds/); - - }); test('healthyThreshold', () => { // GIVEN @@ -67,8 +62,6 @@ describe('health check', () => { expect(() => toThrow(max)).not.toThrow(); expect(() => toThrow(min - 1)).toThrow(/healthyThreshold must be between 2 and 10/); expect(() => toThrow(max + 1)).toThrow(/healthyThreshold must be between 2 and 10/); - - }); test('unhealthyThreshold', () => { // GIVEN @@ -86,7 +79,5 @@ describe('health check', () => { expect(() => toThrow(max)).not.toThrow(); expect(() => toThrow(min - 1)).toThrow(/unhealthyThreshold must be between 2 and 10/); expect(() => toThrow(max + 1)).toThrow(/unhealthyThreshold must be between 2 and 10/); - - }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json index 4b6c3e54f543e..3649195cf9c7e 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json @@ -1040,22 +1040,12 @@ "Backends": [ { "VirtualService": { - "VirtualServiceName": { - "Fn::GetAtt": [ - "service6D174F83", - "VirtualServiceName" - ] - } + "VirtualServiceName": "service1.domain.local" } }, { "VirtualService": { - "VirtualServiceName": { - "Fn::GetAtt": [ - "service27C65CF7D", - "VirtualServiceName" - ] - } + "VirtualServiceName": "service2.domain.local" } } ], @@ -1111,12 +1101,7 @@ "Backends": [ { "VirtualService": { - "VirtualServiceName": { - "Fn::GetAtt": [ - "service3859EB104", - "VirtualServiceName" - ] - } + "VirtualServiceName": "service3.domain.local" } } ], @@ -1241,12 +1226,7 @@ "Backends": [ { "VirtualService": { - "VirtualServiceName": { - "Fn::GetAtt": [ - "service4983B61EE", - "VirtualServiceName" - ] - } + "VirtualServiceName": "service4.domain.local" } } ], diff --git a/packages/@aws-cdk/aws-appmesh/test/mesh.test.ts b/packages/@aws-cdk/aws-appmesh/test/mesh.test.ts index f30d1562416da..e8f7588e59f4f 100644 --- a/packages/@aws-cdk/aws-appmesh/test/mesh.test.ts +++ b/packages/@aws-cdk/aws-appmesh/test/mesh.test.ts @@ -1,8 +1,7 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; - import * as appmesh from '../lib'; describe('mesh', () => { @@ -16,13 +15,11 @@ describe('mesh', () => { new appmesh.Mesh(stack, 'mesh', { meshName: 'test-mesh' }); // THEN - expect(stack). - toHaveResource('AWS::AppMesh::Mesh', { + Template.fromStack(stack). + hasResourceProperties('AWS::AppMesh::Mesh', { Spec: { }, }); - - }); }); @@ -38,16 +35,14 @@ describe('mesh', () => { }); // THEN - expect(stack). - toHaveResource('AWS::AppMesh::Mesh', { + Template.fromStack(stack). + hasResourceProperties('AWS::AppMesh::Mesh', { Spec: { EgressFilter: { Type: 'ALLOW_ALL', }, }, }); - - }); }); }); @@ -66,8 +61,8 @@ describe('mesh', () => { mesh.addVirtualRouter('router'); // THEN - expect(stack). - toHaveResource('AWS::AppMesh::VirtualRouter', { + Template.fromStack(stack). + hasResourceProperties('AWS::AppMesh::VirtualRouter', { Spec: { Listeners: [ { @@ -79,8 +74,6 @@ describe('mesh', () => { ], }, }); - - }); }); }); @@ -105,7 +98,7 @@ describe('mesh', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { ServiceDiscovery: { AWSCloudMap: { @@ -115,8 +108,6 @@ describe('mesh', () => { }, }, }); - - }); test('VirtualService can use CloudMap service with instanceAttributes', () => { @@ -142,7 +133,7 @@ describe('mesh', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { ServiceDiscovery: { AWSCloudMap: { @@ -158,8 +149,6 @@ describe('mesh', () => { }, }, }); - - }); describe('When adding a VirtualNode to a mesh', () => { @@ -178,8 +167,8 @@ describe('mesh', () => { }); // THEN - expect(stack). - toHaveResource('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack). + hasResourceProperties('AWS::AppMesh::VirtualNode', { MeshName: { 'Fn::GetAtt': ['meshACDFE68E', 'MeshName'], }, @@ -192,8 +181,6 @@ describe('mesh', () => { }, }, }); - - }); }); describe('with added listeners', () => { @@ -214,8 +201,8 @@ describe('mesh', () => { }); // THEN - expect(stack). - toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack). + hasResourceProperties('AWS::AppMesh::VirtualNode', { MeshName: { 'Fn::GetAtt': ['meshACDFE68E', 'MeshName'], }, @@ -230,8 +217,6 @@ describe('mesh', () => { ], }, }); - - }); }); describe('with added listeners with healthchecks', () => { @@ -259,14 +244,14 @@ describe('mesh', () => { }); // THEN - expect(stack). - toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack). + hasResourceProperties('AWS::AppMesh::VirtualNode', { MeshName: { 'Fn::GetAtt': ['meshACDFE68E', 'MeshName'], }, Spec: { Listeners: [ - { + Match.objectLike({ HealthCheck: { HealthyThreshold: 3, IntervalMillis: 5000, @@ -276,12 +261,10 @@ describe('mesh', () => { TimeoutMillis: 2000, UnhealthyThreshold: 2, }, - }, + }), ], }, }); - - }); }); describe('with backends', () => { @@ -308,22 +291,18 @@ describe('mesh', () => { }); // THEN - expect(stack). - toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack). + hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { Backends: [ { VirtualService: { - VirtualServiceName: { - 'Fn::GetAtt': ['service1A48078CF', 'VirtualServiceName'], - }, + VirtualServiceName: 'service1.domain.local', }, }, ], }, }); - - }); }); }); @@ -338,13 +317,11 @@ describe('mesh', () => { }); // THEN - expect(stack2). - toHaveResourceLike('AWS::AppMesh::VirtualService', { + Template.fromStack(stack2). + hasResourceProperties('AWS::AppMesh::VirtualService', { MeshName: 'abc', Spec: {}, VirtualServiceName: 'test.domain.local', }); - - }); }); diff --git a/packages/@aws-cdk/aws-appmesh/test/route.test.ts b/packages/@aws-cdk/aws-appmesh/test/route.test.ts index e39b20585a03c..130a46f058f63 100644 --- a/packages/@aws-cdk/aws-appmesh/test/route.test.ts +++ b/packages/@aws-cdk/aws-appmesh/test/route.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as appmesh from '../lib'; @@ -80,7 +79,7 @@ describe('route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { HttpRoute: { Action: { @@ -111,11 +110,11 @@ describe('route', () => { }, }, }, - MeshOwner: ABSENT, + MeshOwner: Match.absent(), RouteName: 'test-http-route', }); - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { Http2Route: { Action: { @@ -149,7 +148,7 @@ describe('route', () => { RouteName: 'test-http2-route', }); - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { TcpRoute: { Action: { @@ -176,7 +175,7 @@ describe('route', () => { RouteName: 'test-tcp-route', }); - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { GrpcRoute: { Action: { @@ -209,8 +208,6 @@ describe('route', () => { }, RouteName: 'test-grpc-route', }); - - }); test('should have expected features', () => { @@ -247,7 +244,7 @@ describe('route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { HttpRoute: { Action: { @@ -311,7 +308,7 @@ describe('route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { HttpRoute: { RetryPolicy: { @@ -326,11 +323,9 @@ describe('route', () => { }, }, }); - - }); - test('http retry events are ABSENT when specified as an empty array', () => { + test('http retry events are Match.absent() when specified as an empty array', () => { // GIVEN const stack = new cdk.Stack(); const mesh = new appmesh.Mesh(stack, 'mesh', { @@ -370,28 +365,26 @@ describe('route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { HttpRoute: { RetryPolicy: { - HttpRetryEvents: ABSENT, + HttpRetryEvents: Match.absent(), TcpRetryEvents: ['connection-error'], }, }, }, }); - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { HttpRoute: { RetryPolicy: { HttpRetryEvents: ['client-error'], - TcpRetryEvents: ABSENT, + TcpRetryEvents: Match.absent(), }, }, }, }); - - }); test('errors when http retry policy has no events', () => { @@ -420,8 +413,6 @@ describe('route', () => { }), }); }).toThrow(/specify one value for at least/i); - - }); test('should allow grpc retries', () => { @@ -454,7 +445,7 @@ describe('route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { GrpcRoute: { RetryPolicy: { @@ -470,11 +461,9 @@ describe('route', () => { }, }, }); - - }); - test('grpc retry events are ABSENT when specified as an empty array', () => { + test('grpc retry events are Match.absent() when specified as an empty array', () => { // GIVEN const stack = new cdk.Stack(); const mesh = new appmesh.Mesh(stack, 'mesh', { @@ -518,30 +507,28 @@ describe('route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { GrpcRoute: { RetryPolicy: { - GrpcRetryEvents: ABSENT, - HttpRetryEvents: ABSENT, + GrpcRetryEvents: Match.absent(), + HttpRetryEvents: Match.absent(), TcpRetryEvents: ['connection-error'], }, }, }, }); - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { GrpcRoute: { RetryPolicy: { GrpcRetryEvents: ['cancelled'], - HttpRetryEvents: ABSENT, - TcpRetryEvents: ABSENT, + HttpRetryEvents: Match.absent(), + TcpRetryEvents: Match.absent(), }, }, }, }); - - }); test('errors when grpc retry policy has no events', () => { @@ -571,8 +558,6 @@ describe('route', () => { }), }); }).toThrow(/specify one value for at least/i); - - }); describe('with shared service mesh', () => { @@ -605,11 +590,9 @@ describe('route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { MeshOwner: meshEnv.account, }); - - }); }); }); @@ -653,7 +636,7 @@ describe('route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { Http2Route: { Match: { @@ -724,8 +707,6 @@ describe('route', () => { }, }, }); - - }); test('should match routes based on method', () => { @@ -756,7 +737,7 @@ describe('route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { Http2Route: { Match: { @@ -766,8 +747,6 @@ describe('route', () => { }, }, }); - - }); test('should match routes based on scheme', () => { @@ -798,7 +777,7 @@ describe('route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { Http2Route: { Match: { @@ -808,8 +787,6 @@ describe('route', () => { }, }, }); - - }); test('should match routes based on metadata', () => { @@ -852,7 +829,7 @@ describe('route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { GrpcRoute: { Match: { @@ -922,8 +899,6 @@ describe('route', () => { }, }, }); - - }); test('should match routes based on path', () => { @@ -966,7 +941,7 @@ describe('route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { HttpRoute: { Match: { @@ -978,7 +953,7 @@ describe('route', () => { }, }); - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { Http2Route: { Match: { @@ -989,8 +964,6 @@ describe('route', () => { }, }, }); - - }); test('should match routes based query parameter', () => { @@ -1022,7 +995,7 @@ describe('route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { HttpRoute: { Match: { @@ -1038,8 +1011,6 @@ describe('route', () => { }, }, }); - - }); test('should match routes based method name', () => { @@ -1073,7 +1044,7 @@ describe('route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { GrpcRoute: { Match: { @@ -1083,8 +1054,6 @@ describe('route', () => { }, }, }); - - }); test('should throw an error with invalid number of headers', () => { @@ -1141,8 +1110,6 @@ describe('route', () => { }), }); }).toThrow(/Number of headers provided for matching must be between 1 and 10, got: 11/); - - }); test('should throw an error with invalid number of query parameters', () => { @@ -1199,8 +1166,6 @@ describe('route', () => { }), }); }).toThrow(/Number of query parameters provided for matching must be between 1 and 10, got: 11/); - - }); test('should throw an error with invalid number of metadata', () => { @@ -1260,8 +1225,6 @@ describe('route', () => { }), }); }).toThrow(/Number of metadata provided for matching must be between 1 and 10, got: 11/); - - }); test('should throw an error if no gRPC match type is defined', () => { @@ -1292,8 +1255,6 @@ describe('route', () => { }), }); }).toThrow(/At least one gRPC route match property must be provided/); - - }); test('should throw an error if method name is specified without service name', () => { @@ -1326,8 +1287,6 @@ describe('route', () => { }), }); }).toThrow(/If you specify a method name, you must also specify a service name/); - - }); test('should allow route priority', () => { @@ -1376,32 +1335,30 @@ describe('route', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { Priority: 0, Http2Route: {}, }, }); - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { Priority: 10, HttpRoute: {}, }, }); - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { Priority: 20, GrpcRoute: {}, }, }); - expect(stack).toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::Route', { Spec: { Priority: 30, TcpRoute: {}, }, }); - - }); test('Can import Routes using an ARN', () => { @@ -1443,7 +1400,5 @@ describe('route', () => { // THEN expect(route.routeName).toEqual(routeName); expect(route.virtualRouter.mesh.meshName).toEqual(meshName); - - }); }); diff --git a/packages/@aws-cdk/aws-appmesh/test/virtual-gateway.test.ts b/packages/@aws-cdk/aws-appmesh/test/virtual-gateway.test.ts index 17a78b7d361d6..eadfebec29493 100644 --- a/packages/@aws-cdk/aws-appmesh/test/virtual-gateway.test.ts +++ b/packages/@aws-cdk/aws-appmesh/test/virtual-gateway.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as acm from '@aws-cdk/aws-certificatemanager'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; @@ -39,7 +38,7 @@ describe('virtual gateway', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualGateway', { Spec: { Listeners: [ { @@ -53,7 +52,7 @@ describe('virtual gateway', () => { VirtualGatewayName: 'testGateway', }); - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualGateway', { Spec: { Listeners: [ { @@ -74,10 +73,10 @@ describe('virtual gateway', () => { ], }, VirtualGatewayName: 'httpGateway', - MeshOwner: ABSENT, + MeshOwner: Match.absent(), }); - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualGateway', { Spec: { Listeners: [ { @@ -99,7 +98,6 @@ describe('virtual gateway', () => { }, VirtualGatewayName: 'http2Gateway', }); - }); test('should have expected features', () => { @@ -122,7 +120,7 @@ describe('virtual gateway', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualGateway', { Spec: { Listeners: [ { @@ -150,7 +148,6 @@ describe('virtual gateway', () => { }, VirtualGatewayName: 'test-gateway', }); - }); test('with an http listener with a TLS certificate from ACM', () => { @@ -179,10 +176,10 @@ describe('virtual gateway', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualGateway', { Spec: { Listeners: [ - { + Match.objectLike({ TLS: { Mode: appmesh.TlsMode.STRICT, Certificate: { @@ -193,12 +190,10 @@ describe('virtual gateway', () => { }, }, }, - }, + }), ], }, }); - - }); test('with an grpc listener with a TLS certificate from file', () => { @@ -223,10 +218,10 @@ describe('virtual gateway', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualGateway', { Spec: { Listeners: [ - { + Match.objectLike({ TLS: { Mode: appmesh.TlsMode.STRICT, Certificate: { @@ -236,12 +231,10 @@ describe('virtual gateway', () => { }, }, }, - }, + }), ], }, }); - - }); test('with an http2 listener with a TLS certificate from SDS', () => { @@ -265,10 +258,10 @@ describe('virtual gateway', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualGateway', { Spec: { Listeners: [ - { + Match.objectLike({ TLS: { Mode: appmesh.TlsMode.STRICT, Certificate: { @@ -277,12 +270,10 @@ describe('virtual gateway', () => { }, }, }, - }, + }), ], }, }); - - }); describe('with an grpc listener with a TLS validation from file', () => { @@ -310,10 +301,10 @@ describe('virtual gateway', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualGateway', { Spec: { Listeners: [ - { + Match.objectLike({ TLS: { Mode: appmesh.TlsMode.STRICT, Certificate: { @@ -330,12 +321,10 @@ describe('virtual gateway', () => { }, }, }, - }, + }), ], }, }); - - }); }); @@ -365,10 +354,10 @@ describe('virtual gateway', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualGateway', { Spec: { Listeners: [ - { + Match.objectLike({ TLS: { Mode: appmesh.TlsMode.STRICT, Certificate: { @@ -384,12 +373,10 @@ describe('virtual gateway', () => { }, }, }, - }, + }), ], }, }); - - }); }); @@ -415,10 +402,10 @@ describe('virtual gateway', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualGateway', { Spec: { Listeners: [ - { + Match.objectLike({ TLS: { Mode: appmesh.TlsMode.PERMISSIVE, Certificate: { @@ -428,12 +415,10 @@ describe('virtual gateway', () => { }, }, }, - }, + }), ], }, }); - - }); describe('with shared service mesh', () => { @@ -455,11 +440,9 @@ describe('virtual gateway', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualGateway', { MeshOwner: meshEnv.account, }); - - }); }); }); @@ -491,7 +474,7 @@ describe('virtual gateway', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'test-gateway-route', Spec: { HttpRoute: { @@ -510,7 +493,6 @@ describe('virtual gateway', () => { }, }, }); - }); }); @@ -542,13 +524,12 @@ describe('virtual gateway', () => { }), }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'test-gateway-route', }); - expect(stack).toHaveResourceLike('AWS::AppMesh::GatewayRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'test-gateway-route-2', }); - }); }); @@ -575,7 +556,7 @@ describe('virtual gateway', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualGateway', { VirtualGatewayName: 'virtual-gateway', Spec: { BackendDefaults: { @@ -593,8 +574,6 @@ describe('virtual gateway', () => { }, }, }); - - }); describe("with client's TLS certificate from SDS", () => { @@ -621,7 +600,7 @@ describe('virtual gateway', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualGateway', { VirtualGatewayName: 'virtual-gateway', Spec: { BackendDefaults: { @@ -649,8 +628,6 @@ describe('virtual gateway', () => { }, }, }); - - }); }); }); @@ -679,23 +656,21 @@ describe('virtual gateway', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualGateway', { VirtualGatewayName: 'virtual-gateway', Spec: { Listeners: [ - { + Match.objectLike({ ConnectionPool: { HTTP: { MaxConnections: 100, MaxPendingRequests: 10, }, }, - }, + }), ], }, }); - - }); test('Can add an grpc connection pool to listener', () => { @@ -721,22 +696,20 @@ describe('virtual gateway', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualGateway', { VirtualGatewayName: 'virtual-gateway', Spec: { Listeners: [ - { + Match.objectLike({ ConnectionPool: { GRPC: { MaxRequests: 10, }, }, - }, + }), ], }, }); - - }); test('Can add an http2 connection pool to listener', () => { @@ -762,22 +735,20 @@ describe('virtual gateway', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualGateway', { VirtualGatewayName: 'virtual-gateway', Spec: { Listeners: [ - { + Match.objectLike({ ConnectionPool: { HTTP2: { MaxRequests: 10, }, }, - }, + }), ], }, }); - - }); test('Can import VirtualGateways using an ARN', () => { @@ -796,7 +767,6 @@ describe('virtual gateway', () => { // THEN expect(virtualGateway.mesh.meshName).toEqual(meshName); expect(virtualGateway.virtualGatewayName).toEqual(virtualGatewayName); - }); test('Can import VirtualGateways using attributes', () => { @@ -816,8 +786,6 @@ describe('virtual gateway', () => { // THEN expect(virtualGateway.mesh.meshName).toEqual(meshName); expect(virtualGateway.virtualGatewayName).toEqual(virtualGatewayName); - - }); test('Can grant an identity StreamAggregatedResources for a given VirtualGateway', () => { @@ -835,7 +803,7 @@ describe('virtual gateway', () => { gateway.grantStreamAggregatedResources(user); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -848,7 +816,5 @@ describe('virtual gateway', () => { ], }, }); - - }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appmesh/test/virtual-node.test.ts b/packages/@aws-cdk/aws-appmesh/test/virtual-node.test.ts index 82bc98c6267ef..f419f7ac41733 100644 --- a/packages/@aws-cdk/aws-appmesh/test/virtual-node.test.ts +++ b/packages/@aws-cdk/aws-appmesh/test/virtual-node.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as acmpca from '@aws-cdk/aws-acmpca'; import * as acm from '@aws-cdk/aws-certificatemanager'; import * as iam from '@aws-cdk/aws-iam'; @@ -36,29 +35,23 @@ describe('virtual node', () => { node.addBackend(appmesh.Backend.virtualService(service2)); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { Backends: [ { VirtualService: { - VirtualServiceName: { - 'Fn::GetAtt': ['service1A48078CF', 'VirtualServiceName'], - }, + VirtualServiceName: 'service1.domain.local', }, }, { VirtualService: { - VirtualServiceName: { - 'Fn::GetAtt': ['service27C65CF7D', 'VirtualServiceName'], - }, + VirtualServiceName: 'service2.domain.local', }, }, ], }, - MeshOwner: ABSENT, + MeshOwner: Match.absent(), }); - - }); }); @@ -81,7 +74,7 @@ describe('virtual node', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { Listeners: [ { @@ -93,8 +86,6 @@ describe('virtual node', () => { ], }, }); - - }); }); @@ -121,7 +112,7 @@ describe('virtual node', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { Listeners: [ { @@ -145,8 +136,6 @@ describe('virtual node', () => { ], }, }); - - }); }); @@ -171,10 +160,10 @@ describe('virtual node', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { Listeners: [ - { + Match.objectLike({ HealthCheck: { HealthyThreshold: 2, IntervalMillis: 5000, @@ -195,12 +184,10 @@ describe('virtual node', () => { }, }, }, - }, + }), ], }, }); - - }); }); @@ -228,10 +215,10 @@ describe('virtual node', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { Listeners: [ - { + Match.objectLike({ HealthCheck: { HealthyThreshold: 2, IntervalMillis: 5000, @@ -252,12 +239,10 @@ describe('virtual node', () => { }, }, }, - }, + }), ], }, }); - - }); }); @@ -287,7 +272,7 @@ describe('virtual node', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { Listeners: [ { @@ -311,8 +296,6 @@ describe('virtual node', () => { ], }, }); - - }); }); @@ -342,7 +325,7 @@ describe('virtual node', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { BackendDefaults: { ClientPolicy: { @@ -360,8 +343,6 @@ describe('virtual node', () => { }, }, }); - - }); }); @@ -390,7 +371,7 @@ describe('virtual node', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { BackendDefaults: { ClientPolicy: { @@ -418,8 +399,6 @@ describe('virtual node', () => { }, }, }); - - }); }); @@ -453,14 +432,12 @@ describe('virtual node', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { Backends: [ { VirtualService: { - VirtualServiceName: { - 'Fn::GetAtt': ['service1A48078CF', 'VirtualServiceName'], - }, + VirtualServiceName: 'service1.domain.local', ClientPolicy: { TLS: { Ports: [8080, 8081], @@ -478,8 +455,75 @@ describe('virtual node', () => { ], }, }); + }); + + test('you can add a Virtual Service as a backend to a Virtual Node which is the provider for that Virtual Service', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const node = new appmesh.VirtualNode(stack, 'test-node', { + mesh, + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + }); + + const myVirtualService = new appmesh.VirtualService(stack, 'service-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.virtualNode(node), + virtualServiceName: 'service1.domain.local', + }); + + node.addBackend(appmesh.Backend.virtualService(myVirtualService)); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { + Spec: { + Backends: [ + { + VirtualService: { + VirtualServiceName: 'service1.domain.local', + }, + }, + ], + }, + }); + }); + + test('you can add a Virtual Service with an automated name as a backend to a Virtual Node which is the provider for that Virtual Service, ', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const node = new appmesh.VirtualNode(stack, 'test-node', { + mesh, + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + }); + const myVirtualService = new appmesh.VirtualService(stack, 'service-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.virtualNode(node), + }); + + node.addBackend(appmesh.Backend.virtualService(myVirtualService)); + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { + Spec: { + Backends: [ + { + VirtualService: { + VirtualServiceName: 'service1', + }, + }, + ], + }, + }); }); }); @@ -511,10 +555,10 @@ describe('virtual node', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { Listeners: [ - { + Match.objectLike({ TLS: { Mode: appmesh.TlsMode.STRICT, Certificate: { @@ -525,12 +569,10 @@ describe('virtual node', () => { }, }, }, - }, + }), ], }, }); - - }); }); @@ -557,10 +599,10 @@ describe('virtual node', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { Listeners: [ - { + Match.objectLike({ TLS: { Mode: appmesh.TlsMode.STRICT, Certificate: { @@ -570,12 +612,10 @@ describe('virtual node', () => { }, }, }, - }, + }), ], }, }); - - }); }); @@ -601,10 +641,10 @@ describe('virtual node', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { Listeners: [ - { + Match.objectLike({ TLS: { Mode: appmesh.TlsMode.STRICT, Certificate: { @@ -613,12 +653,10 @@ describe('virtual node', () => { }, }, }, - }, + }), ], }, }); - - }); }); @@ -645,10 +683,10 @@ describe('virtual node', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { Listeners: [ - { + Match.objectLike({ TLS: { Mode: appmesh.TlsMode.PERMISSIVE, Certificate: { @@ -658,12 +696,10 @@ describe('virtual node', () => { }, }, }, - }, + }), ], }, }); - - }); }); @@ -691,22 +727,20 @@ describe('virtual node', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { Listeners: [ - { + Match.objectLike({ ConnectionPool: { HTTP: { MaxConnections: 100, MaxPendingRequests: 10, }, }, - }, + }), ], }, }); - - }); test('Can add an tcp connection pool to listener', () => { @@ -732,21 +766,19 @@ describe('virtual node', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { - Listeners: [ - { + Listeners: Match.arrayWith([ + Match.objectLike({ ConnectionPool: { TCP: { MaxConnections: 100, }, }, - }, - ], + }), + ]), }, }); - - }); test('Can add an grpc connection pool to listener', () => { @@ -772,21 +804,19 @@ describe('virtual node', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { - Listeners: [ - { + Listeners: Match.arrayWith([ + Match.objectLike({ ConnectionPool: { GRPC: { MaxRequests: 10, }, }, - }, - ], + }), + ]), }, }); - - }); test('Can add an http2 connection pool to listener', () => { @@ -812,21 +842,19 @@ describe('virtual node', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { Listeners: [ - { + Match.objectLike({ ConnectionPool: { HTTP2: { MaxRequests: 10, }, }, - }, + }), ], }, }); - - }); }); @@ -844,8 +872,6 @@ describe('virtual node', () => { // THEN expect(virtualNode.mesh.meshName).toEqual(meshName); expect(virtualNode.virtualNodeName).toEqual(virtualNodeName); - - }); test('Can import Virtual Nodes using attributes', () => { @@ -861,8 +887,6 @@ describe('virtual node', () => { // THEN expect(virtualNode.mesh.meshName).toEqual(meshName); expect(virtualNode.virtualNodeName).toEqual(virtualNodeName); - - }); test('Can grant an identity StreamAggregatedResources for a given VirtualNode', () => { @@ -888,7 +912,7 @@ describe('virtual node', () => { node.grantStreamAggregatedResources(user); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -901,8 +925,6 @@ describe('virtual node', () => { ], }, }); - - }); describe('When creating a VirtualNode', () => { @@ -925,11 +947,9 @@ describe('virtual node', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { MeshOwner: meshEnv.account, }); - - }); }); @@ -949,7 +969,7 @@ describe('virtual node', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualNode', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualNode', { Spec: { ServiceDiscovery: { DNS: { @@ -959,8 +979,6 @@ describe('virtual node', () => { }, }, }); - - }); }); @@ -994,11 +1012,7 @@ describe('virtual node', () => { expect(() => { node.addListener(appmesh.VirtualNodeListener.http()); }).toThrow(/Service discovery information is required for a VirtualNode with a listener/); - - }); }); }); }); - - diff --git a/packages/@aws-cdk/aws-appmesh/test/virtual-router.test.ts b/packages/@aws-cdk/aws-appmesh/test/virtual-router.test.ts index 7342aa8f052bd..24df84fd051ad 100644 --- a/packages/@aws-cdk/aws-appmesh/test/virtual-router.test.ts +++ b/packages/@aws-cdk/aws-appmesh/test/virtual-router.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as appmesh from '../lib'; @@ -11,10 +10,11 @@ describe('virtual router', () => { const mesh = new appmesh.Mesh(stack, 'mesh', { meshName: 'test-mesh', }); + // WHEN mesh.addVirtualRouter('http-router-listener'); - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualRouter', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualRouter', { VirtualRouterName: 'meshhttprouterlistenerF57BCB2F', Spec: { Listeners: [ @@ -27,14 +27,15 @@ describe('virtual router', () => { ], }, }); - }); + test('should have protocol variant listeners', () => { // GIVEN const stack = new cdk.Stack(); const mesh = new appmesh.Mesh(stack, 'mesh', { meshName: 'test-mesh', }); + // WHEN mesh.addVirtualRouter('http-router-listener', { listeners: [ @@ -67,7 +68,7 @@ describe('virtual router', () => { // THEN const expectedPorts = [appmesh.Protocol.HTTP, appmesh.Protocol.HTTP2, appmesh.Protocol.GRPC, appmesh.Protocol.TCP]; expectedPorts.forEach(protocol => { - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualRouter', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualRouter', { VirtualRouterName: `${protocol}-router-listener`, Spec: { Listeners: [ @@ -79,11 +80,9 @@ describe('virtual router', () => { }, ], }, - MeshOwner: ABSENT, + MeshOwner: Match.absent(), }); }); - - }); describe('with shared service mesh', () => { @@ -105,11 +104,9 @@ describe('virtual router', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualRouter', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualRouter', { MeshOwner: meshEnv.account, }); - - }); }); }); @@ -154,8 +151,8 @@ describe('virtual router', () => { }); // THEN - expect(stack). - toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack). + hasResourceProperties('AWS::AppMesh::Route', { RouteName: 'route-1', Spec: { HttpRoute: { @@ -178,8 +175,6 @@ describe('virtual router', () => { 'Fn::GetAtt': ['meshrouter81B8087E', 'VirtualRouterName'], }, }); - - }); }); describe('When adding routes to a VirtualRouter with existing routes', () => { @@ -268,8 +263,8 @@ describe('virtual router', () => { }); // THEN - expect(stack). - toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack). + hasResourceProperties('AWS::AppMesh::Route', { RouteName: 'route-1', Spec: { HttpRoute: { @@ -290,8 +285,8 @@ describe('virtual router', () => { }, }); - expect(stack). - toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack). + hasResourceProperties('AWS::AppMesh::Route', { RouteName: 'route-2', Spec: { HttpRoute: { @@ -312,8 +307,8 @@ describe('virtual router', () => { }, }); - expect(stack). - toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack). + hasResourceProperties('AWS::AppMesh::Route', { RouteName: 'route-3', Spec: { HttpRoute: { @@ -333,8 +328,6 @@ describe('virtual router', () => { }, }, }); - - }); }); describe('When adding a TCP route to existing VirtualRouter', () => { @@ -374,8 +367,8 @@ describe('virtual router', () => { }); // THEN - expect(stack). - toHaveResourceLike('AWS::AppMesh::Route', { + Template.fromStack(stack). + hasResourceProperties('AWS::AppMesh::Route', { RouteName: 'route-tcp-1', Spec: { TcpRoute: { @@ -395,8 +388,6 @@ describe('virtual router', () => { 'Fn::GetAtt': ['meshrouter81B8087E', 'VirtualRouterName'], }, }); - - }); }); @@ -414,8 +405,6 @@ describe('virtual router', () => { // THEN expect(virtualRouter.mesh.meshName).toEqual(meshName); expect(virtualRouter.virtualRouterName).toEqual(virtualRouterName); - - }); test('Can import Virtual Routers using attributes', () => { // GIVEN @@ -433,7 +422,5 @@ describe('virtual router', () => { // THEN expect(virtualRouter1.mesh.meshName).toEqual(meshName); expect(virtualRouter1.virtualRouterName).toEqual(virtualRouterName); - - }); }); diff --git a/packages/@aws-cdk/aws-appmesh/test/virtual-service.test.ts b/packages/@aws-cdk/aws-appmesh/test/virtual-service.test.ts index d74ad01920ef0..2cec646ed7377 100644 --- a/packages/@aws-cdk/aws-appmesh/test/virtual-service.test.ts +++ b/packages/@aws-cdk/aws-appmesh/test/virtual-service.test.ts @@ -1,7 +1,5 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; - import * as appmesh from '../lib'; describe('virtual service', () => { @@ -17,8 +15,6 @@ describe('virtual service', () => { // THEN expect(virtualRouter.mesh.meshName).toEqual(meshName); expect(virtualRouter.virtualRouterName).toEqual(virtualServiceName); - - }); test('Can import Virtual Services using attributes', () => { @@ -36,8 +32,6 @@ describe('virtual service', () => { // THEN expect(virtualService.mesh.meshName).toEqual(meshName); expect(virtualService.virtualServiceName).toEqual(virtualServiceName); - - }); describe('When adding a VirtualService to a mesh', () => { @@ -63,8 +57,8 @@ describe('virtual service', () => { }); // THEN - expect(stack). - toHaveResource('AWS::AppMesh::VirtualService', { + Template.fromStack(stack). + hasResourceProperties('AWS::AppMesh::VirtualService', { Spec: { Provider: { VirtualRouter: { @@ -74,10 +68,8 @@ describe('virtual service', () => { }, }, }, - MeshOwner: ABSENT, + MeshOwner: Match.absent(), }); - - }); }); @@ -104,8 +96,8 @@ describe('virtual service', () => { }); // THEN - expect(stack). - toHaveResource('AWS::AppMesh::VirtualService', { + Template.fromStack(stack). + hasResourceProperties('AWS::AppMesh::VirtualService', { Spec: { Provider: { VirtualNode: { @@ -116,8 +108,6 @@ describe('virtual service', () => { }, }, }); - - }); }); }); @@ -149,11 +139,9 @@ describe('virtual service', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AppMesh::VirtualService', { + Template.fromStack(stack).hasResourceProperties('AWS::AppMesh::VirtualService', { MeshOwner: meshEnv.account, }); - - }); }); }); diff --git a/packages/@aws-cdk/aws-apprunner/README.md b/packages/@aws-cdk/aws-apprunner/README.md index e9c01f631d4b8..658c46fc9ecb6 100644 --- a/packages/@aws-cdk/aws-apprunner/README.md +++ b/packages/@aws-cdk/aws-apprunner/README.md @@ -23,8 +23,8 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. -```ts -import apprunner = require('@aws-cdk/aws-apprunner'); +```ts nofixture +import * as apprunner from '@aws-cdk/aws-apprunner'; ``` ## Introduction @@ -46,8 +46,8 @@ The `Service` construct allows you to create AWS App Runner services with `ECR P To create a `Service` with ECR Public: ```ts -new Service(stack, 'Service', { - source: Source.fromEcrPublic({ +new apprunner.Service(this, 'Service', { + source: apprunner.Source.fromEcrPublic({ imageConfiguration: { port: 8000 }, imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', }), @@ -59,10 +59,12 @@ new Service(stack, 'Service', { To create a `Service` from an existing ECR repository: ```ts -new Service(stack, 'Service', { - source: Source.fromEcr({ +import * as ecr from '@aws-cdk/aws-ecr'; + +new apprunner.Service(this, 'Service', { + source: apprunner.Source.fromEcr({ imageConfiguration: { port: 80 }, - repository: ecr.Repository.fromRepositoryName(stack, 'NginxRepository', 'nginx'), + repository: ecr.Repository.fromRepositoryName(this, 'NginxRepository', 'nginx'), tag: 'latest', }), }); @@ -71,11 +73,13 @@ new Service(stack, 'Service', { To create a `Service` from local docker image asset directory built and pushed to Amazon ECR: ```ts -const imageAsset = new assets.DockerImageAsset(stack, 'ImageAssets', { +import * as assets from '@aws-cdk/aws-ecr-assets'; + +const imageAsset = new assets.DockerImageAsset(this, 'ImageAssets', { directory: path.join(__dirname, './docker.assets'), }); -new Service(stack, 'Service', { - source: Source.fromAsset({ +new apprunner.Service(this, 'Service', { + source: apprunner.Source.fromAsset({ imageConfiguration: { port: 8000 }, asset: imageAsset, }), @@ -89,12 +93,12 @@ To create a `Service` from the GitHub repository, you need to specify an existin See [Managing App Runner connections](https://docs.aws.amazon.com/apprunner/latest/dg/manage-connections.html) for more details. ```ts -new Service(stack, 'Service', { - source: Source.fromGitHub({ +new apprunner.Service(this, 'Service', { + source: apprunner.Source.fromGitHub({ repositoryUrl: 'https://github.com/aws-containers/hello-app-runner', branch: 'main', - configurationSource: ConfigurationSourceType.REPOSITORY, - connection: GitHubConnection.fromConnectionArn('CONNECTION_ARN'), + configurationSource: apprunner.ConfigurationSourceType.REPOSITORY, + connection: apprunner.GitHubConnection.fromConnectionArn('CONNECTION_ARN'), }), }); ``` @@ -102,23 +106,22 @@ new Service(stack, 'Service', { Use `codeConfigurationValues` to override configuration values with the `API` configuration source type. ```ts -new Service(stack, 'Service', { - source: Source.fromGitHub({ +new apprunner.Service(this, 'Service', { + source: apprunner.Source.fromGitHub({ repositoryUrl: 'https://github.com/aws-containers/hello-app-runner', branch: 'main', - configurationSource: ConfigurationSourceType.API, + configurationSource: apprunner.ConfigurationSourceType.API, codeConfigurationValues: { - runtime: Runtime.PYTHON_3, + runtime: apprunner.Runtime.PYTHON_3, port: '8000', startCommand: 'python app.py', buildCommand: 'yum install -y pycairo && pip install -r requirements.txt', }, - connection: GitHubConnection.fromConnectionArn('CONNECTION_ARN'), + connection: apprunner.GitHubConnection.fromConnectionArn('CONNECTION_ARN'), }), }); ``` - ## IAM Roles You are allowed to define `instanceRole` and `accessRole` for the `Service`. diff --git a/packages/@aws-cdk/aws-apprunner/package.json b/packages/@aws-cdk/aws-apprunner/package.json index 8daa46ac8d724..be5b3afe4c9cc 100644 --- a/packages/@aws-cdk/aws-apprunner/package.json +++ b/packages/@aws-cdk/aws-apprunner/package.json @@ -7,6 +7,13 @@ "jsii": { "outdir": "dist", "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + }, "targets": { "dotnet": { "namespace": "Amazon.CDK.AWS.AppRunner", @@ -76,13 +83,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-ecr": "0.0.0", diff --git a/packages/@aws-cdk/aws-apprunner/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-apprunner/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..b74a57c217fbd --- /dev/null +++ b/packages/@aws-cdk/aws-apprunner/rosetta/default.ts-fixture @@ -0,0 +1,12 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as apprunner from '@aws-cdk/aws-apprunner'; +import * as path from 'path'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apprunner/test/service.test.ts b/packages/@aws-cdk/aws-apprunner/test/service.test.ts index 412ff09cd14e9..2d5285d06f7fa 100644 --- a/packages/@aws-cdk/aws-apprunner/test/service.test.ts +++ b/packages/@aws-cdk/aws-apprunner/test/service.test.ts @@ -1,5 +1,4 @@ import * as path from 'path'; -import '@aws-cdk/assert-internal/jest'; import { Template } from '@aws-cdk/assertions'; import * as ecr from '@aws-cdk/aws-ecr'; import * as ecr_assets from '@aws-cdk/aws-ecr-assets'; diff --git a/packages/@aws-cdk/aws-appstream/package.json b/packages/@aws-cdk/aws-appstream/package.json index 07cfbc2facf63..0069735f46906 100644 --- a/packages/@aws-cdk/aws-appstream/package.json +++ b/packages/@aws-cdk/aws-appstream/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -77,7 +84,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-appsync/lib/data-source.ts b/packages/@aws-cdk/aws-appsync/lib/data-source.ts index b7570be255fac..c89479c4ef69b 100644 --- a/packages/@aws-cdk/aws-appsync/lib/data-source.ts +++ b/packages/@aws-cdk/aws-appsync/lib/data-source.ts @@ -111,7 +111,8 @@ export abstract class BaseDataSource extends CoreConstruct { if (extended.type !== 'NONE') { this.serviceRole = props.serviceRole || new Role(this, 'ServiceRole', { assumedBy: new ServicePrincipal('appsync') }); } - const name = props.name ?? id; + // Replace unsupported characters from DataSource name. The only allowed pattern is: {[_A-Za-z][_0-9A-Za-z]*} + const name = (props.name ?? id).replace(/[\W]+/g, ''); this.ds = new CfnDataSource(this, 'Resource', { apiId: props.api.apiId, name: name, diff --git a/packages/@aws-cdk/aws-appsync/package.json b/packages/@aws-cdk/aws-appsync/package.json index 14015b3ead955..f158ec0f97599 100644 --- a/packages/@aws-cdk/aws-appsync/package.json +++ b/packages/@aws-cdk/aws-appsync/package.json @@ -85,8 +85,8 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-cognito": "0.0.0", diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-lambda.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-lambda.test.ts index b714b0f28cef9..786937eaee629 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-lambda.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-lambda.test.ts @@ -65,6 +65,42 @@ describe('Lambda Data Source configuration', () => { }); }); + test('appsync sanitized datasource name from unsupported characters', () => { + const badCharacters = [...'!@#$%^&*()+-=[]{}\\|;:\'",<>?/']; + + badCharacters.forEach((badCharacter) => { + // WHEN + const newStack = new cdk.Stack(); + const graphqlapi = new appsync.GraphqlApi(newStack, 'baseApi', { + name: 'api', + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), + }); + const dummyFunction = new lambda.Function(newStack, 'func', { + code: lambda.Code.fromAsset(path.join(__dirname, 'verify/iam-query')), + handler: 'iam-query.handler', + runtime: lambda.Runtime.NODEJS_12_X, + }); + graphqlapi.addLambdaDataSource(`data-${badCharacter}-source`, dummyFunction); + + // THEN + Template.fromStack(newStack).hasResourceProperties('AWS::AppSync::DataSource', { + Type: 'AWS_LAMBDA', + Name: 'datasource', + }); + }); + }); + + test('appsync leaves underscore untouched in datasource name', () => { + // WHEN + api.addLambdaDataSource('data_source', func); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppSync::DataSource', { + Type: 'AWS_LAMBDA', + Name: 'data_source', + }); + }); + test('appsync errors when creating multiple lambda data sources with no configuration', () => { // THEN expect(() => { diff --git a/packages/@aws-cdk/aws-aps/package.json b/packages/@aws-cdk/aws-aps/package.json index ce01bfd038a3e..7fbef265f7a3b 100644 --- a/packages/@aws-cdk/aws-aps/package.json +++ b/packages/@aws-cdk/aws-aps/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-aps", "module": "aws_cdk.aws_aps" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-athena/package.json b/packages/@aws-cdk/aws-athena/package.json index 5c2317020e5d9..134a9cdbe2bdf 100644 --- a/packages/@aws-cdk/aws-athena/package.json +++ b/packages/@aws-cdk/aws-athena/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "cdk-build": { "cloudformation": "AWS::Athena", @@ -77,8 +84,8 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-auditmanager/package.json b/packages/@aws-cdk/aws-auditmanager/package.json index 72056b745c96a..16fe6cc71fd69 100644 --- a/packages/@aws-cdk/aws-auditmanager/package.json +++ b/packages/@aws-cdk/aws-auditmanager/package.json @@ -28,6 +28,13 @@ "distName": "aws-cdk.aws-auditmanager", "module": "aws_cdk.aws_auditmanager" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-autoscaling-common/package.json b/packages/@aws-cdk/aws-autoscaling-common/package.json index 8272f5d3ac15e..69fd192599983 100644 --- a/packages/@aws-cdk/aws-autoscaling-common/package.json +++ b/packages/@aws-cdk/aws-autoscaling-common/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -64,13 +71,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "fast-check": "^2.20.0", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "fast-check": "^2.22.0", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-iam": "0.0.0", diff --git a/packages/@aws-cdk/aws-autoscaling-common/test/intervals.test.ts b/packages/@aws-cdk/aws-autoscaling-common/test/intervals.test.ts index 0b0b9b996a1bb..e36923299003f 100644 --- a/packages/@aws-cdk/aws-autoscaling-common/test/intervals.test.ts +++ b/packages/@aws-cdk/aws-autoscaling-common/test/intervals.test.ts @@ -14,8 +14,6 @@ describe('intervals', () => { { lower: 80, upper: 90, change: +1 }, { lower: 90, upper: Infinity, change: +2 }, ]); - - }); test('test interval completion', () => { @@ -37,8 +35,6 @@ describe('intervals', () => { { lower: 65, upper: 85, change: +3 }, { lower: 85, upper: Infinity, change: undefined }, ]); - - }); test('bounds propagation fails if middle boundary missing', () => { @@ -48,8 +44,6 @@ describe('intervals', () => { { upper: 20, change: -1 }, ], false); }).toThrow(); - - }); test('lower alarm index is lower than higher alarm index', () => { @@ -63,8 +57,6 @@ describe('intervals', () => { || alarms.lowerAlarmIntervalIndex < alarms.upperAlarmIntervalIndex); }, )); - - }); test('never pick undefined intervals for relative alarms', () => { @@ -77,8 +69,6 @@ describe('intervals', () => { && (alarms.upperAlarmIntervalIndex === undefined || intervals[alarms.upperAlarmIntervalIndex].change !== undefined); }, )); - - }); test('pick intervals on either side of the undefined interval, if present', () => { @@ -93,8 +83,6 @@ describe('intervals', () => { return (alarms.lowerAlarmIntervalIndex === i - 1 && alarms.upperAlarmIntervalIndex === i + 1); }, )); - - }); test('no picking upper bound infinity for lower alarm', () => { @@ -107,8 +95,6 @@ describe('intervals', () => { return intervals[alarms.lowerAlarmIntervalIndex!].upper !== Infinity; }, )); - - }); test('no picking lower bound 0 for upper alarm', () => { @@ -121,8 +107,6 @@ describe('intervals', () => { return intervals[alarms.upperAlarmIntervalIndex!].lower !== 0; }, )); - - }); }); diff --git a/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json b/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json index fa5e4e3143807..164d2d933695c 100644 --- a/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json +++ b/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -64,14 +71,14 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-autoscaling": "0.0.0", diff --git a/packages/@aws-cdk/aws-autoscaling-hooktargets/test/hooks.test.ts b/packages/@aws-cdk/aws-autoscaling-hooktargets/test/hooks.test.ts index 4615c45913b44..413142423f7b5 100644 --- a/packages/@aws-cdk/aws-autoscaling-hooktargets/test/hooks.test.ts +++ b/packages/@aws-cdk/aws-autoscaling-hooktargets/test/hooks.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { arrayWith } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; @@ -27,7 +26,7 @@ describe('given an AutoScalingGroup and no role', () => { }); afterEach(() => { - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Version: '2012-10-17', Statement: [ @@ -54,8 +53,8 @@ describe('given an AutoScalingGroup and no role', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { 'Fn::GetAtt': ['Queue4A7E3555', 'Arn'] } }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { 'Fn::GetAtt': ['Queue4A7E3555', 'Arn'] } }); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -95,8 +94,8 @@ describe('given an AutoScalingGroup and no role', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { Ref: 'TopicBFC7AF6E' } }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { Ref: 'TopicBFC7AF6E' } }); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -133,13 +132,13 @@ describe('given an AutoScalingGroup and no role', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { Ref: 'ASGLifecycleHookTransTopic9B0D4842' } }); - expect(stack).toHaveResource('AWS::SNS::Subscription', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { Ref: 'ASGLifecycleHookTransTopic9B0D4842' } }); + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { Protocol: 'lambda', TopicArn: { Ref: 'ASGLifecycleHookTransTopic9B0D4842' }, Endpoint: { 'Fn::GetAtt': ['Fn9270CBC0', 'Arn'] }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -177,7 +176,7 @@ describe('given an AutoScalingGroup and no role', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::SNS::Topic', { + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Topic', { KmsMasterKeyId: { 'Fn::GetAtt': [ 'keyFEDD6EC0', @@ -185,9 +184,9 @@ describe('given an AutoScalingGroup and no role', () => { ], }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith( + Statement: Match.arrayWith([ { Effect: 'Allow', Action: [ @@ -201,7 +200,7 @@ describe('given an AutoScalingGroup and no role', () => { ], }, }, - ), + ]), }, }); }); @@ -223,7 +222,7 @@ describe('given an AutoScalingGroup and a role', () => { }); afterEach(() => { - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Version: '2012-10-17', Statement: [ @@ -253,7 +252,7 @@ describe('given an AutoScalingGroup and a role', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { 'Fn::GetAtt': ['Queue4A7E3555', 'Arn'] } }); + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { 'Fn::GetAtt': ['Queue4A7E3555', 'Arn'] } }); }); test('can use topic as hook target with a role', () => { @@ -271,8 +270,8 @@ describe('given an AutoScalingGroup and a role', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { Ref: 'TopicBFC7AF6E' } }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { Ref: 'TopicBFC7AF6E' } }); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -313,13 +312,13 @@ describe('given an AutoScalingGroup and a role', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { Ref: 'ASGLifecycleHookTransTopic9B0D4842' } }); - expect(stack).toHaveResource('AWS::SNS::Subscription', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { Ref: 'ASGLifecycleHookTransTopic9B0D4842' } }); + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { Protocol: 'lambda', TopicArn: { Ref: 'ASGLifecycleHookTransTopic9B0D4842' }, Endpoint: { 'Fn::GetAtt': ['Fn9270CBC0', 'Arn'] }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { diff --git a/packages/@aws-cdk/aws-autoscaling/package.json b/packages/@aws-cdk/aws-autoscaling/package.json index 86e27d409f981..09a081cdbd9fe 100644 --- a/packages/@aws-cdk/aws-autoscaling/package.json +++ b/packages/@aws-cdk/aws-autoscaling/package.json @@ -79,15 +79,15 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-autoscaling-common": "0.0.0", diff --git a/packages/@aws-cdk/aws-autoscaling/test/aspects/require-imdsv2-aspect.test.ts b/packages/@aws-cdk/aws-autoscaling/test/aspects/require-imdsv2-aspect.test.ts index 22a58f097a98b..5fa4210d2a6f5 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/aspects/require-imdsv2-aspect.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/aspects/require-imdsv2-aspect.test.ts @@ -1,8 +1,4 @@ -import { - expect as expectCDK, - haveResourceLike, -} from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import { @@ -37,11 +33,12 @@ describe('AutoScalingGroupRequireImdsv2Aspect', () => { cdk.Aspects.of(stack).add(aspect); // THEN - expectCDK(stack).notTo(haveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', Match.not({ MetadataOptions: { HttpTokens: 'required', }, })); + expect(asg.node.metadataEntry).toContainEqual({ data: expect.stringContaining('CfnLaunchConfiguration.MetadataOptions field is a CDK token.'), type: 'aws:cdk:warning', @@ -62,11 +59,11 @@ describe('AutoScalingGroupRequireImdsv2Aspect', () => { cdk.Aspects.of(stack).add(aspect); // THEN - expectCDK(stack).to(haveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { MetadataOptions: { HttpTokens: 'required', }, - })); + }); }); }); 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 8ca98c19deeb8..945339ca86f5b 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,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT, InspectionFailure, ResourcePart } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; @@ -22,7 +21,7 @@ describe('auto scaling group', () => { vpc, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Parameters': { 'SsmParameterValueawsserviceamiamazonlinuxlatestamznamihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter': { 'Type': 'AWS::SSM::Parameter::Value', @@ -136,8 +135,6 @@ describe('auto scaling group', () => { }, }, }); - - }); test('can set minCapacity, maxCapacity, desiredCapacity to 0', () => { @@ -153,14 +150,11 @@ describe('auto scaling group', () => { desiredCapacity: 0, }); - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MinSize: '0', MaxSize: '0', DesiredCapacity: '0', - }, - ); - - + }); }); test('validation is not performed when using Tokens', () => { @@ -177,14 +171,11 @@ describe('auto scaling group', () => { }); // THEN: no exception - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MinSize: '5', MaxSize: '1', DesiredCapacity: '20', - }, - ); - - + }); }); test('userdata can be overridden by image', () => { @@ -206,8 +197,6 @@ describe('auto scaling group', () => { // THEN expect(asg.userData.render()).toEqual('#!/bin/bash\nit me!'); - - }); test('userdata can be overridden at ASG directly', () => { @@ -233,8 +222,6 @@ describe('auto scaling group', () => { // THEN expect(asg.userData.render()).toEqual('#!/bin/bash\nno me!'); - - }); test('can specify only min capacity', () => { @@ -251,13 +238,10 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MinSize: '10', MaxSize: '10', - }, - ); - - + }); }); test('can specify only max capacity', () => { @@ -274,13 +258,10 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MinSize: '1', MaxSize: '10', - }, - ); - - + }); }); test('can specify only desiredCount', () => { @@ -297,14 +278,11 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MinSize: '1', MaxSize: '10', DesiredCapacity: '10', - }, - ); - - + }); }); test('addToRolePolicy can be used to add statements to the role policy', () => { @@ -322,7 +300,7 @@ describe('auto scaling group', () => { resources: ['*'], })); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -352,7 +330,7 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { UpdatePolicy: { AutoScalingReplacingUpdate: { WillReplace: true, @@ -363,9 +341,7 @@ describe('auto scaling group', () => { MinSuccessfulInstancesPercent: 50, }, }, - }, ResourcePart.CompleteDefinition); - - + }); }); testDeprecated('can configure rolling update', () => { @@ -386,7 +362,7 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { UpdatePolicy: { 'AutoScalingRollingUpdate': { 'MinSuccessfulInstancesPercent': 50, @@ -395,9 +371,7 @@ describe('auto scaling group', () => { 'SuspendProcesses': ['HealthCheck', 'ReplaceUnhealthy', 'AZRebalance', 'AlarmNotification', 'ScheduledActions'], }, }, - }, ResourcePart.CompleteDefinition); - - + }); }); testDeprecated('can configure resource signals', () => { @@ -415,16 +389,14 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { CreationPolicy: { ResourceSignal: { Count: 5, Timeout: 'PT11M6S', }, }, - }, ResourcePart.CompleteDefinition); - - + }); }); test('can configure EC2 health check', () => { @@ -441,11 +413,9 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { HealthCheckType: 'EC2', }); - - }); test('can configure EBS health check', () => { @@ -462,12 +432,10 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { HealthCheckType: 'ELB', HealthCheckGracePeriod: 900, }); - - }); test('can add Security Group to Fleet', () => { @@ -482,7 +450,7 @@ describe('auto scaling group', () => { vpc, }); asg.addSecurityGroup(mockSecurityGroup(stack)); - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { SecurityGroups: [ { 'Fn::GetAtt': [ @@ -493,7 +461,6 @@ describe('auto scaling group', () => { 'most-secure', ], }); - }); test('can set tags', () => { @@ -514,7 +481,7 @@ describe('auto scaling group', () => { cdk.Tags.of(asg).add('notsuper', 'caramel', { applyToLaunchedInstances: false }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { Tags: [ { Key: 'Name', @@ -533,7 +500,6 @@ describe('auto scaling group', () => { }, ], }); - }); test('allows setting spot price', () => { @@ -552,11 +518,9 @@ describe('auto scaling group', () => { // THEN expect(asg.spotPrice).toEqual('0.05'); - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { SpotPrice: '0.05', }); - - }); test('allows association of public IP address', () => { @@ -578,11 +542,9 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { AssociatePublicIpAddress: true, - }, - ); - + }); }); test('association of public IP address requires public subnet', () => { @@ -602,7 +564,6 @@ describe('auto scaling group', () => { associatePublicIpAddress: true, }); }).toThrow(); - }); test('allows disassociation of public IP address', () => { @@ -622,11 +583,9 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { AssociatePublicIpAddress: false, - }, - ); - + }); }); test('does not specify public IP address association by default', () => { @@ -645,16 +604,9 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', (resource: any, errors: InspectionFailure) => { - for (const key of Object.keys(resource)) { - if (key === 'AssociatePublicIpAddress') { - errors.failureReason = 'Has AssociatePublicIpAddress'; - return false; - } - } - return true; + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { + AssociatePublicIpAddress: Match.absent(), }); - }); test('an existing security group can be specified instead of auto-created', () => { @@ -672,11 +624,9 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { SecurityGroups: ['most-secure'], - }, - ); - + }); }); test('an existing role can be specified instead of auto-created', () => { @@ -695,10 +645,9 @@ describe('auto scaling group', () => { // THEN expect(asg.role).toEqual(importedRole); - expect(stack).toHaveResource('AWS::IAM::InstanceProfile', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::InstanceProfile', { 'Roles': ['HelloDude'], }); - }); test('defaultChild is available on an ASG', () => { @@ -713,8 +662,6 @@ describe('auto scaling group', () => { // THEN expect(asg.node.defaultChild instanceof autoscaling.CfnAutoScalingGroup).toEqual(true); - - }); test('can set blockDeviceMappings', () => { @@ -755,7 +702,7 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { BlockDeviceMappings: [ { DeviceName: 'ebs', @@ -766,7 +713,7 @@ describe('auto scaling group', () => { VolumeSize: 15, VolumeType: 'io1', }, - NoDevice: ABSENT, + NoDevice: Match.absent(), }, { DeviceName: 'ebs-snapshot', @@ -776,12 +723,12 @@ describe('auto scaling group', () => { VolumeSize: 500, VolumeType: 'sc1', }, - NoDevice: ABSENT, + NoDevice: Match.absent(), }, { DeviceName: 'ephemeral', VirtualName: 'ephemeral0', - NoDevice: ABSENT, + NoDevice: Match.absent(), }, { DeviceName: 'disabled', @@ -793,8 +740,6 @@ describe('auto scaling group', () => { }, ], }); - - }); test('can configure maxInstanceLifetime', () => { @@ -809,11 +754,9 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { 'MaxInstanceLifetime': 604800, }); - - }); test('throws if maxInstanceLifetime < 7 days', () => { @@ -830,8 +773,6 @@ describe('auto scaling group', () => { maxInstanceLifetime: cdk.Duration.days(6), }); }).toThrow(/maxInstanceLifetime must be between 7 and 365 days \(inclusive\)/); - - }); test('throws if maxInstanceLifetime > 365 days', () => { @@ -848,8 +789,6 @@ describe('auto scaling group', () => { maxInstanceLifetime: cdk.Duration.days(366), }); }).toThrow(/maxInstanceLifetime must be between 7 and 365 days \(inclusive\)/); - - }); test('can configure instance monitoring', () => { @@ -866,10 +805,9 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { InstanceMonitoring: false, }); - }); test('instance monitoring defaults to absent', () => { @@ -885,10 +823,9 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { - InstanceMonitoring: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { + InstanceMonitoring: Match.absent(), }); - }); test('throws if ephemeral volumeIndex < 0', () => { @@ -908,8 +845,6 @@ describe('auto scaling group', () => { }], }); }).toThrow(/volumeIndex must be a number starting from 0/); - - }); test('throws if volumeType === IO1 without iops', () => { @@ -933,8 +868,6 @@ describe('auto scaling group', () => { }], }); }).toThrow(/ops property is required with volumeType: EbsDeviceVolumeType.IO1/); - - }); test('warning if iops without volumeType', () => { @@ -959,8 +892,6 @@ describe('auto scaling group', () => { // THEN expect(asg.node.metadataEntry[0].type).toEqual(cxschema.ArtifactMetadataEntryType.WARN); expect(asg.node.metadataEntry[0].data).toEqual('iops will be ignored without volumeType: EbsDeviceVolumeType.IO1'); - - }); test('warning if iops and volumeType !== IO1', () => { @@ -986,8 +917,6 @@ describe('auto scaling group', () => { // THEN expect(asg.node.metadataEntry[0].type).toEqual(cxschema.ArtifactMetadataEntryType.WARN); expect(asg.node.metadataEntry[0].data).toEqual('iops will be ignored without volumeType: EbsDeviceVolumeType.IO1'); - - }); test('step scaling on metric', () => { @@ -1015,15 +944,13 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ComparisonOperator: 'LessThanOrEqualToThreshold', EvaluationPeriods: 1, MetricName: 'Metric', Namespace: 'Test', Period: 300, }); - - }); test('step scaling on MathExpression', () => { @@ -1056,11 +983,11 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).not.toHaveResource('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', Match.not({ Period: 60, - }); + })); - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { 'ComparisonOperator': 'LessThanOrEqualToThreshold', 'EvaluationPeriods': 1, 'Metrics': [ @@ -1083,8 +1010,6 @@ describe('auto scaling group', () => { ], 'Threshold': 49, }); - - }); test('test GroupMetrics.all(), adds a single MetricsCollection with no Metrics specified', () => { @@ -1100,15 +1025,14 @@ describe('auto scaling group', () => { }); // Then - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MetricsCollection: [ { Granularity: '1Minute', - Metrics: ABSENT, + Metrics: Match.absent(), }, ], }); - }); test('test can specify a subset of group metrics', () => { @@ -1135,7 +1059,7 @@ describe('auto scaling group', () => { }); // Then - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MetricsCollection: [ { Granularity: '1Minute', @@ -1146,7 +1070,6 @@ describe('auto scaling group', () => { }, ], }); - }); test('test deduplication of group metrics ', () => { @@ -1165,7 +1088,7 @@ describe('auto scaling group', () => { }); // Then - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MetricsCollection: [ { Granularity: '1Minute', @@ -1173,7 +1096,6 @@ describe('auto scaling group', () => { }, ], }); - }); test('allow configuring notifications', () => { @@ -1200,7 +1122,7 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { NotificationConfigurations: [ { TopicARN: { Ref: 'MyTopic86869434' }, @@ -1216,10 +1138,7 @@ describe('auto scaling group', () => { ], }, ], - }, - ); - - + }); }); testDeprecated('throw if notification and notificationsTopics are both configured', () => { @@ -1240,7 +1159,6 @@ describe('auto scaling group', () => { }], }); }).toThrow('Cannot set \'notificationsTopic\' and \'notifications\', \'notificationsTopic\' is deprecated use \'notifications\' instead'); - }); test('notificationTypes default includes all non test NotificationType', () => { @@ -1262,7 +1180,7 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { NotificationConfigurations: [ { TopicARN: { Ref: 'MyTopic86869434' }, @@ -1274,10 +1192,7 @@ describe('auto scaling group', () => { ], }, ], - }, - ); - - + }); }); testDeprecated('setting notificationTopic configures all non test NotificationType', () => { @@ -1295,7 +1210,7 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { NotificationConfigurations: [ { TopicARN: { Ref: 'MyTopic86869434' }, @@ -1307,10 +1222,7 @@ describe('auto scaling group', () => { ], }, ], - }, - ); - - + }); }); test('NotificationTypes.ALL includes all non test NotificationType', () => { @@ -1333,11 +1245,9 @@ describe('auto scaling group', () => { // THEN expect(asg.areNewInstancesProtectedFromScaleIn()).toEqual(true); - expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { NewInstancesProtectedFromScaleIn: true, }); - - }); test('Can protect new instances from scale-in via setter', () => { @@ -1355,11 +1265,9 @@ describe('auto scaling group', () => { // THEN expect(asg.areNewInstancesProtectedFromScaleIn()).toEqual(true); - expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { NewInstancesProtectedFromScaleIn: true, }); - - }); test('requires imdsv2', () => { @@ -1376,7 +1284,7 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { MetadataOptions: { HttpTokens: 'required', }, @@ -1400,7 +1308,7 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { TerminationPolicies: [ 'OldestInstance', 'Default', @@ -1433,7 +1341,7 @@ test('Can set autoScalingGroupName', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { AutoScalingGroupName: 'MyAsg', }); }); @@ -1470,7 +1378,7 @@ test('can use Vpc imported from unparseable list tokens', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { VPCZoneIdentifier: { 'Fn::Split': [',', { 'Fn::ImportValue': 'myPrivateSubnetIds' }], }, 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 2fd252e5e459d..1e34f43260c62 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/cfn-init.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/cfn-init.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { anything, arrayWith, expect, haveResourceLike, ResourcePart } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import { Duration, Stack } from '@aws-cdk/core'; import * as autoscaling from '../lib'; @@ -37,13 +36,13 @@ test('Signals.waitForAll uses desiredCapacity if available', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { CreationPolicy: { ResourceSignal: { Count: 5, }, }, - }, ResourcePart.CompleteDefinition)); + }); }); test('Signals.waitForAll uses minCapacity if desiredCapacity is not available', () => { @@ -55,13 +54,13 @@ test('Signals.waitForAll uses minCapacity if desiredCapacity is not available', }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { CreationPolicy: { ResourceSignal: { Count: 2, }, }, - }, ResourcePart.CompleteDefinition)); + }); }); test('Signals.waitForMinCapacity uses minCapacity', () => { @@ -72,13 +71,13 @@ test('Signals.waitForMinCapacity uses minCapacity', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { CreationPolicy: { ResourceSignal: { Count: 2, }, }, - }, ResourcePart.CompleteDefinition)); + }); }); test('Signals.waitForCount uses given number', () => { @@ -89,13 +88,13 @@ test('Signals.waitForCount uses given number', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { CreationPolicy: { ResourceSignal: { Count: 10, }, }, - }, ResourcePart.CompleteDefinition)); + }); }); test('When signals are given appropriate IAM policy is added', () => { @@ -106,15 +105,15 @@ test('When signals are given appropriate IAM policy is added', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Action: 'cloudformation:SignalResource', Effect: 'Allow', Resource: { Ref: 'AWS::StackId' }, - }), + }]), }, - })); + }); }); test('UpdatePolicy.rollingUpdate() still correctly inserts IgnoreUnmodifiedGroupSizeProperties', () => { @@ -125,13 +124,13 @@ test('UpdatePolicy.rollingUpdate() still correctly inserts IgnoreUnmodifiedGroup }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { UpdatePolicy: { AutoScalingScheduledAction: { IgnoreUnmodifiedGroupSizeProperties: true, }, }, - }, ResourcePart.CompleteDefinition)); + }); }); test('UpdatePolicy.rollingUpdate() with Signals uses those defaults', () => { @@ -146,7 +145,7 @@ test('UpdatePolicy.rollingUpdate() with Signals uses those defaults', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { CreationPolicy: { AutoScalingCreationPolicy: { MinSuccessfulInstancesPercent: 50, @@ -163,7 +162,7 @@ test('UpdatePolicy.rollingUpdate() with Signals uses those defaults', () => { WaitOnResourceSignals: true, }, }, - }, ResourcePart.CompleteDefinition)); + }); }); test('UpdatePolicy.rollingUpdate() without Signals', () => { @@ -174,12 +173,12 @@ test('UpdatePolicy.rollingUpdate() without Signals', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { UpdatePolicy: { AutoScalingRollingUpdate: { }, }, - }, ResourcePart.CompleteDefinition)); + }); }); test('UpdatePolicy.replacingUpdate() renders correct UpdatePolicy', () => { @@ -190,13 +189,13 @@ test('UpdatePolicy.replacingUpdate() renders correct UpdatePolicy', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { UpdatePolicy: { AutoScalingReplacingUpdate: { WillReplace: true, }, }, - }, ResourcePart.CompleteDefinition)); + }); }); test('Using init config in ASG leads to default updatepolicy', () => { @@ -210,11 +209,11 @@ test('Using init config in ASG leads to default updatepolicy', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { UpdatePolicy: { - AutoScalingRollingUpdate: anything(), + AutoScalingRollingUpdate: Match.anyValue(), }, - }, ResourcePart.CompleteDefinition)); + }); }); test('Using init config in ASG leads to correct UserData and permissions', () => { @@ -228,7 +227,7 @@ test('Using init config in ASG leads to correct UserData and permissions', () => }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { UserData: { 'Fn::Base64': { 'Fn::Join': ['', [ @@ -244,14 +243,15 @@ test('Using init config in ASG leads to correct UserData and permissions', () => ]], }, }, - })); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Action: ['cloudformation:DescribeStackResource', 'cloudformation:SignalResource'], Effect: 'Allow', Resource: { Ref: 'AWS::StackId' }, - }), + }]), }, - })); + }); }); diff --git a/packages/@aws-cdk/aws-autoscaling/test/lifecyclehooks.test.ts b/packages/@aws-cdk/aws-autoscaling/test/lifecyclehooks.test.ts index a1f3717f91042..2cecb8b227107 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/lifecyclehooks.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/lifecyclehooks.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; @@ -20,22 +19,22 @@ describe('lifecycle hooks', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LifecycleHook', { LifecycleTransition: 'autoscaling:EC2_INSTANCE_LAUNCHING', DefaultResult: 'ABANDON', NotificationTargetARN: 'target:arn', }); // Lifecycle Hook has a dependency on the policy object - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::LifecycleHook', { DependsOn: [ 'ASGLifecycleHookTransitionRoleDefaultPolicy2E50C7DB', 'ASGLifecycleHookTransitionRole3AAA6BB7', ], - }, ResourcePart.CompleteDefinition); + }); // A default role is provided - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Version: '2012-10-17', Statement: [ @@ -51,7 +50,7 @@ describe('lifecycle hooks', () => { }); // FakeNotificationTarget.bind() was executed - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -78,13 +77,13 @@ test('we can add a lifecycle hook to an ASG with no role and with no notificatio }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LifecycleHook', { LifecycleTransition: 'autoscaling:EC2_INSTANCE_LAUNCHING', DefaultResult: 'ABANDON', }); // A default role is NOT provided - expect(stack).not.toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', Match.not({ AssumeRolePolicyDocument: { Version: '2012-10-17', Statement: [ @@ -97,21 +96,10 @@ test('we can add a lifecycle hook to an ASG with no role and with no notificatio }, ], }, - }); + })); // FakeNotificationTarget.bind() was NOT executed - expect(stack).not.toHaveResource('AWS::IAM::Policy', { - PolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Action: 'action:Work', - Effect: 'Allow', - Resource: '*', - }, - ], - }, - }); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 0); }); test('we can add a lifecycle hook to an ASG with a role and with a notificationTargetArn', () => { @@ -131,14 +119,14 @@ test('we can add a lifecycle hook to an ASG with a role and with a notificationT }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: 'target:arn', LifecycleTransition: 'autoscaling:EC2_INSTANCE_LAUNCHING', DefaultResult: 'ABANDON', }); // the provided role (myrole), not the default role, is used - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Version: '2012-10-17', Statement: [ diff --git a/packages/@aws-cdk/aws-autoscaling/test/scaling.test.ts b/packages/@aws-cdk/aws-autoscaling/test/scaling.test.ts index a497efab73135..d427b6afa1ff8 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/scaling.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/scaling.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; @@ -24,15 +23,13 @@ describe('scaling', () => { }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'ASGAverageCPUUtilization' }, TargetValue: 30, }, - })); - - + }); }); test('network ingress', () => { @@ -46,15 +43,13 @@ describe('scaling', () => { }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'ASGAverageNetworkIn' }, TargetValue: 100, }, - })); - - + }); }); test('network egress', () => { @@ -68,15 +63,13 @@ describe('scaling', () => { }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'ASGAverageNetworkOut' }, TargetValue: 100, }, - })); - - + }); }); test('request count per second', () => { @@ -103,7 +96,7 @@ describe('scaling', () => { ], }; - expect(stack).to(haveResource('AWS::AutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingConfiguration: { TargetValue: 600, @@ -122,9 +115,7 @@ describe('scaling', () => { }, }, }, - })); - - + }); }); test('request count per minute', () => { @@ -151,7 +142,7 @@ describe('scaling', () => { ], }; - expect(stack).to(haveResource('AWS::AutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingConfiguration: { TargetValue: 10, @@ -170,9 +161,7 @@ describe('scaling', () => { }, }, }, - })); - - + }); }); test('custom metric', () => { @@ -193,7 +182,7 @@ describe('scaling', () => { }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingConfiguration: { CustomizedMetricSpecification: { @@ -204,9 +193,7 @@ describe('scaling', () => { }, TargetValue: 2, }, - })); - - + }); }); }); @@ -231,7 +218,7 @@ describe('scaling', () => { }); // THEN: scaling in policy - expect(stack).to(haveResource('AWS::AutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScalingPolicy', { MetricAggregationType: 'Average', PolicyType: 'StepScaling', StepAdjustments: [ @@ -245,17 +232,17 @@ describe('scaling', () => { ScalingAdjustment: -2, }, ], - })); + }); - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', Threshold: 3, AlarmActions: [{ Ref: 'FixtureASGMetricUpperPolicyC464CAFB' }], AlarmDescription: 'Upper threshold scaling alarm', - })); + }); // THEN: scaling out policy - expect(stack).to(haveResource('AWS::AutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScalingPolicy', { MetricAggregationType: 'Average', PolicyType: 'StepScaling', StepAdjustments: [ @@ -264,16 +251,14 @@ describe('scaling', () => { ScalingAdjustment: 1, }, ], - })); + }); - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ComparisonOperator: 'LessThanOrEqualToThreshold', Threshold: 2, AlarmActions: [{ Ref: 'FixtureASGMetricLowerPolicy4A1CDE42' }], AlarmDescription: 'Lower threshold scaling alarm', - })); - - + }); }); }); @@ -293,11 +278,12 @@ test('step scaling from percentile metric', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScalingPolicy', { PolicyType: 'StepScaling', MetricAggregationType: 'Average', - })); - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + }); + + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 1, AlarmActions: [ @@ -307,7 +293,7 @@ test('step scaling from percentile metric', () => { MetricName: 'Metric', Namespace: 'Test', Threshold: 100, - })); + }); }); test('step scaling with evaluation period configured', () => { @@ -328,18 +314,19 @@ test('step scaling with evaluation period configured', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScalingPolicy', { PolicyType: 'StepScaling', MetricAggregationType: 'Maximum', - })); - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + }); + + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 10, ExtendedStatistic: 'p99', MetricName: 'Metric', Namespace: 'Test', Threshold: 100, - })); + }); }); class ASGFixture extends Construct { 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 57789d5ae36fe..8579099ffd6cf 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/scheduled-action.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/scheduled-action.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { expect, haveResource, MatchStyle } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import { describeDeprecated } from '@aws-cdk/cdk-build-tools'; import * as cdk from '@aws-cdk/core'; @@ -19,12 +18,10 @@ describeDeprecated('scheduled action', () => { }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::ScheduledAction', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScheduledAction', { Recurrence: '0 8 * * *', MinSize: 10, - })); - - + }); }); test('correctly formats date objects', () => { @@ -40,11 +37,9 @@ describeDeprecated('scheduled action', () => { }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::ScheduledAction', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScheduledAction', { StartTime: '2033-09-10T12:00:00Z', - })); - - + }); }); test('have timezone property', () => { @@ -60,12 +55,11 @@ describeDeprecated('scheduled action', () => { }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::ScheduledAction', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScheduledAction', { MinSize: 12, Recurrence: '0 12 * * *', TimeZone: 'Asia/Seoul', - })); - + }); }); test('autoscaling group has recommended updatepolicy for scheduled actions', () => { @@ -80,7 +74,7 @@ describeDeprecated('scheduled action', () => { }); // THEN - expect(stack).toMatch({ + Template.fromStack(stack).templateMatches({ Resources: { ASG46ED3070: { Type: 'AWS::AutoScaling::AutoScalingGroup', @@ -124,9 +118,7 @@ describeDeprecated('scheduled action', () => { Default: '/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2', }, }, - }, MatchStyle.SUPERSET); - - + }); }); }); diff --git a/packages/@aws-cdk/aws-autoscalingplans/package.json b/packages/@aws-cdk/aws-autoscalingplans/package.json index 5cc5af877100b..26c9be8cc368f 100644 --- a/packages/@aws-cdk/aws-autoscalingplans/package.json +++ b/packages/@aws-cdk/aws-autoscalingplans/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -76,7 +83,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-backup/README.md b/packages/@aws-cdk/aws-backup/README.md index 6eaae31c19aa2..5cf5bc4c1aa1a 100644 --- a/packages/@aws-cdk/aws-backup/README.md +++ b/packages/@aws-cdk/aws-backup/README.md @@ -32,7 +32,8 @@ const plan = backup.BackupPlan.dailyWeeklyMonthly5YearRetention(this, 'Plan'); Assigning resources to a plan can be done with `addSelection()`: -```ts fixture=with-plan +```ts +declare const plan: backup.BackupPlan; const myTable = dynamodb.Table.fromTableName(this, 'Table', 'myTableName'); const myCoolConstruct = new Construct(this, 'MyCoolConstruct'); @@ -50,16 +51,17 @@ created for the selection. The `BackupSelection` implements `IGrantable`. To add rules to a plan, use `addRule()`: -```ts fixture=with-plan +```ts +declare const plan: backup.BackupPlan; plan.addRule(new backup.BackupPlanRule({ completionWindow: Duration.hours(2), startWindow: Duration.hours(1), scheduleExpression: events.Schedule.cron({ // Only cron expressions are supported day: '15', hour: '3', - minute: '30' + minute: '30', }), - moveToColdStorageAfter: Duration.days(30) + moveToColdStorageAfter: Duration.days(30), })); ``` @@ -69,7 +71,8 @@ If no value is specified, the retention period is set to 35 days which is the ma Property `moveToColdStorageAfter` must not be specified because PITR does not support this option. This example defines an AWS Backup rule with PITR and a retention period set to 14 days: -```ts fixture=with-plan +```ts +declare const plan: backup.BackupPlan; plan.addRule(new backup.BackupPlanRule({ enableContinuousBackup: true, deleteAfter: Duration.days(14), @@ -78,7 +81,8 @@ plan.addRule(new backup.BackupPlanRule({ Ready-made rules are also available: -```ts fixture=with-plan +```ts +declare const plan: backup.BackupPlan; plan.addRule(backup.BackupPlanRule.daily()); plan.addRule(backup.BackupPlanRule.weekly()); ``` @@ -152,7 +156,7 @@ const vault = new backup.BackupVault(this, 'Vault', { }, }), ], - }); + }), }) ``` @@ -166,8 +170,8 @@ new backup.BackupVault(this, 'Vault', { blockRecoveryPointDeletion: true, }); -const plan = backup.BackupPlan.dailyMonthly1YearRetention(this, 'Plan'); -plan.backupVault.blockRecoveryPointDeletion(); +declare const backupVault: backup.BackupVault; +backupVault.blockRecoveryPointDeletion(); ``` By default access is not restricted. diff --git a/packages/@aws-cdk/aws-backup/package.json b/packages/@aws-cdk/aws-backup/package.json index 0e1e552e15db4..5462be05a3813 100644 --- a/packages/@aws-cdk/aws-backup/package.json +++ b/packages/@aws-cdk/aws-backup/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -79,7 +86,7 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-dynamodb": "0.0.0", diff --git a/packages/@aws-cdk/aws-backup/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-backup/rosetta/default.ts-fixture index cff23bb514119..5f28d8bba18e2 100644 --- a/packages/@aws-cdk/aws-backup/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/aws-backup/rosetta/default.ts-fixture @@ -3,6 +3,8 @@ import { Duration, RemovalPolicy, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import * as backup from '@aws-cdk/aws-backup'; import * as iam from '@aws-cdk/aws-iam'; +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as events from '@aws-cdk/aws-events'; import * as kms from '@aws-cdk/aws-kms'; import * as sns from '@aws-cdk/aws-sns'; diff --git a/packages/@aws-cdk/aws-backup/rosetta/with-plan.ts-fixture b/packages/@aws-cdk/aws-backup/rosetta/with-plan.ts-fixture deleted file mode 100644 index 8dbfd6ac72c89..0000000000000 --- a/packages/@aws-cdk/aws-backup/rosetta/with-plan.ts-fixture +++ /dev/null @@ -1,16 +0,0 @@ -// Fixture with packages imported, but nothing else -import { Duration, RemovalPolicy, Stack } from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import * as backup from '@aws-cdk/aws-backup'; -import * as dynamodb from '@aws-cdk/aws-dynamodb'; -import * as events from '@aws-cdk/aws-events'; - -class Fixture extends Stack { - constructor(scope: Construct, id: string) { - super(scope, id); - - const plan = backup.BackupPlan.dailyWeeklyMonthly5YearRetention(this, 'Plan'); - - /// here - } -} diff --git a/packages/@aws-cdk/aws-batch/package.json b/packages/@aws-cdk/aws-batch/package.json index 772e44b594ca0..0c405c1a720f5 100644 --- a/packages/@aws-cdk/aws-batch/package.json +++ b/packages/@aws-cdk/aws-batch/package.json @@ -84,8 +84,8 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-ec2": "0.0.0", diff --git a/packages/@aws-cdk/aws-budgets/package.json b/packages/@aws-cdk/aws-budgets/package.json index 63db174d263bb..3da84da4dcca0 100644 --- a/packages/@aws-cdk/aws-budgets/package.json +++ b/packages/@aws-cdk/aws-budgets/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -76,7 +83,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-cassandra/package.json b/packages/@aws-cdk/aws-cassandra/package.json index 90f6bc335bd18..5325bf8bfe1bf 100644 --- a/packages/@aws-cdk/aws-cassandra/package.json +++ b/packages/@aws-cdk/aws-cassandra/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-ce/package.json b/packages/@aws-cdk/aws-ce/package.json index 9c72038371be8..ff751b12bb50d 100644 --- a/packages/@aws-cdk/aws-ce/package.json +++ b/packages/@aws-cdk/aws-ce/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-certificatemanager/README.md b/packages/@aws-cdk/aws-certificatemanager/README.md index 331d40ad27276..0983dcc020c1d 100644 --- a/packages/@aws-cdk/aws-certificatemanager/README.md +++ b/packages/@aws-cdk/aws-certificatemanager/README.md @@ -40,9 +40,6 @@ If Amazon Route 53 is your DNS provider for the requested domain, the DNS record created automatically: ```ts -import * as acm from '@aws-cdk/aws-certificatemanager'; -import * as route53 from '@aws-cdk/aws-route53'; - const myHostedZone = new route53.HostedZone(this, 'HostedZone', { zoneName: 'example.com', }); @@ -106,6 +103,7 @@ The `DnsValidatedCertificate` construct exists to facilitate creating these cert Route53-based DNS validation. ```ts +declare const myHostedZone: route53.HostedZone; new acm.DnsValidatedCertificate(this, 'CrossRegionCertificate', { domainName: 'hello.example.com', hostedZone: myHostedZone, @@ -120,10 +118,10 @@ AWS Certificate Manager can create [private certificates](https://docs.aws.amazo ```ts import * as acmpca from '@aws-cdk/aws-acmpca'; -new acm.PrivateCertificate(stack, 'PrivateCertificate', { +new acm.PrivateCertificate(this, 'PrivateCertificate', { domainName: 'test.example.com', subjectAlternativeNames: ['cool.example.com', 'test.example.net'], // optional - certificateAuthority: acmpca.CertificateAuthority.fromCertificateAuthorityArn(stack, 'CA', + certificateAuthority: acmpca.CertificateAuthority.fromCertificateAuthorityArn(this, 'CA', 'arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/023077d8-2bfa-4eb0-8f22-05c96deade77'), }); ``` @@ -134,7 +132,7 @@ If you want to import an existing certificate, you can do so from its ARN: ```ts const arn = 'arn:aws:...'; -const certificate = Certificate.fromCertificateArn(this, 'Certificate', arn); +const certificate = acm.Certificate.fromCertificateArn(this, 'Certificate', arn); ``` ## Sharing between Stacks @@ -152,8 +150,14 @@ An alarm can be created to determine whether a certificate is soon due for renewal ussing the following code: ```ts -const certificate = new Certificate(this, 'Certificate', { /* ... */ }); -certificate.metricDaysToExpiry().createAlarm({ +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; + +declare const myHostedZone: route53.HostedZone; +const certificate = new acm.Certificate(this, 'Certificate', { + domainName: 'hello.example.com', + validation: acm.CertificateValidation.fromDns(myHostedZone), +}); +certificate.metricDaysToExpiry().createAlarm(this, 'Alarm', { comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD, evaluationPeriods: 1, threshold: 45, // Automatic rotation happens between 60 and 45 days before expiry diff --git a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js index 528dd55eb460a..672b5762dbc15 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js +++ b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js @@ -110,26 +110,14 @@ const requestCertificate = async function (requestId, domainName, subjectAlterna console.log('Waiting for ACM to provide DNS records for validation...'); - let records; - for (let attempt = 0; attempt < maxAttempts && !records; attempt++) { + let records = []; + for (let attempt = 0; attempt < maxAttempts && !records.length; attempt++) { const { Certificate } = await acm.describeCertificate({ CertificateArn: reqCertResponse.CertificateArn }).promise(); - const options = Certificate.DomainValidationOptions || []; - // Ensure all records are ready; there is (at least a theory there's) a chance of a partial response here in rare cases. - if (options.length > 0 && options.every(opt => opt && !!opt.ResourceRecord)) { - // some alternative names will produce the same validation record - // as the main domain (eg. example.com + *.example.com) - // filtering duplicates to avoid errors with adding the same record - // to the route53 zone twice - const unique = options - .map((val) => val.ResourceRecord) - .reduce((acc, cur) => { - acc[cur.Name] = cur; - return acc; - }, {}); - records = Object.keys(unique).sort().map(key => unique[key]); - } else { + + records = getDomainValidationRecords(Certificate); + if (!records.length) { // Exponential backoff with jitter based on 200ms base // component of backoff fixed to ensure minimum total wait time on // slow targets. @@ -137,42 +125,13 @@ const requestCertificate = async function (requestId, domainName, subjectAlterna await sleep(random() * base * 50 + base * 150); } } - if (!records) { + if (!records.length) { throw new Error(`Response from describeCertificate did not contain DomainValidationOptions after ${maxAttempts} attempts.`) } - console.log(`Upserting ${records.length} DNS records into zone ${hostedZoneId}:`); - const changeBatch = await route53.changeResourceRecordSets({ - ChangeBatch: { - Changes: records.map((record) => { - console.log(`${record.Name} ${record.Type} ${record.Value}`) - return { - Action: 'UPSERT', - ResourceRecordSet: { - Name: record.Name, - Type: record.Type, - TTL: 60, - ResourceRecords: [{ - Value: record.Value - }] - } - }; - }), - }, - HostedZoneId: hostedZoneId - }).promise(); - - console.log('Waiting for DNS records to commit...'); - await route53.waitFor('resourceRecordSetsChanged', { - // Wait up to 5 minutes - $waiter: { - delay: 30, - maxAttempts: 10 - }, - Id: changeBatch.ChangeInfo.Id - }).promise(); + await commitRoute53Records(route53, records, hostedZoneId); console.log('Waiting for validation...'); await acm.waitFor('certificateValidated', { @@ -193,40 +152,59 @@ const requestCertificate = async function (requestId, domainName, subjectAlterna * * @param {string} arn The certificate ARN */ -const deleteCertificate = async function (arn, region) { +const deleteCertificate = async function (arn, region, hostedZoneId, route53Endpoint, cleanupRecords) { const acm = new aws.ACM({ region }); + const route53 = route53Endpoint ? new aws.Route53({ endpoint: route53Endpoint }) : new aws.Route53(); + if (waiter) { + // Used by the test suite, since waiters aren't mockable yet + route53.waitFor = acm.waitFor = waiter; + } try { console.log(`Waiting for certificate ${arn} to become unused`); let inUseByResources; + let records = []; for (let attempt = 0; attempt < maxAttempts; attempt++) { const { Certificate } = await acm.describeCertificate({ CertificateArn: arn }).promise(); + if (cleanupRecords) { + records = getDomainValidationRecords(Certificate); + } inUseByResources = Certificate.InUseBy || []; - if (inUseByResources.length) { + if (inUseByResources.length || !records.length) { // Exponential backoff with jitter based on 200ms base // component of backoff fixed to ensure minimum total wait time on // slow targets. const base = Math.pow(2, attempt); await sleep(random() * base * 50 + base * 150); } else { - break + break; } } if (inUseByResources.length) { throw new Error(`Response from describeCertificate did not contain an empty InUseBy list after ${maxAttempts} attempts.`) } + if (cleanupRecords && !records.length) { + throw new Error(`Response from describeCertificate did not contain DomainValidationOptions after ${maxAttempts} attempts.`) + } console.log(`Deleting certificate ${arn}`); await acm.deleteCertificate({ CertificateArn: arn }).promise(); + + if (cleanupRecords) { + console.log(`Deleting ${records.length} DNS records from zone ${hostedZoneId}:`); + + await commitRoute53Records(route53, records, hostedZoneId, 'DELETE'); + } + } catch (err) { if (err.name !== 'ResourceNotFoundException') { throw err; @@ -234,6 +212,66 @@ const deleteCertificate = async function (arn, region) { } }; +/** + * Retrieve the unique domain validation options as records to be upserted (or deleted) from Route53. + * + * Returns an empty array ([]) if the domain validation options is empty or the records are not yet ready. + */ +function getDomainValidationRecords(certificate) { + const options = certificate.DomainValidationOptions || []; + // Ensure all records are ready; there is (at least a theory there's) a chance of a partial response here in rare cases. + if (options.length > 0 && options.every(opt => opt && !!opt.ResourceRecord)) { + // some alternative names will produce the same validation record + // as the main domain (eg. example.com + *.example.com) + // filtering duplicates to avoid errors with adding the same record + // to the route53 zone twice + const unique = options + .map((val) => val.ResourceRecord) + .reduce((acc, cur) => { + acc[cur.Name] = cur; + return acc; + }, {}); + return Object.keys(unique).sort().map(key => unique[key]); + } + return []; +} + +/** + * Execute Route53 ChangeResourceRecordSets for a set of records within a Hosted Zone, + * and wait for the records to commit. Defaults to an 'UPSERT' action. + */ +async function commitRoute53Records(route53, records, hostedZoneId, action = 'UPSERT') { + const changeBatch = await route53.changeResourceRecordSets({ + ChangeBatch: { + Changes: records.map((record) => { + console.log(`${record.Name} ${record.Type} ${record.Value}`); + return { + Action: action, + ResourceRecordSet: { + Name: record.Name, + Type: record.Type, + TTL: 60, + ResourceRecords: [{ + Value: record.Value + }] + } + }; + }), + }, + HostedZoneId: hostedZoneId + }).promise(); + + console.log('Waiting for DNS records to commit...'); + await route53.waitFor('resourceRecordSetsChanged', { + // Wait up to 5 minutes + $waiter: { + delay: 30, + maxAttempts: 10 + }, + Id: changeBatch.ChangeInfo.Id + }).promise(); +} + /** * Main handler, invoked by Lambda */ @@ -262,7 +300,13 @@ exports.certificateRequestHandler = async function (event, context) { // If the resource didn't create correctly, the physical resource ID won't be the // certificate ARN, so don't try to delete it in that case. if (physicalResourceId.startsWith('arn:')) { - await deleteCertificate(physicalResourceId, event.ResourceProperties.Region); + await deleteCertificate( + physicalResourceId, + event.ResourceProperties.Region, + event.ResourceProperties.HostedZoneId, + event.ResourceProperties.Route53Endpoint, + event.ResourceProperties.CleanupRecords === "true", + ); } break; default: diff --git a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json index 9ac256541f4e3..4a1c4ac2385f3 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json +++ b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json @@ -29,21 +29,21 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.89", + "@types/aws-lambda": "^8.10.92", "@types/sinon": "^9.0.11", "@aws-cdk/cdk-build-tools": "0.0.0", "aws-sdk": "^2.596.0", - "aws-sdk-mock": "^5.5.0", + "aws-sdk-mock": "5.6.0", "eslint": "^7.32.0", "eslint-config-standard": "^14.1.1", - "eslint-plugin-import": "^2.25.3", + "eslint-plugin-import": "^2.25.4", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.3.1", "eslint-plugin-standard": "^4.1.0", - "jest": "^27.4.5", + "jest": "^27.5.1", "lambda-tester": "^3.6.0", "sinon": "^9.2.4", - "nock": "^13.2.1", - "ts-jest": "^27.1.2" + "nock": "^13.2.4", + "ts-jest": "^27.1.3" } } diff --git a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/test/handler.test.js b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/test/handler.test.js index 9f180f20211e9..a6a0632fd5aeb 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/test/handler.test.js +++ b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/test/handler.test.js @@ -996,4 +996,172 @@ describe('DNS Validated Certificate Handler', () => { expect(request.isDone()).toBe(true); }); }); + + describe('Delete option record cleanup', () => { + let describeCertificateFake; + let deleteCertificateFake; + let changeResourceRecordSetsFake; + + beforeEach(() => { + deleteCertificateFake = sinon.fake.resolves({}); + AWS.mock('ACM', 'deleteCertificate', deleteCertificateFake); + changeResourceRecordSetsFake = sinon.fake.resolves({ + ChangeInfo: { + Id: 'bogus' + } + }); + AWS.mock('Route53', 'changeResourceRecordSets', changeResourceRecordSetsFake); + + describeCertificateFake = sinon.fake.resolves({ + Certificate: { + CertificateArn: testCertificateArn, + DomainValidationOptions: [{ + ValidationStatus: 'SUCCESS', + ResourceRecord: { + Name: testRRName, + Type: 'CNAME', + Value: testRRValue + } + }] + } + }); + AWS.mock('ACM', 'describeCertificate', describeCertificateFake); + }); + + test('ignores records if CleanupRecords is not set', () => { + const request = nock(ResponseURL).put('/', body => { + return body.Status === 'SUCCESS'; + }).reply(200); + + return LambdaTester(handler.certificateRequestHandler) + .event({ + RequestType: 'Delete', + RequestId: testRequestId, + PhysicalResourceId: testCertificateArn, + ResourceProperties: { + Region: 'us-east-1', + HostedZoneId: testHostedZoneId, + } + }) + .expectResolve(() => { + sinon.assert.calledWith(describeCertificateFake, sinon.match({ + CertificateArn: testCertificateArn + })); + sinon.assert.calledWith(deleteCertificateFake, sinon.match({ + CertificateArn: testCertificateArn + })); + sinon.assert.notCalled(changeResourceRecordSetsFake); + expect(request.isDone()).toBe(true); + }); + }); + + test('ignores records if CleanupRecords is not set to "true"', () => { + const request = nock(ResponseURL).put('/', body => { + return body.Status === 'SUCCESS'; + }).reply(200); + + return LambdaTester(handler.certificateRequestHandler) + .event({ + RequestType: 'Delete', + RequestId: testRequestId, + PhysicalResourceId: testCertificateArn, + ResourceProperties: { + Region: 'us-east-1', + HostedZoneId: testHostedZoneId, + CleanupRecords: 'TRUE', // Not "true" + } + }) + .expectResolve(() => { + sinon.assert.calledWith(describeCertificateFake, sinon.match({ + CertificateArn: testCertificateArn + })); + sinon.assert.calledWith(deleteCertificateFake, sinon.match({ + CertificateArn: testCertificateArn + })); + sinon.assert.notCalled(changeResourceRecordSetsFake); + expect(request.isDone()).toBe(true); + }); + }); + + test('deletes records if CleanupRecords is set to true and records are present', () => { + const request = nock(ResponseURL).put('/', body => { + return body.Status === 'SUCCESS'; + }).reply(200); + + AWS.mock('Route53', 'changeResourceRecordSets', changeResourceRecordSetsFake); + + return LambdaTester(handler.certificateRequestHandler) + .event({ + RequestType: 'Delete', + RequestId: testRequestId, + PhysicalResourceId: testCertificateArn, + ResourceProperties: { + Region: 'us-east-1', + HostedZoneId: testHostedZoneId, + CleanupRecords: 'true', + }, + }) + .expectResolve(() => { + sinon.assert.calledWith(describeCertificateFake, sinon.match({ + CertificateArn: testCertificateArn + })); + sinon.assert.calledWith(deleteCertificateFake, sinon.match({ + CertificateArn: testCertificateArn + })); + sinon.assert.calledWith(changeResourceRecordSetsFake, sinon.match({ + ChangeBatch: { + Changes: [{ + Action: 'DELETE', + ResourceRecordSet: { + Name: testRRName, + Type: 'CNAME', + TTL: 60, + ResourceRecords: [{ + Value: testRRValue + }] + } + }] + }, + HostedZoneId: testHostedZoneId + })); + expect(request.isDone()).toBe(true); + }); + }); + + test('fails if CleanupRecords is set to true and records are not present', () => { + describeCertificateFake = sinon.fake.resolves({ + Certificate: { + CertificateArn: testCertificateArn, + } + }); + AWS.remock('ACM', 'describeCertificate', describeCertificateFake); + + const request = nock(ResponseURL).put('/', body => { + return body.Status === 'FAILED' && + body.Reason.startsWith('Response from describeCertificate did not contain DomainValidationOptions'); + }).reply(200); + + AWS.mock('Route53', 'changeResourceRecordSets', changeResourceRecordSetsFake); + + return LambdaTester(handler.certificateRequestHandler) + .event({ + RequestType: 'Delete', + RequestId: testRequestId, + PhysicalResourceId: testCertificateArn, + ResourceProperties: { + Region: 'us-east-1', + HostedZoneId: testHostedZoneId, + CleanupRecords: 'true', + }, + }) + .expectResolve(() => { + sinon.assert.calledWith(describeCertificateFake, sinon.match({ + CertificateArn: testCertificateArn + })); + sinon.assert.notCalled(deleteCertificateFake); + sinon.assert.notCalled(changeResourceRecordSetsFake); + expect(request.isDone()).toBe(true); + }); + }); + }); }); diff --git a/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts b/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts index 60c965e21a6ab..db7dc6f1f8663 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts +++ b/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts @@ -47,6 +47,17 @@ export interface DnsValidatedCertificateProps extends CertificateProps { */ readonly customResourceRole?: iam.IRole; + /** + * When set to true, when the DnsValidatedCertificate is deleted, + * the associated Route53 validation records are removed. + * + * CAUTION: If multiple certificates share the same domains (and same validation records), + * this can cause the other certificates to fail renewal and/or not validate. + * Not recommended for production use. + * + * @default false + */ + readonly cleanupRoute53Records?: boolean; } /** @@ -113,6 +124,8 @@ export class DnsValidatedCertificate extends CertificateBase implements ICertifi HostedZoneId: this.hostedZoneId, Region: props.region, Route53Endpoint: props.route53Endpoint, + // Custom resources properties are always converted to strings; might as well be explict here. + CleanupRecords: props.cleanupRoute53Records ? 'true' : undefined, Tags: cdk.Lazy.list({ produce: () => this.tags.renderTags() }), }, }); diff --git a/packages/@aws-cdk/aws-certificatemanager/package.json b/packages/@aws-cdk/aws-certificatemanager/package.json index 659b98eb834b6..b2dcdedca636d 100644 --- a/packages/@aws-cdk/aws-certificatemanager/package.json +++ b/packages/@aws-cdk/aws-certificatemanager/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -72,11 +79,11 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-acmpca": "0.0.0", diff --git a/packages/@aws-cdk/aws-certificatemanager/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-certificatemanager/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..0a11d49d2511f --- /dev/null +++ b/packages/@aws-cdk/aws-certificatemanager/rosetta/default.ts-fixture @@ -0,0 +1,12 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as acm from '@aws-cdk/aws-certificatemanager'; +import * as route53 from '@aws-cdk/aws-route53'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-certificatemanager/test/certificate.test.ts b/packages/@aws-cdk/aws-certificatemanager/test/certificate.test.ts index 8edcdf95b8158..4b49f423f0a1a 100644 --- a/packages/@aws-cdk/aws-certificatemanager/test/certificate.test.ts +++ b/packages/@aws-cdk/aws-certificatemanager/test/certificate.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as route53 from '@aws-cdk/aws-route53'; import { Duration, Lazy, Stack } from '@aws-cdk/core'; import { Certificate, CertificateValidation } from '../lib'; @@ -10,7 +10,7 @@ test('apex domain selection by default', () => { domainName: 'test.example.com', }); - expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { DomainName: 'test.example.com', DomainValidationOptions: [{ DomainName: 'test.example.com', @@ -48,7 +48,7 @@ test('validation domain can be overridden', () => { }), }); - expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { DomainValidationOptions: [{ DomainName: 'test.example.com', ValidationDomain: 'test.example.com', @@ -75,7 +75,7 @@ test('can configure validation method', () => { validation: CertificateValidation.fromDns(), }); - expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { DomainName: 'test.example.com', ValidationMethod: 'DNS', }); @@ -103,7 +103,7 @@ test('validationdomains can be given for a Token', () => { }), }); - expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { DomainName: 'my.example.com', DomainValidationOptions: [{ DomainName: 'my.example.com', @@ -123,7 +123,7 @@ test('CertificateValidation.fromEmail', () => { }), }); - expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { DomainName: 'test.example.com', SubjectAlternativeNames: ['extra.example.com'], DomainValidationOptions: [ @@ -151,7 +151,7 @@ describe('CertificateValidation.fromDns', () => { validation: CertificateValidation.fromDns(), }); - expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { DomainName: 'test.example.com', SubjectAlternativeNames: ['extra.example.com'], ValidationMethod: 'DNS', @@ -170,7 +170,7 @@ describe('CertificateValidation.fromDns', () => { validation: CertificateValidation.fromDns(exampleCom), }); - expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { DomainName: 'test.example.com', DomainValidationOptions: [ { @@ -198,7 +198,7 @@ describe('CertificateValidation.fromDns', () => { }); //Wildcard domain names are de-duped. - expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { DomainName: 'test.example.com', DomainValidationOptions: [ { @@ -226,7 +226,7 @@ describe('CertificateValidation.fromDns', () => { }); //Wildcard domain names are de-duped. - expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { DomainName: 'test.example.com', DomainValidationOptions: [ { @@ -275,7 +275,7 @@ test('CertificateValidation.fromDnsMultiZone', () => { }), }); - expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { DomainName: 'test.example.com', DomainValidationOptions: [ { diff --git a/packages/@aws-cdk/aws-certificatemanager/test/dns-validated-certificate.test.ts b/packages/@aws-cdk/aws-certificatemanager/test/dns-validated-certificate.test.ts index aadce823b74a0..d6881d61d79ac 100644 --- a/packages/@aws-cdk/aws-certificatemanager/test/dns-validated-certificate.test.ts +++ b/packages/@aws-cdk/aws-certificatemanager/test/dns-validated-certificate.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { SynthUtils } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import { HostedZone, PublicHostedZone } from '@aws-cdk/aws-route53'; import { App, Stack, Token, Tags } from '@aws-cdk/core'; @@ -15,10 +14,13 @@ test('creates CloudFormation Custom Resource', () => { new DnsValidatedCertificate(stack, 'Certificate', { domainName: 'test.example.com', hostedZone: exampleDotComZone, + subjectAlternativeNames: ['test2.example.com'], + cleanupRoute53Records: true, }); - expect(stack).toHaveResource('AWS::CloudFormation::CustomResource', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFormation::CustomResource', { DomainName: 'test.example.com', + SubjectAlternativeNames: ['test2.example.com'], ServiceToken: { 'Fn::GetAtt': [ 'CertificateCertificateRequestorFunction5E845413', @@ -28,13 +30,14 @@ test('creates CloudFormation Custom Resource', () => { HostedZoneId: { Ref: 'ExampleDotCom4D1B83AA', }, + CleanupRecords: 'true', }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Handler: 'index.certificateRequestHandler', Runtime: 'nodejs12.x', Timeout: 900, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyName: 'CertificateCertificateRequestorFunctionServiceRoleDefaultPolicy3C8845BC', Roles: [ { @@ -92,7 +95,7 @@ test('adds validation error on domain mismatch', () => { }); expect(() => { - SynthUtils.synthesize(stack); + Template.fromStack(stack); }).toThrow(/DNS zone hello.com is not authoritative for certificate domain name example.com/); }); @@ -108,7 +111,7 @@ test('does not try to validate unresolved tokens', () => { hostedZone: helloDotComZone, }); - SynthUtils.synthesize(stack); // does not throw + Template.fromStack(stack); // does not throw }); test('test root certificate', () => { @@ -123,7 +126,7 @@ test('test root certificate', () => { hostedZone: exampleDotComZone, }); - expect(stack).toHaveResource('AWS::CloudFormation::CustomResource', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFormation::CustomResource', { ServiceToken: { 'Fn::GetAtt': [ 'CertCertificateRequestorFunction98FDF273', @@ -150,7 +153,7 @@ test('test tags are passed to customresource', () => { hostedZone: exampleDotComZone, }); - expect(stack).toHaveResource('AWS::CloudFormation::CustomResource', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFormation::CustomResource', { ServiceToken: { 'Fn::GetAtt': [ 'CertCertificateRequestorFunction98FDF273', @@ -185,7 +188,7 @@ test('works with imported zone', () => { }); // THEN - expect(stack).toHaveResource('AWS::CloudFormation::CustomResource', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFormation::CustomResource', { ServiceToken: { 'Fn::GetAtt': [ 'CertCertificateRequestorFunction98FDF273', @@ -217,7 +220,7 @@ test('works with imported role', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Role: 'arn:aws:iam::account-id:role/role-name', }); }); diff --git a/packages/@aws-cdk/aws-certificatemanager/test/private-certificate.test.ts b/packages/@aws-cdk/aws-certificatemanager/test/private-certificate.test.ts index e43a4ca005691..fe9008f15e135 100644 --- a/packages/@aws-cdk/aws-certificatemanager/test/private-certificate.test.ts +++ b/packages/@aws-cdk/aws-certificatemanager/test/private-certificate.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as acmpca from '@aws-cdk/aws-acmpca'; import { Duration, Lazy, Stack } from '@aws-cdk/core'; import { PrivateCertificate } from '../lib'; @@ -12,7 +12,7 @@ test('private certificate authority', () => { 'arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/023077d8-2bfa-4eb0-8f22-05c96deade77'), }); - expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { DomainName: 'test.example.com', CertificateAuthorityArn: 'arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/023077d8-2bfa-4eb0-8f22-05c96deade77', }); @@ -28,7 +28,7 @@ test('private certificate authority with subjectAlternativeNames', () => { 'arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/023077d8-2bfa-4eb0-8f22-05c96deade77'), }); - expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { DomainName: 'test.example.com', SubjectAlternativeNames: ['extra.example.com'], CertificateAuthorityArn: 'arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/023077d8-2bfa-4eb0-8f22-05c96deade77', @@ -45,7 +45,7 @@ test('private certificate authority with multiple subjectAlternativeNames', () = 'arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/023077d8-2bfa-4eb0-8f22-05c96deade77'), }); - expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { DomainName: 'test.example.com', SubjectAlternativeNames: ['*.test.example.com', '*.foo.test.example.com', 'bar.test.example.com'], CertificateAuthorityArn: 'arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/023077d8-2bfa-4eb0-8f22-05c96deade77', @@ -73,7 +73,7 @@ test('private certificate authority with tokens', () => { certificateAuthority: acmpca.CertificateAuthority.fromCertificateAuthorityArn(stack, 'CA', certificateAuthority), }); - expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { DomainName: 'test.example.com', SubjectAlternativeNames: ['extra.example.com'], CertificateAuthorityArn: 'arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/023077d8-2bfa-4eb0-8f22-05c96deade77', diff --git a/packages/@aws-cdk/aws-certificatemanager/test/util.test.ts b/packages/@aws-cdk/aws-certificatemanager/test/util.test.ts index a4bdb2e05e1e0..5c1ccf5315499 100644 --- a/packages/@aws-cdk/aws-certificatemanager/test/util.test.ts +++ b/packages/@aws-cdk/aws-certificatemanager/test/util.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import { PublicHostedZone } from '@aws-cdk/aws-route53'; import { App, Aws, Stack } from '@aws-cdk/core'; import { Certificate, DnsValidatedCertificate } from '../lib'; diff --git a/packages/@aws-cdk/aws-chatbot/README.md b/packages/@aws-cdk/aws-chatbot/README.md index 5e871bb81db48..6f868ff02e17d 100644 --- a/packages/@aws-cdk/aws-chatbot/README.md +++ b/packages/@aws-cdk/aws-chatbot/README.md @@ -18,6 +18,7 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw ```ts import * as chatbot from '@aws-cdk/aws-chatbot'; import * as sns from '@aws-cdk/aws-sns'; +import * as iam from '@aws-cdk/aws-iam'; const slackChannel = new chatbot.SlackChannelConfiguration(this, 'MySlackChannel', { slackChannelConfigurationName: 'YOUR_CHANNEL_NAME', @@ -33,7 +34,7 @@ slackChannel.addToRolePolicy(new iam.PolicyStatement({ resources: ['arn:aws:s3:::abc/xyz/123.txt'], })); -slackChannel.addNotificationTopic(new sns.Topic(this, 'MyTopic')) +slackChannel.addNotificationTopic(new sns.Topic(this, 'MyTopic')); ``` ## Log Group diff --git a/packages/@aws-cdk/aws-chatbot/package.json b/packages/@aws-cdk/aws-chatbot/package.json index efe5c1e39ad75..259cf25fb4985 100644 --- a/packages/@aws-cdk/aws-chatbot/package.json +++ b/packages/@aws-cdk/aws-chatbot/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -74,12 +81,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-cloudwatch": "0.0.0", diff --git a/packages/@aws-cdk/aws-chatbot/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-chatbot/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..50d86e8a055ce --- /dev/null +++ b/packages/@aws-cdk/aws-chatbot/rosetta/default.ts-fixture @@ -0,0 +1,10 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-chatbot/test/slack-channel-configuration.test.ts b/packages/@aws-cdk/aws-chatbot/test/slack-channel-configuration.test.ts index a5d2b443ed486..9918dba29a1a1 100644 --- a/packages/@aws-cdk/aws-chatbot/test/slack-channel-configuration.test.ts +++ b/packages/@aws-cdk/aws-chatbot/test/slack-channel-configuration.test.ts @@ -1,5 +1,4 @@ -import { ABSENT } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as iam from '@aws-cdk/aws-iam'; import * as logs from '@aws-cdk/aws-logs'; @@ -21,7 +20,7 @@ describe('SlackChannelConfiguration', () => { slackChannelConfigurationName: 'Test', }); - expect(stack).toHaveResourceLike('AWS::Chatbot::SlackChannelConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::Chatbot::SlackChannelConfiguration', { ConfigurationName: 'Test', IamRoleArn: { 'Fn::GetAtt': [ @@ -33,7 +32,7 @@ describe('SlackChannelConfiguration', () => { SlackWorkspaceId: 'ABC123', }); - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -57,7 +56,7 @@ describe('SlackChannelConfiguration', () => { loggingLevel: chatbot.LoggingLevel.ERROR, }); - expect(stack).toHaveResourceLike('AWS::Chatbot::SlackChannelConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::Chatbot::SlackChannelConfiguration', { ConfigurationName: 'Test', IamRoleArn: { 'Fn::GetAtt': [ @@ -81,7 +80,7 @@ describe('SlackChannelConfiguration', () => { notificationTopics: [topic], }); - expect(stack).toHaveResourceLike('AWS::Chatbot::SlackChannelConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::Chatbot::SlackChannelConfiguration', { ConfigurationName: 'Test', IamRoleArn: { 'Fn::GetAtt': [ @@ -109,7 +108,7 @@ describe('SlackChannelConfiguration', () => { const topic = new sns.Topic(stack, 'MyTopic'); slackChannel.addNotificationTopic(topic); - expect(stack).toHaveResourceLike('AWS::Chatbot::SlackChannelConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::Chatbot::SlackChannelConfiguration', { ConfigurationName: 'Test', SnsTopicArns: [ { @@ -129,7 +128,7 @@ describe('SlackChannelConfiguration', () => { role: role, }); - expect(stack).toCountResources('AWS::IAM::Role', 0); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 0); }); test('created with new role and extra iam policies', () => { @@ -147,7 +146,7 @@ describe('SlackChannelConfiguration', () => { resources: ['arn:aws:s3:::abc/xyz/123.txt'], })); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -169,7 +168,7 @@ describe('SlackChannelConfiguration', () => { logRetention: logs.RetentionDays.ONE_MONTH, }); - expect(stack).toHaveResourceLike('Custom::LogRetention', { + Template.fromStack(stack).hasResourceProperties('Custom::LogRetention', { LogGroupName: '/aws/chatbot/ConfigurationName', RetentionInDays: 30, LogGroupRegion: 'us-east-1', @@ -199,7 +198,7 @@ describe('SlackChannelConfiguration', () => { }, metricName: 'MetricName', })); - expect(stack).toHaveResourceLike('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { Namespace: 'AWS/Chatbot', MetricName: 'MetricName', Dimensions: [ @@ -228,10 +227,10 @@ describe('SlackChannelConfiguration', () => { region: 'us-east-1', metricName: 'MetricName', })); - expect(stack).toHaveResourceLike('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { Namespace: 'AWS/Chatbot', MetricName: 'MetricName', - Dimensions: ABSENT, + Dimensions: Match.absent(), ComparisonOperator: 'GreaterThanThreshold', EvaluationPeriods: 1, Threshold: 0, @@ -249,8 +248,8 @@ describe('SlackChannelConfiguration', () => { resources: ['arn:aws:s3:::abc/xyz/123.txt'], })); - expect(stack).toCountResources('AWS::IAM::Role', 0); - expect(stack).toCountResources('AWS::IAM::Policy', 0); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 0); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 0); }); test('should throw error if ARN invalid', () => { diff --git a/packages/@aws-cdk/aws-cloud9/README.md b/packages/@aws-cdk/aws-cloud9/README.md index 36bdc6d4a3d4c..ba418cc56dd46 100644 --- a/packages/@aws-cdk/aws-cloud9/README.md +++ b/packages/@aws-cdk/aws-cloud9/README.md @@ -23,17 +23,23 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. -AWS Cloud9 is a cloud-based integrated development environment (IDE) that lets you write, run, and debug your code with just a browser. It includes a code editor, debugger, and terminal. Cloud9 comes prepackaged with essential tools for popular programming languages, including JavaScript, Python, PHP, and more, so you don’t need to install files or configure your development machine to start new projects. Since your Cloud9 IDE is cloud-based, you can work on your projects from your office, home, or anywhere using an internet-connected machine. Cloud9 also provides a seamless experience for developing serverless applications enabling you to easily define resources, debug, and switch between local and remote execution of serverless applications. With Cloud9, you can quickly share your development environment with your team, enabling you to pair program and track each other's inputs in real time. +AWS Cloud9 is a cloud-based integrated development environment (IDE) that lets you write, run, and debug your code with just a +browser. It includes a code editor, debugger, and terminal. Cloud9 comes prepackaged with essential tools for popular +programming languages, including JavaScript, Python, PHP, and more, so you don’t need to install files or configure your +development machine to start new projects. Since your Cloud9 IDE is cloud-based, you can work on your projects from your +office, home, or anywhere using an internet-connected machine. Cloud9 also provides a seamless experience for developing +serverless applications enabling you to easily define resources, debug, and switch between local and remote execution of +serverless applications. With Cloud9, you can quickly share your development environment with your team, enabling you to pair +program and track each other's inputs in real time. ## Creating EC2 Environment -EC2 Environments are defined with `Ec2Environment`. To create an EC2 environment in the private subnet, specify `subnetSelection` with private `subnetType`. +EC2 Environments are defined with `Ec2Environment`. To create an EC2 environment in the private subnet, specify +`subnetSelection` with private `subnetType`. ```ts -import * as cloud9 from '@aws-cdk/aws-cloud9'; - // create a cloud9 ec2 environment in a new VPC const vpc = new ec2.Vpc(this, 'VPC', { maxAzs: 3}); new cloud9.Ec2Environment(this, 'Cloud9Env', { vpc }); @@ -42,19 +48,19 @@ new cloud9.Ec2Environment(this, 'Cloud9Env', { vpc }); const defaultVpc = ec2.Vpc.fromLookup(this, 'DefaultVPC', { isDefault: true }); new cloud9.Ec2Environment(this, 'Cloud9Env2', { vpc: defaultVpc, - instanceType: new ec2.InstanceType('t3.large') + instanceType: new ec2.InstanceType('t3.large'), }); // or specify in a different subnetSelection const c9env = new cloud9.Ec2Environment(this, 'Cloud9Env3', { - vpc, - subnetSelection: { - subnetType: ec2.SubnetType.PRIVATE - } + vpc, + subnetSelection: { + subnetType: ec2.SubnetType.PRIVATE, + }, }); // print the Cloud9 IDE URL in the output -new cdk.CfnOutput(this, 'URL', { value: c9env.ideUrl }); +new CfnOutput(this, 'URL', { value: c9env.ideUrl }); ``` ## Cloning Repositories @@ -62,16 +68,19 @@ new cdk.CfnOutput(this, 'URL', { value: c9env.ideUrl }); Use `clonedRepositories` to clone one or multiple AWS Codecommit repositories into the environment: ```ts +import * as codecommit from '@aws-cdk/aws-codecommit'; + // create a codecommit repository to clone into the cloud9 environment const repoNew = new codecommit.Repository(this, 'RepoNew', { repositoryName: 'new-repo', }); // import an existing codecommit repository to clone into the cloud9 environment -const repoExisting = codecommit.Repository.fromRepositoryName(stack, 'RepoExisting', 'existing-repo'); +const repoExisting = codecommit.Repository.fromRepositoryName(this, 'RepoExisting', 'existing-repo'); // create a new Cloud9 environment and clone the two repositories -new cloud9.Ec2Environment(stack, 'C9Env', { +declare const vpc: ec2.Vpc; +new cloud9.Ec2Environment(this, 'C9Env', { vpc, clonedRepositories: [ cloud9.CloneRepository.fromCodeCommit(repoNew, '/src/new-repo'), diff --git a/packages/@aws-cdk/aws-cloud9/package.json b/packages/@aws-cdk/aws-cloud9/package.json index 8c434124da5b8..2fd6aab3799e2 100644 --- a/packages/@aws-cdk/aws-cloud9/package.json +++ b/packages/@aws-cdk/aws-cloud9/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-codecommit": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloud9/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-cloud9/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..e2687d0d2c5bf --- /dev/null +++ b/packages/@aws-cdk/aws-cloud9/rosetta/default.ts-fixture @@ -0,0 +1,12 @@ +// Fixture with packages imported, but nothing else +import { CfnOutput, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as cloud9 from '@aws-cdk/aws-cloud9'; +import * as ec2 from '@aws-cdk/aws-ec2'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} diff --git a/packages/@aws-cdk/aws-cloudformation/lib/custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/lib/custom-resource.ts index 9c30e6c08540c..b71a9f56aea61 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/custom-resource.ts @@ -99,21 +99,35 @@ export interface CustomResourceProps { * [resource provider framework]: https://docs.aws.amazon.com/cdk/api/latest/docs/custom-resources-readme.html * * ```ts - * // use the provider framework from aws-cdk/custom-resources: - * provider: new custom_resources.Provider({ + * import * as custom_resources from '@aws-cdk/custom-resources'; + * import * as lambda from '@aws-cdk/aws-lambda'; + * import { Stack } from '@aws-cdk/core'; + * declare const myOnEventLambda: lambda.Function; + * declare const myIsCompleteLambda: lambda.Function; + * const stack = new Stack(); + * + * const provider = new custom_resources.Provider(stack, 'myProvider', { * onEventHandler: myOnEventLambda, * isCompleteHandler: myIsCompleteLambda, // optional * }); * ``` * * ```ts + * import * as cloudformation from '@aws-cdk/aws-cloudformation'; + * import * as lambda from '@aws-cdk/aws-lambda'; + * declare const myFunction: lambda.Function; + * * // invoke an AWS Lambda function when a lifecycle event occurs: - * provider: CustomResourceProvider.fromLambda(myFunction) + * const provider = cloudformation.CustomResourceProvider.fromLambda(myFunction); * ``` * * ```ts + * import * as cloudformation from '@aws-cdk/aws-cloudformation'; + * import * as sns from '@aws-cdk/aws-sns'; + * declare const myTopic: sns.Topic; + * * // publish lifecycle events to an SNS topic: - * provider: CustomResourceProvider.fromTopic(myTopic) + * const provider = cloudformation.CustomResourceProvider.fromTopic(myTopic); * ``` */ readonly provider: ICustomResourceProvider; diff --git a/packages/@aws-cdk/aws-cloudformation/package.json b/packages/@aws-cdk/aws-cloudformation/package.json index 18f689c9fa90f..710300daaacde 100644 --- a/packages/@aws-cdk/aws-cloudformation/package.json +++ b/packages/@aws-cdk/aws-cloudformation/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -69,7 +76,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/aws-sns-subscriptions": "0.0.0", @@ -79,9 +86,9 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/aws-lambda": "^8.10.89", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/aws-lambda": "^8.10.92", + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-iam": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudformation/test/deps.test.ts b/packages/@aws-cdk/aws-cloudformation/test/deps.test.ts index 0354c7e9e4fa8..1e814b754eece 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/deps.test.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/deps.test.ts @@ -1,7 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import { ResourcePart } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { testDeprecated, describeDeprecated } from '@aws-cdk/cdk-build-tools'; import { App, CfnResource, Stack } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; @@ -39,8 +38,8 @@ describe('resource dependencies', () => { // THEN: the dependency needs to transfer from the resource within the // nested stack to the nested stack resource itself so the nested stack // will only be deployed the dependent resource - expect(parent).toHaveResource('AWS::CloudFormation::Stack', { DependsOn: ['ResourceInParent'] }, ResourcePart.CompleteDefinition); - expect(nested).toMatchTemplate({ Resources: { ResourceInNested: { Type: 'NESTED' } } }); // no DependsOn for the actual resource + Template.fromStack(parent).hasResource('AWS::CloudFormation::Stack', { DependsOn: ['ResourceInParent'] }); + Template.fromStack(nested).templateMatches({ Resources: { ResourceInNested: { Type: 'NESTED' } } }); // no DependsOn for the actual resource })); // eslint-disable-next-line jest/valid-describe @@ -57,8 +56,8 @@ describe('resource dependencies', () => { // THEN: the dependency needs to transfer from the resource within the // nested stack to the *parent* nested stack - expect(grantparent).toHaveResource('AWS::CloudFormation::Stack', { DependsOn: ['ResourceInGrandparent'] }, ResourcePart.CompleteDefinition); - expect(nested).toMatchTemplate({ Resources: { ResourceInNested: { Type: 'NESTED' } } }); // no DependsOn for the actual resource + Template.fromStack(grantparent).hasResource('AWS::CloudFormation::Stack', { DependsOn: ['ResourceInGrandparent'] }); + Template.fromStack(nested).templateMatches({ Resources: { ResourceInNested: { Type: 'NESTED' } } }); // no DependsOn for the actual resource })); // eslint-disable-next-line jest/valid-describe @@ -73,9 +72,9 @@ describe('resource dependencies', () => { addDep(resourceInParent, resourceInNested); // THEN: resource in parent needs to depend on the nested stack - expect(parent).toHaveResource('PARENT', { + Template.fromStack(parent).hasResource('PARENT', { DependsOn: [parent.resolve(nested.nestedStackResource!.logicalId)], - }, ResourcePart.CompleteDefinition); + }); })); // eslint-disable-next-line jest/valid-describe @@ -91,9 +90,9 @@ describe('resource dependencies', () => { addDep(resourceInGrandparent, resourceInNested); // THEN: resource in grantparent needs to depend on the top-level nested stack - expect(grandparent).toHaveResource('GRANDPARENT', { + Template.fromStack(grandparent).hasResource('GRANDPARENT', { DependsOn: [grandparent.resolve(parent.nestedStackResource!.logicalId)], - }, ResourcePart.CompleteDefinition); + }); })); // eslint-disable-next-line jest/valid-describe @@ -154,13 +153,13 @@ describe('resource dependencies', () => { addDep(resourceInNested1, resourceInNested2); // THEN: dependency transfered to nested stack resources - expect(stack).toHaveResource('AWS::CloudFormation::Stack', { + Template.fromStack(stack).hasResource('AWS::CloudFormation::Stack', { DependsOn: [stack.resolve(nested2.nestedStackResource!.logicalId)], - }, ResourcePart.CompleteDefinition); + }); - expect(stack).not.toHaveResource('AWS::CloudFormation::Stack', { + expect(Template.fromStack(stack).findResources('AWS::CloudFormation::Stack', { DependsOn: [stack.resolve(nested1.nestedStackResource!.logicalId)], - }, ResourcePart.CompleteDefinition); + })).toEqual({}); })); }); @@ -226,9 +225,9 @@ describe('stack dependencies', () => { nested1.addDependency(nested2); // THEN - expect(stack).toHaveResource('AWS::CloudFormation::Stack', { + Template.fromStack(stack).hasResource('AWS::CloudFormation::Stack', { DependsOn: [stack.resolve(nested2.nestedStackResource!.logicalId)], - }, ResourcePart.CompleteDefinition); + }); }); testDeprecated('nested stack depends on a deeply nested stack', () => { @@ -242,9 +241,9 @@ describe('stack dependencies', () => { nested1.addDependency(nested21); // THEN: transfered to a resource dep between the resources in the common stack - expect(stack).toHaveResource('AWS::CloudFormation::Stack', { + Template.fromStack(stack).hasResource('AWS::CloudFormation::Stack', { DependsOn: [stack.resolve(nested2.nestedStackResource!.logicalId)], - }, ResourcePart.CompleteDefinition); + }); }); testDeprecated('deeply nested stack depends on a parent nested stack', () => { @@ -258,9 +257,9 @@ describe('stack dependencies', () => { nested21.addDependency(nested1); // THEN: transfered to a resource dep between the resources in the common stack - expect(stack).toHaveResource('AWS::CloudFormation::Stack', { + Template.fromStack(stack).hasResource('AWS::CloudFormation::Stack', { DependsOn: [stack.resolve(nested1.nestedStackResource!.logicalId)], - }, ResourcePart.CompleteDefinition); + }); }); testDeprecated('top-level stack depends on a nested stack within a sibling', () => { diff --git a/packages/@aws-cdk/aws-cloudformation/test/nested-stack.test.ts b/packages/@aws-cdk/aws-cloudformation/test/nested-stack.test.ts index 07a43586971bd..a625012eba58a 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/nested-stack.test.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/nested-stack.test.ts @@ -1,7 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import { SynthUtils } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as s3_assets from '@aws-cdk/aws-s3-assets'; import * as sns from '@aws-cdk/aws-sns'; import { describeDeprecated } from '@aws-cdk/cdk-build-tools'; @@ -122,7 +121,7 @@ describeDeprecated('NestedStack', () => { }); // the parent template includes the parameters and the nested stack resource which points to the s3 url - expect(parent).toMatchTemplate({ + Template.fromStack(parent).templateMatches({ Resources: { nestedstackNestedStacknestedstackNestedStackResource71CDD241: { Type: 'AWS::CloudFormation::Stack', @@ -276,7 +275,7 @@ describeDeprecated('NestedStack', () => { app.synth(); // nested template should use a parameter to reference the resource from the parent stack - expect(nested).toMatchTemplate({ + Template.fromStack(nested).templateMatches({ Resources: { resource: @@ -303,7 +302,7 @@ describeDeprecated('NestedStack', () => { }); // parent template should pass in the value through the parameter - expect(parentStack).toHaveResource('AWS::CloudFormation::Stack', { + Template.fromStack(parentStack).hasResourceProperties('AWS::CloudFormation::Stack', { Parameters: { referencetoparentparentresourceD56EA8F7Ref: { Ref: 'parentresource', @@ -347,7 +346,7 @@ describeDeprecated('NestedStack', () => { app.synth(); // nested template should use a parameter to reference the resource from the parent stack - expect(nested).toMatchTemplate({ + Template.fromStack(nested).templateMatches({ Resources: { resource: { Type: 'AWS::Child::Resource' }, }, @@ -357,7 +356,7 @@ describeDeprecated('NestedStack', () => { }); // parent template should pass in the value through the parameter - expect(parentStack).toHaveResource('AWS::Parent::Resource', { + Template.fromStack(parentStack).hasResourceProperties('AWS::Parent::Resource', { RefToResourceInNestedStack: { 'Fn::GetAtt': [ 'nestedNestedStacknestedNestedStackResource3DD143BF', @@ -387,7 +386,7 @@ describeDeprecated('NestedStack', () => { const assembly = app.synth(); // producing stack should have an export - expect(stack2).toMatchTemplate({ + Template.fromStack(stack2).templateMatches({ Resources: { ResourceInStack2: { Type: 'MyResource' }, }, @@ -400,7 +399,7 @@ describeDeprecated('NestedStack', () => { }); // nested stack uses Fn::ImportValue like normal - expect(nestedUnderStack1).toMatchTemplate({ + Template.fromStack(nestedUnderStack1).templateMatches({ Resources: { ResourceInNestedStack1: { Type: 'Nested::Resource', @@ -464,7 +463,7 @@ describeDeprecated('NestedStack', () => { const assembly = app.synth(); // nested stack should output this value as if it was referenced by the parent (without the export) - expect(nestedUnderStack1).toMatchTemplate({ + Template.fromStack(nestedUnderStack1).templateMatches({ Resources: { ResourceInNestedStack: { Type: 'MyResource', @@ -491,7 +490,7 @@ describeDeprecated('NestedStack', () => { }); // consuming stack should use ImportValue to import the value from the parent stack - expect(stack2).toMatchTemplate({ + Template.fromStack(stack2).templateMatches({ Resources: { ResourceInStack2: { Type: 'JustResource', @@ -532,7 +531,7 @@ describeDeprecated('NestedStack', () => { app.synth(); // producing nested stack - expect(nested1).toMatchTemplate({ + Template.fromStack(nested1).templateMatches({ Resources: { Resource1: { Type: 'Resource1', @@ -548,7 +547,7 @@ describeDeprecated('NestedStack', () => { }); // consuming nested stack - expect(nested2).toMatchTemplate({ + Template.fromStack(nested2).templateMatches({ Resources: { Resource2: { Type: 'Resource2', @@ -567,7 +566,7 @@ describeDeprecated('NestedStack', () => { }); // parent - expect(parent).toHaveResource('AWS::CloudFormation::Stack', { + Template.fromStack(parent).hasResourceProperties('AWS::CloudFormation::Stack', { Parameters: { referencetoParentNested1NestedStackNested1NestedStackResource9C05342COutputsParentNested1Resource15F3F0657Ref: { 'Fn::GetAtt': [ @@ -591,7 +590,7 @@ describeDeprecated('NestedStack', () => { }); // THEN - expect(nested).toHaveResource('Nested::Resource', { + Template.fromStack(nested).hasResourceProperties('Nested::Resource', { MyStackId: { Ref: 'AWS::StackId' }, }); }); @@ -608,7 +607,7 @@ describeDeprecated('NestedStack', () => { }); // THEN - expect(parent).toHaveResource('Parent::Resource', { + Template.fromStack(parent).hasResourceProperties('Parent::Resource', { NestedStackId: { Ref: 'NestedStackNestedStackNestedStackNestedStackResourceB70834FD' }, }); }); @@ -625,7 +624,7 @@ describeDeprecated('NestedStack', () => { }); // THEN - expect(nested).toHaveResource('Nested::Resource', { + Template.fromStack(nested).hasResourceProperties('Nested::Resource', { MyStackName: { Ref: 'AWS::StackName' }, }); }); @@ -642,7 +641,7 @@ describeDeprecated('NestedStack', () => { }); // THEN - expect(parent).toHaveResource('Parent::Resource', { + Template.fromStack(parent).hasResourceProperties('Parent::Resource', { NestedStackName: { 'Fn::Select': [ 1, @@ -689,7 +688,7 @@ describeDeprecated('NestedStack', () => { const assembly = app.synth(); // nested2 is a "leaf", so it's just the resource - expect(nested2).toMatchTemplate({ + Template.fromStack(nested2).templateMatches({ Resources: { Resource2: { Type: 'Resource::2' }, }, @@ -701,8 +700,9 @@ describeDeprecated('NestedStack', () => { const hashSuffix = 'E28F0693'; // nested1 wires the nested2 template through parameters, so we expect those - expect(nested1).toHaveResource('Resource::1'); - const nested2Template = SynthUtils.toCloudFormation(nested1); + const nested1Template = Template.fromStack(nested1); + nested1Template.resourceCountIs('Resource::1', 1); + const nested2Template = nested1Template.toJSON(); expect(nested2Template.Parameters).toEqual({ referencetostackAssetParameters8169c6f8aaeaf5e2e8620f5f895ffe2099202ccb4b6889df48fe0967a894235cS3BucketE8768F5CRef: { Type: 'String' }, referencetostackAssetParameters8169c6f8aaeaf5e2e8620f5f895ffe2099202ccb4b6889df48fe0967a894235cS3VersionKey49DD83A2Ref: { Type: 'String' }, @@ -710,7 +710,7 @@ describeDeprecated('NestedStack', () => { // parent stack should have two sets of parameters. one for the first nested stack and the second // for the second nested stack, passed in as parameters to the first - const template = SynthUtils.toCloudFormation(parent); + const template = Template.fromStack(parent).toJSON(); expect(template.Parameters).toEqual({ AssetParameters8169c6f8aaeaf5e2e8620f5f895ffe2099202ccb4b6889df48fe0967a894235cS3BucketDE3B88D6: { Type: 'String', Description: 'S3 bucket for asset "8169c6f8aaeaf5e2e8620f5f895ffe2099202ccb4b6889df48fe0967a894235c"' }, AssetParameters8169c6f8aaeaf5e2e8620f5f895ffe2099202ccb4b6889df48fe0967a894235cS3VersionKey3A62EFEA: { Type: 'String', Description: 'S3 key for asset version "8169c6f8aaeaf5e2e8620f5f895ffe2099202ccb4b6889df48fe0967a894235c"' }, @@ -721,7 +721,7 @@ describeDeprecated('NestedStack', () => { }); // proxy asset params to nested stack - expect(parent).toHaveResource('AWS::CloudFormation::Stack', { + Template.fromStack(parent).hasResourceProperties('AWS::CloudFormation::Stack', { Parameters: { referencetostackAssetParameters8169c6f8aaeaf5e2e8620f5f895ffe2099202ccb4b6889df48fe0967a894235cS3BucketE8768F5CRef: { Ref: 'AssetParameters8169c6f8aaeaf5e2e8620f5f895ffe2099202ccb4b6889df48fe0967a894235cS3BucketDE3B88D6' }, referencetostackAssetParameters8169c6f8aaeaf5e2e8620f5f895ffe2099202ccb4b6889df48fe0967a894235cS3VersionKey49DD83A2Ref: { Ref: 'AssetParameters8169c6f8aaeaf5e2e8620f5f895ffe2099202ccb4b6889df48fe0967a894235cS3VersionKey3A62EFEA' }, @@ -780,7 +780,7 @@ describeDeprecated('NestedStack', () => { // THEN const assembly = app.synth(); - const template = SynthUtils.toCloudFormation(parent); + const template = Template.fromStack(parent).toJSON(); // two sets of asset parameters: one for the nested stack itself and one as a proxy for the asset within the stack expect(template.Parameters).toEqual({ @@ -793,7 +793,7 @@ describeDeprecated('NestedStack', () => { }); // asset proxy parameters are passed to the nested stack - expect(parent).toHaveResource('AWS::CloudFormation::Stack', { + Template.fromStack(parent).hasResourceProperties('AWS::CloudFormation::Stack', { Parameters: { referencetoParentStackAssetParametersdb01ee2eb7adc7915e364dc410d861e569543f9be3761d535a68d5c2cc181281S3Bucket82C55B96Ref: { Ref: 'AssetParametersdb01ee2eb7adc7915e364dc410d861e569543f9be3761d535a68d5c2cc181281S3BucketC188F637' }, referencetoParentStackAssetParametersdb01ee2eb7adc7915e364dc410d861e569543f9be3761d535a68d5c2cc181281S3VersionKeyA43C3CC6Ref: { Ref: 'AssetParametersdb01ee2eb7adc7915e364dc410d861e569543f9be3761d535a68d5c2cc181281S3VersionKeyC7F4DBF2' }, @@ -887,7 +887,7 @@ describeDeprecated('NestedStack', () => { }); // THEN - expect(nested).toMatchTemplate({ + Template.fromStack(nested).templateMatches({ Resources: { resourceinnested: { Type: 'CONSUMED', @@ -905,7 +905,7 @@ describeDeprecated('NestedStack', () => { }, }); - expect(parent).toHaveResource('CONSUMER', { + Template.fromStack(parent).hasResourceProperties('CONSUMER', { ConsumedAttribute: { 'Fn::GetAtt': [ 'nestedNestedStacknestedNestedStackResource3DD143BF', @@ -984,7 +984,7 @@ describeDeprecated('NestedStack', () => { }); // THEN - expect(top).toHaveResource('AWS::CloudFormation::Stack', { + Template.fromStack(top).hasResourceProperties('AWS::CloudFormation::Stack', { Parameters: { referencetostackAssetParameters842982bd421cce9742ba27151ef12ed699d44d22801f41e8029f63f2358a3f2fS3Bucket5DA5D2E7Ref: { Ref: 'AssetParameters842982bd421cce9742ba27151ef12ed699d44d22801f41e8029f63f2358a3f2fS3BucketDD4D96B5', @@ -998,7 +998,7 @@ describeDeprecated('NestedStack', () => { }, }); - expect(nested1).toHaveResource('AWS::CloudFormation::Stack', { + Template.fromStack(nested1).hasResourceProperties('AWS::CloudFormation::Stack', { Parameters: { referencetostacktoplevelBB16BF13Ref: { Ref: 'referencetostacktoplevelBB16BF13Ref', @@ -1006,7 +1006,7 @@ describeDeprecated('NestedStack', () => { }, }); - expect(nested2).toMatchTemplate({ + Template.fromStack(nested2).templateMatches({ Resources: { refToTopLevel: { Type: 'BottomLevel', @@ -1048,7 +1048,7 @@ describeDeprecated('NestedStack', () => { const paramName = 'referencetoGrandparentResourceInGrandparent010E997ARef'; // child (bottom) references through a parameter. - expect(bottom).toMatchTemplate({ + Template.fromStack(bottom).templateMatches({ Resources: { ResourceInChild: { Type: 'ResourceInChild', @@ -1063,14 +1063,14 @@ describeDeprecated('NestedStack', () => { }); // the parent (middle) sets the value of this parameter to be a reference to another parameter - expect(middle).toHaveResource('AWS::CloudFormation::Stack', { + Template.fromStack(middle).hasResourceProperties('AWS::CloudFormation::Stack', { Parameters: { [paramName]: { Ref: paramName }, }, }); // grandparent (top) assigns the actual value to the parameter - expect(top).toHaveResource('AWS::CloudFormation::Stack', { + Template.fromStack(top).hasResourceProperties('AWS::CloudFormation::Stack', { Parameters: { [paramName]: { Ref: 'ResourceInGrandparent' }, diff --git a/packages/@aws-cdk/aws-cloudformation/test/resource.test.ts b/packages/@aws-cdk/aws-cloudformation/test/resource.test.ts index c6cefdc714675..e2f6eae16f3b3 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/resource.test.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/resource.test.ts @@ -1,5 +1,4 @@ -import { ResourcePart } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as lambda from '@aws-cdk/aws-lambda'; import * as sns from '@aws-cdk/aws-sns'; import { describeDeprecated, testDeprecated } from '@aws-cdk/cdk-build-tools'; @@ -23,7 +22,7 @@ describeDeprecated('custom resources honor removalPolicy', () => { new TestCustomResource(stack, 'Custom'); // THEN - expect(stack).toHaveResource('AWS::CloudFormation::CustomResource', {}, ResourcePart.CompleteDefinition); + Template.fromStack(stack).hasResource('AWS::CloudFormation::CustomResource', {}); expect(app.synth().tryGetArtifact(stack.stackName)!.findMetadataByType('aws:cdk:protected').length).toEqual(0); }); @@ -36,7 +35,7 @@ describeDeprecated('custom resources honor removalPolicy', () => { new TestCustomResource(stack, 'Custom', { removalPolicy: cdk.RemovalPolicy.DESTROY }); // THEN - expect(stack).toHaveResource('AWS::CloudFormation::CustomResource', {}, ResourcePart.CompleteDefinition); + Template.fromStack(stack).hasResource('AWS::CloudFormation::CustomResource', {}); expect(app.synth().tryGetArtifact(stack.stackName)!.findMetadataByType('aws:cdk:protected').length).toEqual(0); }); @@ -49,10 +48,10 @@ describeDeprecated('custom resources honor removalPolicy', () => { new TestCustomResource(stack, 'Custom', { removalPolicy: cdk.RemovalPolicy.RETAIN }); // THEN - expect(stack).toHaveResource('AWS::CloudFormation::CustomResource', { + Template.fromStack(stack).hasResource('AWS::CloudFormation::CustomResource', { DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition); + }); }); }); @@ -66,7 +65,7 @@ testDeprecated('custom resource is added twice, lambda is added once', () => { new TestCustomResource(stack, 'Custom2'); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'SingletonLambdaTestCustomResourceProviderServiceRole81FEAB5C': { 'Type': 'AWS::IAM::Role', @@ -149,7 +148,7 @@ testDeprecated('custom resources can specify a resource type that starts with Cu resourceType: 'Custom::MyCustomResourceType', provider: CustomResourceProvider.fromTopic(new sns.Topic(stack, 'Provider')), }); - expect(stack).toHaveResource('Custom::MyCustomResourceType'); + Template.fromStack(stack).hasResourceProperties('Custom::MyCustomResourceType', {}); }); describeDeprecated('fails if custom resource type is invalid', () => { diff --git a/packages/@aws-cdk/aws-cloudfront-origins/README.md b/packages/@aws-cdk/aws-cloudfront-origins/README.md index cb7af64ff8618..d4e948b1e887e 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/README.md +++ b/packages/@aws-cdk/aws-cloudfront-origins/README.md @@ -18,9 +18,6 @@ An S3 bucket can be added as an origin. If the bucket is configured as a website documents. ```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) }, @@ -38,9 +35,6 @@ URLs and not S3 URLs directly. Alternatively, a custom origin access identity ca 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, { @@ -60,12 +54,12 @@ accessible (`internetFacing` is true). Both Application and Network load balance import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; -const vpc = new ec2.Vpc(...); +declare const vpc: ec2.Vpc; // Create an application load balancer in a VPC. 'internetFacing' must be 'true' // for CloudFront to access the load balancer and use it as an origin. const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { vpc, - internetFacing: true + internetFacing: true, }); new cloudfront.Distribution(this, 'myDist', { defaultBehavior: { origin: new origins.LoadBalancerV2Origin(lb) }, @@ -75,13 +69,22 @@ new cloudfront.Distribution(this, 'myDist', { The origin can also be customized to respond on different ports, have different connection properties, etc. ```ts +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; + +declare const loadBalancer: elbv2.ApplicationLoadBalancer; const origin = new origins.LoadBalancerV2Origin(loadBalancer, { connectionAttempts: 3, connectionTimeout: Duration.seconds(5), + readTimeout: Duration.seconds(45), + keepaliveTimeout: Duration.seconds(45), protocolPolicy: cloudfront.OriginProtocolPolicy.MATCH_VIEWER, }); ``` +Note that the `readTimeout` and `keepaliveTimeout` properties can extend their values over 60 seconds only if a limit increase request for CloudFront origin response timeout +quota has been approved in the target account; otherwise, values over 60 seconds will produce an error at deploy time. Consider that this value is +still limited to a maximum value of 180 seconds, which is a hard limit for that quota. + ## From an HTTP endpoint Origins can also be created from any other HTTP endpoint, given the domain name, and optionally, other origin properties. @@ -103,6 +106,7 @@ CloudFront automatically switches to the secondary origin. You achieve that behavior in the CDK using the `OriginGroup` class: ```ts +const myBucket = new s3.Bucket(this, 'myBucket'); new cloudfront.Distribution(this, 'myDist', { defaultBehavior: { origin: new origins.OriginGroup({ diff --git a/packages/@aws-cdk/aws-cloudfront-origins/lib/http-origin.ts b/packages/@aws-cdk/aws-cloudfront-origins/lib/http-origin.ts index d6aa44c0bb73d..e4d6ac190dcff 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/lib/http-origin.ts +++ b/packages/@aws-cdk/aws-cloudfront-origins/lib/http-origin.ts @@ -35,7 +35,10 @@ export interface HttpOriginProps extends cloudfront.OriginProps { /** * Specifies how long, in seconds, CloudFront waits for a response from the origin, also known as the origin response timeout. - * The valid range is from 1 to 60 seconds, inclusive. + * The valid range is from 1 to 180 seconds, inclusive. + * + * Note that values over 60 seconds are possible only after a limit increase request for the origin response timeout quota + * has been approved in the target account; otherwise, values over 60 seconds will produce an error at deploy time. * * @default Duration.seconds(30) */ @@ -43,7 +46,10 @@ export interface HttpOriginProps extends cloudfront.OriginProps { /** * Specifies how long, in seconds, CloudFront persists its connection to the origin. - * The valid range is from 1 to 60 seconds, inclusive. + * The valid range is from 1 to 180 seconds, inclusive. + * + * Note that values over 60 seconds are possible only after a limit increase request for the origin response timeout quota + * has been approved in the target account; otherwise, values over 60 seconds will produce an error at deploy time. * * @default Duration.seconds(5) */ @@ -58,8 +64,8 @@ export class HttpOrigin extends cloudfront.OriginBase { constructor(domainName: string, private readonly props: HttpOriginProps = {}) { super(domainName, props); - validateSecondsInRangeOrUndefined('readTimeout', 1, 60, props.readTimeout); - validateSecondsInRangeOrUndefined('keepaliveTimeout', 1, 60, props.keepaliveTimeout); + validateSecondsInRangeOrUndefined('readTimeout', 1, 180, props.readTimeout); + validateSecondsInRangeOrUndefined('keepaliveTimeout', 1, 180, props.keepaliveTimeout); } protected renderCustomOriginConfig(): cloudfront.CfnDistribution.CustomOriginConfigProperty | undefined { diff --git a/packages/@aws-cdk/aws-cloudfront-origins/package.json b/packages/@aws-cdk/aws-cloudfront-origins/package.json index 64298d1a43a83..d7bea5cd1ecca 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/package.json +++ b/packages/@aws-cdk/aws-cloudfront-origins/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -70,12 +77,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "aws-sdk": "^2.848.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-cloudfront-origins/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-cloudfront-origins/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..73800aee2e589 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/rosetta/default.ts-fixture @@ -0,0 +1,15 @@ +// Fixture with packages imported, but nothing else +import { Duration, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as origins from '@aws-cdk/aws-cloudfront-origins'; +import * as s3 from '@aws-cdk/aws-s3'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + + } +} diff --git a/packages/@aws-cdk/aws-cloudfront-origins/test/http-origin.test.ts b/packages/@aws-cdk/aws-cloudfront-origins/test/http-origin.test.ts index dc8c6c702fce3..f1109a7f8c7db 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/test/http-origin.test.ts +++ b/packages/@aws-cdk/aws-cloudfront-origins/test/http-origin.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import * as cloudfront from '@aws-cdk/aws-cloudfront'; import { App, Duration, Stack } from '@aws-cdk/core'; import { HttpOrigin } from '../lib'; @@ -71,26 +70,26 @@ test.each([ Duration.seconds(0), Duration.seconds(0.5), Duration.seconds(60.5), - Duration.seconds(61), + Duration.seconds(181), Duration.minutes(5), -])('validates readTimeout is an integer between 1 and 60 seconds', (readTimeout) => { +])('validates readTimeout is an integer between 1 and 180 seconds', (readTimeout) => { expect(() => { new HttpOrigin('www.example.com', { readTimeout, }); - }).toThrow(`readTimeout: Must be an int between 1 and 60 seconds (inclusive); received ${readTimeout.toSeconds()}.`); + }).toThrow(`readTimeout: Must be an int between 1 and 180 seconds (inclusive); received ${readTimeout.toSeconds()}.`); }); test.each([ Duration.seconds(0), Duration.seconds(0.5), Duration.seconds(60.5), - Duration.seconds(61), + Duration.seconds(181), Duration.minutes(5), -])('validates keepaliveTimeout is an integer between 1 and 60 seconds', (keepaliveTimeout) => { +])('validates keepaliveTimeout is an integer between 1 and 180 seconds', (keepaliveTimeout) => { expect(() => { new HttpOrigin('www.example.com', { keepaliveTimeout, }); - }).toThrow(`keepaliveTimeout: Must be an int between 1 and 60 seconds (inclusive); received ${keepaliveTimeout.toSeconds()}.`); + }).toThrow(`keepaliveTimeout: Must be an int between 1 and 180 seconds (inclusive); received ${keepaliveTimeout.toSeconds()}.`); }); diff --git a/packages/@aws-cdk/aws-cloudfront-origins/test/load-balancer-origin.test.ts b/packages/@aws-cdk/aws-cloudfront-origins/test/load-balancer-origin.test.ts index 8a21feb22f30e..899fe27ce2e39 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/test/load-balancer-origin.test.ts +++ b/packages/@aws-cdk/aws-cloudfront-origins/test/load-balancer-origin.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import * as cloudfront from '@aws-cdk/aws-cloudfront'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import { App, Duration, Stack } from '@aws-cdk/core'; diff --git a/packages/@aws-cdk/aws-cloudfront-origins/test/origin-group.test.ts b/packages/@aws-cdk/aws-cloudfront-origins/test/origin-group.test.ts index d8f8bb10fabb2..8289c9c2794e3 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/test/origin-group.test.ts +++ b/packages/@aws-cdk/aws-cloudfront-origins/test/origin-group.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cloudfront from '@aws-cdk/aws-cloudfront'; import * as s3 from '@aws-cdk/aws-s3'; import { Stack } from '@aws-cdk/core'; @@ -29,7 +29,7 @@ describe('Origin Groups', () => { const primaryOriginId = 'DistributionOrigin13547B94F'; const failoverOriginId = 'DistributionOrigin2C85CC43B'; const originGroupId = 'DistributionOriginGroup1A1A31B49'; - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { DefaultCacheBehavior: { TargetOriginId: originGroupId, 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 9f817d1d7e986..5d2d27a1bce0e 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,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cloudfront from '@aws-cdk/aws-cloudfront'; import * as s3 from '@aws-cdk/aws-s3'; import { App, Duration, Stack } from '@aws-cdk/core'; @@ -77,16 +77,17 @@ describe('With bucket', () => { const origin = new S3Origin(bucket, { originAccessIdentity }); new cloudfront.Distribution(stack, 'Dist', { defaultBehavior: { origin } }); - expect(stack).toHaveResourceLike('AWS::CloudFront::CloudFrontOriginAccessIdentity', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::CloudFrontOriginAccessIdentity', { CloudFrontOriginAccessIdentityConfig: { Comment: 'Identity for bucket provided by test', }, }); - expect(stack).toHaveResourceLike('AWS::S3::BucketPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { PolicyDocument: { Statement: [{ Action: 's3:GetObject', + Effect: 'Allow', Principal: { CanonicalUser: { 'Fn::GetAtt': ['OriginAccessIdentityDF1E3CAC', 'S3CanonicalUserId'] }, }, @@ -104,15 +105,16 @@ describe('With bucket', () => { const origin = new S3Origin(bucket); new cloudfront.Distribution(stack, 'Dist', { defaultBehavior: { origin } }); - expect(stack).toHaveResourceLike('AWS::CloudFront::CloudFrontOriginAccessIdentity', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::CloudFrontOriginAccessIdentity', { CloudFrontOriginAccessIdentityConfig: { Comment: 'Identity for StackDistOrigin15754CE84', }, }); - expect(stack).toHaveResourceLike('AWS::S3::BucketPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { PolicyDocument: { Statement: [{ Action: 's3:GetObject', + Effect: 'Allow', Principal: { CanonicalUser: { 'Fn::GetAtt': ['DistOrigin1S3Origin87D64058', 'S3CanonicalUserId'] }, }, @@ -133,20 +135,20 @@ describe('With bucket', () => { const origin = new S3Origin(bucket); new cloudfront.Distribution(stack, 'Dist', { defaultBehavior: { origin } }); - expect(stack).toHaveResource('AWS::CloudFront::Distribution'); - expect(bucketStack).toHaveResource('AWS::S3::Bucket'); - expect(bucketStack).toHaveResourceLike('AWS::CloudFront::CloudFrontOriginAccessIdentity', { + Template.fromStack(stack).resourceCountIs('AWS::CloudFront::Distribution', 1); + Template.fromStack(bucketStack).resourceCountIs('AWS::S3::Bucket', 1); + Template.fromStack(bucketStack).hasResourceProperties('AWS::CloudFront::CloudFrontOriginAccessIdentity', { CloudFrontOriginAccessIdentityConfig: { Comment: 'Identity for StackDistOrigin15754CE84', }, }); - expect(bucketStack).toHaveResourceLike('AWS::S3::BucketPolicy', { + Template.fromStack(bucketStack).hasResourceProperties('AWS::S3::BucketPolicy', { PolicyDocument: { - Statement: [{ + Statement: [Match.objectLike({ Principal: { CanonicalUser: { 'Fn::GetAtt': ['StackDistOrigin15754CE84S3Origin25582A25', 'S3CanonicalUserId'] }, }, - }], + })], }, }); }); diff --git a/packages/@aws-cdk/aws-cloudfront/README.md b/packages/@aws-cdk/aws-cloudfront/README.md index e87a4f9b56617..3a727d6c1763f 100644 --- a/packages/@aws-cdk/aws-cloudfront/README.md +++ b/packages/@aws-cdk/aws-cloudfront/README.md @@ -91,7 +91,7 @@ your domain name, and provide one (or more) domain names from the certificate fo The certificate must be present in the AWS Certificate Manager (ACM) service in the US East (N. Virginia) region; the certificate may either be created by ACM, or created elsewhere and imported into ACM. When a certificate is used, the distribution will support HTTPS connections -from SNI only and a minimum protocol version of TLSv1.2_2021 if the '@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021' feature flag is set, and TLSv1.2_2019 otherwise. +from SNI only and a minimum protocol version of TLSv1.2_2021 if the `@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021` feature flag is set, and TLSv1.2_2019 otherwise. ```ts // To use your own domain name in a Distribution, you must associate a certificate @@ -539,6 +539,203 @@ const distribution = cloudfront.Distribution.fromDistributionAttributes(this, 'I }); ``` +## Migrating from the original CloudFrontWebDistribution to the newer Distribution construct + +It's possible to migrate a distribution from the original to the modern API. +The changes necessary are the following: + +### The Distribution + +Replace `new CloudFrontWebDistribution` with `new Distribution`. Some +configuration properties have been changed: + +| Old API | New API | +|--------------------------------|------------------------------------------------------------------------------------------------| +| `originConfigs` | `defaultBehavior`; use `additionalBehaviors` if necessary | +| `viewerCertificate` | `certificate`; use `domainNames` for aliases | +| `errorConfigurations` | `errorResponses` | +| `loggingConfig` | `enableLogging`; configure with `logBucket` `logFilePrefix` and `logIncludesCookies` | +| `viewerProtocolPolicy` | removed; set on each behavior instead. default changed from `REDIRECT_TO_HTTPS` to `ALLOW_ALL` | + +After switching constructs, you need to maintain the same logical ID for the underlying [CfnDistribution](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-cloudfront.CfnDistribution.html) if you wish to avoid the deletion and recreation of your distribution. +To do this, use [escape hatches](https://docs.aws.amazon.com/cdk/v2/guide/cfn_layer.html) to override the logical ID created by the new Distribution construct with the logical ID created by the old construct. + +Example: + +```ts +declare const sourceBucket: s3.Bucket; + +const myDistribution = new cloudfront.Distribution(this, 'MyCfWebDistribution', { + defaultBehavior: { + origin: new origins.S3Origin(sourceBucket), + }, +}); +const cfnDistribution = myDistribution.node.defaultChild as cloudfront.CfnDistribution; +cfnDistribution.overrideLogicalId('MyDistributionCFDistribution3H55TI9Q'); +``` + +### Behaviors + +The modern API makes use of the [CloudFront Origins](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront_origins-readme.html) module to easily configure your origin. Replace your origin configuration with the relevant CloudFront Origins class. For example, here's a behavior with an S3 origin: + +```ts +declare const sourceBucket: s3.Bucket; +declare const oai: cloudfront.OriginAccessIdentity; + +new cloudfront.CloudFrontWebDistribution(this, 'MyCfWebDistribution', { + originConfigs: [ + { + s3OriginSource: { + s3BucketSource: sourceBucket, + originAccessIdentity: oai, + }, + behaviors : [ {isDefaultBehavior: true}], + }, + ], +}); +``` + +Becomes: + +```ts +declare const sourceBucket: s3.Bucket; + +const distribution = new cloudfront.Distribution(this, 'MyCfWebDistribution', { + defaultBehavior: { + origin: new origins.S3Origin(sourceBucket) // This class automatically creates an Origin Access Identity + }, +}); +``` + +In the original API all behaviors are defined in the `originConfigs` property. The new API is optimized for a single origin and behavior, so the default behavior and additional behaviors will be defined separately. + +```ts +declare const sourceBucket: s3.Bucket; +declare const oai: cloudfront.OriginAccessIdentity; + +new cloudfront.CloudFrontWebDistribution(this, 'MyCfWebDistribution', { + originConfigs: [ + { + s3OriginSource: { + s3BucketSource: sourceBucket, + originAccessIdentity: oai, + }, + behaviors: [ {isDefaultBehavior: true}], + }, + { + customOriginSource: { + domainName: 'MYALIAS', + }, + behaviors: [{ pathPattern: '/somewhere' }] + } + ], +}); +``` + +Becomes: + +```ts +declare const sourceBucket: s3.Bucket; + +const distribution = new cloudfront.Distribution(this, 'MyCfWebDistribution', { + defaultBehavior: { + origin: new origins.S3Origin(sourceBucket) // This class automatically creates an Origin Access Identity + }, + additionalBehaviors: { + '/somewhere': { + origin: new origins.HttpOrigin('MYALIAS'), + } + } +}); +``` + +### Certificates + +If you are using an ACM certificate, you can pass the certificate directly to the `certificate` prop. +Any aliases used before in the `ViewerCertificate` class should be passed in to the `domainNames` prop in the modern API. + +```ts +import * as acm from '@aws-cdk/aws-certificatemanager'; +declare const certificate: acm.Certificate; +declare const sourceBucket: s3.Bucket; + +const viewerCertificate = cloudfront.ViewerCertificate.fromAcmCertificate(certificate, { + aliases: ['MYALIAS'], +}); + +new cloudfront.CloudFrontWebDistribution(this, 'MyCfWebDistribution', { + originConfigs: [ + { + s3OriginSource: { + s3BucketSource: sourceBucket, + }, + behaviors : [ {isDefaultBehavior: true} ], + }, + ], + viewerCertificate: viewerCertificate, +}); +``` + +Becomes: + +```ts +import * as acm from '@aws-cdk/aws-certificatemanager'; +declare const certificate: acm.Certificate; +declare const sourceBucket: s3.Bucket; + +const distribution = new cloudfront.Distribution(this, 'MyCfWebDistribution', { + defaultBehavior: { + origin: new origins.S3Origin(sourceBucket), + }, + domainNames: ['MYALIAS'], + certificate: certificate, +}); +``` + +IAM certificates aren't directly supported by the new API, but can be easily configured through [escape hatches](https://docs.aws.amazon.com/cdk/v2/guide/cfn_layer.html) + +```ts +declare const sourceBucket: s3.Bucket; +const viewerCertificate = cloudfront.ViewerCertificate.fromIamCertificate('MYIAMROLEIDENTIFIER', { + aliases: ['MYALIAS'], +}); + +new cloudfront.CloudFrontWebDistribution(this, 'MyCfWebDistribution', { + originConfigs: [ + { + s3OriginSource: { + s3BucketSource: sourceBucket, + }, + behaviors : [ {isDefaultBehavior: true} ], + }, + ], + viewerCertificate: viewerCertificate, +}); +``` + +Becomes: + +```ts +declare const sourceBucket: s3.Bucket; +const distribution = new cloudfront.Distribution(this, 'MyCfWebDistribution', { + defaultBehavior: { + origin: new origins.S3Origin(sourceBucket), + }, + domainNames: ['MYALIAS'], +}); + +const cfnDistribution = distribution.node.defaultChild as cloudfront.CfnDistribution; + +cfnDistribution.addPropertyOverride('ViewerCertificate.IamCertificateId', 'MYIAMROLEIDENTIFIER'); +cfnDistribution.addPropertyOverride('ViewerCertificate.SslSupportMethod', 'sni-only'); +``` + +### Other changes + +A number of default settings have changed on the new API when creating a new distribution, behavior, and origin. +After making the major changes needed for the migration, run `cdk diff` to see what settings have changed. +If no changes are desired during migration, you will at the least be able to use [escape hatches](https://docs.aws.amazon.com/cdk/v2/guide/cfn_layer.html) to override what the CDK synthesizes, if you can't change the properties directly. + ## CloudFrontWebDistribution API > The `CloudFrontWebDistribution` construct is the original construct written for working with CloudFront distributions. @@ -662,7 +859,7 @@ new cloudfront.CloudFrontWebDistribution(this, 'MyDistribution', { behaviors : [ {isDefaultBehavior: true}], }, ], - geoRestriction: cloudfront.GeoRestriction.whitelist('US', 'UK'), + geoRestriction: cloudfront.GeoRestriction.allowlist('US', 'GB'), }); ``` diff --git a/packages/@aws-cdk/aws-cloudfront/lib/cache-policy.ts b/packages/@aws-cdk/aws-cloudfront/lib/cache-policy.ts index 533ccbd5d78b9..e870a538fc887 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/cache-policy.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/cache-policy.ts @@ -129,9 +129,14 @@ export class CachePolicy extends Resource implements ICachePolicy { physicalName: props.cachePolicyName, }); - const cachePolicyName = props.cachePolicyName ?? `${Names.uniqueId(this)}-${Stack.of(this).region}`; + const cachePolicyName = props.cachePolicyName ?? `${Names.uniqueId(this).slice(0, 110)}-${Stack.of(this).region}`; + if (!Token.isUnresolved(cachePolicyName) && !cachePolicyName.match(/^[\w-]+$/i)) { - throw new Error(`'cachePolicyName' can only include '-', '_', and alphanumeric characters, got: '${props.cachePolicyName}'`); + throw new Error(`'cachePolicyName' can only include '-', '_', and alphanumeric characters, got: '${cachePolicyName}'`); + } + + if (cachePolicyName.length > 128) { + throw new Error(`'cachePolicyName' cannot be longer than 128 characters, got: '${cachePolicyName.length}'`); } const minTtl = (props.minTtl ?? Duration.seconds(0)).toSeconds(); diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index c593edd9efec7..bd9fb1cb50202 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -285,7 +285,7 @@ export class Distribution extends Resource implements IDistribution { // Comments have an undocumented limit of 128 characters const trimmedComment = props.comment && props.comment.length > 128 - ? `${props.comment.substr(0, 128 - 3)}...` + ? `${props.comment.slice(0, 128 - 3)}...` : props.comment; const distribution = new CfnDistribution(this, 'Resource', { diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index 57fc0e6c2a1b5..37ee6edb996a2 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -79,14 +79,14 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "aws-sdk": "^2.848.0", - "jest": "^27.4.5" + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-certificatemanager": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudfront/test/cache-policy.test.ts b/packages/@aws-cdk/aws-cloudfront/test/cache-policy.test.ts index 84996aadccb79..2abeffd3cc39b 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/cache-policy.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/cache-policy.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { App, Aws, Duration, Stack } from '@aws-cdk/core'; import { CachePolicy, CacheCookieBehavior, CacheHeaderBehavior, CacheQueryStringBehavior } from '../lib'; @@ -22,7 +22,7 @@ describe('CachePolicy', () => { test('minimal example', () => { new CachePolicy(stack, 'CachePolicy'); - expect(stack).toHaveResource('AWS::CloudFront::CachePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::CachePolicy', { CachePolicyConfig: { Name: 'StackCachePolicy0D6FCBC0-testregion', MinTTL: 0, @@ -59,7 +59,7 @@ describe('CachePolicy', () => { enableAcceptEncodingBrotli: true, }); - expect(stack).toHaveResource('AWS::CloudFront::CachePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::CachePolicy', { CachePolicyConfig: { Name: 'MyPolicy', Comment: 'A default policy', @@ -85,7 +85,12 @@ describe('CachePolicy', () => { }); }); - test('throws if given a cachePolicyName with invalid characters', () => { + test('throws on long policy names over 128 characters', () => { + const errorMessage = /'cachePolicyName' cannot be longer than 128 characters/; + expect(() => new CachePolicy(stack, 'CachePolicy1', { cachePolicyName: 'FooBarBaz'.repeat(15) })).toThrow(errorMessage); + }); + + test('throws if cachePolicyName contains invalid characters', () => { const errorMessage = /'cachePolicyName' can only include '-', '_', and alphanumeric characters/; expect(() => new CachePolicy(stack, 'CachePolicy1', { cachePolicyName: 'My Policy' })).toThrow(errorMessage); expect(() => new CachePolicy(stack, 'CachePolicy2', { cachePolicyName: 'MyPolicy!' })).toThrow(errorMessage); @@ -106,7 +111,7 @@ describe('CachePolicy', () => { test('default TTLs', () => { new CachePolicy(stack, 'CachePolicy', { cachePolicyName: 'MyPolicy' }); - expect(stack).toHaveResourceLike('AWS::CloudFront::CachePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::CachePolicy', { CachePolicyConfig: { MinTTL: 0, DefaultTTL: 86400, @@ -121,7 +126,7 @@ describe('CachePolicy', () => { minTtl: Duration.days(2), }); - expect(stack).toHaveResourceLike('AWS::CloudFront::CachePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::CachePolicy', { CachePolicyConfig: { MinTTL: 172800, DefaultTTL: 172800, @@ -136,7 +141,7 @@ describe('CachePolicy', () => { defaultTtl: Duration.days(400), }); - expect(stack).toHaveResourceLike('AWS::CloudFront::CachePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::CachePolicy', { CachePolicyConfig: { MinTTL: 0, DefaultTTL: 34560000, diff --git a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts index 470be56acf0d5..01b70136258e0 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts @@ -1,11 +1,10 @@ -import { ABSENT, objectLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as acm from '@aws-cdk/aws-certificatemanager'; import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; +import { testFutureBehavior, testLegacyBehavior } from '@aws-cdk/cdk-build-tools/lib/feature-flag'; import { App, Duration, Stack } from '@aws-cdk/core'; import { CLOUDFRONT_DEFAULT_SECURITY_POLICY_TLS_V1_2_2021 } from '@aws-cdk/cx-api'; -import { testFutureBehavior, testLegacyBehavior } from '@aws-cdk/cdk-build-tools/lib/feature-flag'; import { CfnDistribution, Distribution, @@ -35,7 +34,7 @@ test('minimal example renders correctly', () => { const origin = defaultOrigin(); new Distribution(stack, 'MyDist', { defaultBehavior: { origin } }); - expect(stack).toHaveResource('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { DefaultCacheBehavior: { CachePolicyId: '658327ea-f89d-4fab-a63d-7e88639e58f6', @@ -79,7 +78,7 @@ test('exhaustive example of props renders correctly', () => { webAclId: '473e64fd-f30b-4765-81a0-62ad96dd167a', }); - expect(stack).toHaveResource('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { Aliases: ['example.com'], DefaultCacheBehavior: { @@ -130,7 +129,7 @@ test('ensure comment prop is not greater than max lenght', () => { ellipsis so a user would know there was more to read and everything beyond this point should not show up`, }); - expect(stack).toHaveResource('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { DefaultCacheBehavior: { CachePolicyId: '658327ea-f89d-4fab-a63d-7e88639e58f6', @@ -180,7 +179,7 @@ describe('multiple behaviors', () => { }, }); - expect(stack).toHaveResource('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { DefaultCacheBehavior: { CachePolicyId: '658327ea-f89d-4fab-a63d-7e88639e58f6', @@ -219,7 +218,7 @@ describe('multiple behaviors', () => { }, }); - expect(stack).toHaveResource('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { DefaultCacheBehavior: { CachePolicyId: '658327ea-f89d-4fab-a63d-7e88639e58f6', @@ -266,7 +265,7 @@ describe('multiple behaviors', () => { }); dist.addBehavior('api/2*', origin); - expect(stack).toHaveResource('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { DefaultCacheBehavior: { CachePolicyId: '658327ea-f89d-4fab-a63d-7e88639e58f6', @@ -359,7 +358,7 @@ describe('certificates', () => { certificate, }); - expect(customStack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(customStack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { Aliases: ['example.com', 'www.example.com'], ViewerCertificate: { @@ -386,7 +385,7 @@ describe('certificates', () => { certificate, }); - expect(customStack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(customStack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { Aliases: ['example.com', 'www.example.com'], ViewerCertificate: { @@ -409,7 +408,7 @@ describe('certificates', () => { certificate: certificate, }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { ViewerCertificate: { AcmCertificateArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012', @@ -457,7 +456,7 @@ describe('custom error responses', () => { }], }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { CustomErrorResponses: [ { @@ -486,9 +485,9 @@ describe('logging', () => { const origin = defaultOrigin(); new Distribution(stack, 'MyDist', { defaultBehavior: { origin } }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { - Logging: ABSENT, + Logging: Match.absent(), }, }); }); @@ -512,7 +511,7 @@ describe('logging', () => { enableLogging: true, }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { Logging: { Bucket: { 'Fn::GetAtt': ['MyDistLoggingBucket9B8976BC', 'RegionalDomainName'] }, @@ -529,7 +528,7 @@ describe('logging', () => { logBucket: loggingBucket, }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { Logging: { Bucket: { 'Fn::GetAtt': ['MyLoggingBucket4382CD04', 'RegionalDomainName'] }, @@ -547,7 +546,7 @@ describe('logging', () => { logIncludesCookies: true, }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { Logging: { Bucket: { 'Fn::GetAtt': ['MyDistLoggingBucket9B8976BC', 'RegionalDomainName'] }, @@ -587,7 +586,7 @@ describe('with Lambda@Edge functions', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { DefaultCacheBehavior: { LambdaFunctionAssociations: [ @@ -617,7 +616,7 @@ describe('with Lambda@Edge functions', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -656,10 +655,10 @@ describe('with Lambda@Edge functions', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { CacheBehaviors: [ - { + Match.objectLike({ PathPattern: 'images/*', LambdaFunctionAssociations: [ { @@ -669,7 +668,7 @@ describe('with Lambda@Edge functions', () => { }, }, ], - }, + }), ], }, }); @@ -711,8 +710,8 @@ describe('with Lambda@Edge functions', () => { }, }); - expect(stack).toHaveResource('AWS::Lambda::Function', { - Environment: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { + Environment: Match.absent(), Code: { ZipFile: 'whateverwithenv', }, @@ -764,7 +763,7 @@ describe('with Lambda@Edge functions', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { DefaultCacheBehavior: { LambdaFunctionAssociations: [ @@ -798,7 +797,7 @@ describe('with CloudFront functions', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { DefaultCacheBehavior: { FunctionAssociations: [ @@ -826,7 +825,7 @@ test('price class is included if provided', () => { priceClass: PriceClass.PRICE_CLASS_200, }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { PriceClass: 'PriceClass_200', }, @@ -840,7 +839,7 @@ test('escape hatches are supported', () => { const cfnDist = dist.node.defaultChild as CfnDistribution; cfnDist.addPropertyOverride('DistributionConfig.DefaultCacheBehavior.ForwardedValues.Headers', ['*']); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { DefaultCacheBehavior: { ForwardedValues: { @@ -859,9 +858,9 @@ describe('origin IDs', () => { defaultBehavior: { origin: defaultOrigin() }, }); - expect(nestedStack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(nestedStack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { - Origins: [objectLike({ + Origins: [Match.objectLike({ Id: 'ngerThanTheOneHundredAndTwentyEightCharacterLimitAReallyAwesomeDistributionWithAMemorableNameThatIWillNeverForgetOrigin1D38031F9', })], }, @@ -875,10 +874,10 @@ describe('origin IDs', () => { defaultBehavior: { origin: defaultOriginGroup() }, }); - expect(nestedStack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(nestedStack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { OriginGroups: { - Items: [objectLike({ + Items: [Match.objectLike({ Id: 'hanTheOneHundredAndTwentyEightCharacterLimitAReallyAwesomeDistributionWithAMemorableNameThatIWillNeverForgetOriginGroup1B5CE3FE6', })], }, diff --git a/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts b/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts index 55d6f3689103c..5b4a73f965930 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts @@ -1,5 +1,5 @@ import * as path from 'path'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; @@ -20,7 +20,7 @@ describe('stacks', () => { test('creates a custom resource and supporting resources in main stack', () => { new cloudfront.experimental.EdgeFunction(stack, 'MyFn', defaultEdgeFunctionProps()); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [{ Action: 'sts:AssumeRole', @@ -46,13 +46,13 @@ describe('stacks', () => { }, }], }); - expect(stack).toHaveResourceLike('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Handler: '__entrypoint__.handler', Role: { 'Fn::GetAtt': ['CustomCrossRegionStringParameterReaderCustomResourceProviderRole71CD6825', 'Arn'], }, }); - expect(stack).toHaveResource('Custom::CrossRegionStringParameterReader', { + Template.fromStack(stack).hasResourceProperties('Custom::CrossRegionStringParameterReader', { ServiceToken: { 'Fn::GetAtt': ['CustomCrossRegionStringParameterReaderCustomResourceProviderHandler65B5F33A', 'Arn'], }, @@ -66,7 +66,7 @@ describe('stacks', () => { const fnStack = getFnStack(); - expect(fnStack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(fnStack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -86,16 +86,16 @@ describe('stacks', () => { { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole']] }, ], }); - expect(fnStack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(fnStack).hasResourceProperties('AWS::Lambda::Function', { Code: { ZipFile: 'foo' }, Handler: 'index.handler', Role: { 'Fn::GetAtt': ['MyFnServiceRoleF3016589', 'Arn'] }, Runtime: 'nodejs12.x', }); - expect(fnStack).toHaveResource('AWS::Lambda::Version', { + Template.fromStack(fnStack).hasResourceProperties('AWS::Lambda::Version', { FunctionName: { Ref: 'MyFn6F8F742F' }, }); - expect(fnStack).toHaveResource('AWS::SSM::Parameter', { + Template.fromStack(fnStack).hasResourceProperties('AWS::SSM::Parameter', { Type: 'String', Value: { Ref: 'MyFnCurrentVersion309B29FC29686ce94039b6e08d1645be854b3ac9' }, Name: '/cdk/EdgeFunctionArn/testregion/Stack/MyFn', @@ -128,7 +128,7 @@ describe('stacks', () => { }); new cloudfront.experimental.EdgeFunction(stack, 'MyFn', defaultEdgeFunctionProps()); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -148,13 +148,13 @@ describe('stacks', () => { { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole']] }, ], }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Code: { ZipFile: 'foo' }, Handler: 'index.handler', Role: { 'Fn::GetAtt': ['MyFnServiceRole3F9D41E1', 'Arn'] }, Runtime: 'nodejs12.x', }); - expect(stack).toHaveResource('AWS::Lambda::Version', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Version', { FunctionName: { Ref: 'MyFn223608AD' }, }); }); @@ -164,7 +164,7 @@ describe('stacks', () => { new cloudfront.experimental.EdgeFunction(stack, 'MyFn2', defaultEdgeFunctionProps()); const fnStack = getFnStack(); - expect(fnStack).toCountResources('AWS::Lambda::Function', 2); + Template.fromStack(fnStack).resourceCountIs('AWS::Lambda::Function', 2); }); test('can set the stack id for each function', () => { @@ -174,9 +174,9 @@ describe('stacks', () => { new cloudfront.experimental.EdgeFunction(stack, 'MyFn2', defaultEdgeFunctionProps(fn2StackId)); const fn1Stack = app.node.findChild(fn1StackId) as cdk.Stack; - expect(fn1Stack).toCountResources('AWS::Lambda::Function', 1); + Template.fromStack(fn1Stack).resourceCountIs('AWS::Lambda::Function', 1); const fn2Stack = app.node.findChild(fn2StackId) as cdk.Stack; - expect(fn2Stack).toCountResources('AWS::Lambda::Function', 1); + Template.fromStack(fn2Stack).resourceCountIs('AWS::Lambda::Function', 1); }); test('cross-region stack supports defining functions within stages', () => { @@ -188,15 +188,13 @@ describe('stacks', () => { new cloudfront.experimental.EdgeFunction(stack, 'MyFn', defaultEdgeFunctionProps()); - // Because 'expect(stack)' doesn't work correctly for stacks in nested assemblies - const stackArtifact = stage.synth().getStackArtifact(stack.artifactId); - expect(stackArtifact).toHaveResourceLike('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Handler: '__entrypoint__.handler', Role: { 'Fn::GetAtt': ['CustomCrossRegionStringParameterReaderCustomResourceProviderRole71CD6825', 'Arn'], }, }); - expect(stackArtifact).toHaveResource('Custom::CrossRegionStringParameterReader', { + Template.fromStack(stack).hasResourceProperties('Custom::CrossRegionStringParameterReader', { ServiceToken: { 'Fn::GetAtt': ['CustomCrossRegionStringParameterReaderCustomResourceProviderHandler65B5F33A', 'Arn'], }, @@ -220,10 +218,10 @@ describe('stacks', () => { const secondFnStack = app.node.findChild(`edge-lambda-stack-${secondStack.node.addr}`) as cdk.Stack; // Two SSM parameters - expect(firstFnStack).toHaveResourceLike('AWS::SSM::Parameter', { + Template.fromStack(firstFnStack).hasResourceProperties('AWS::SSM::Parameter', { Name: '/cdk/EdgeFunctionArn/testregion/FirstStack/MyFn', }); - expect(secondFnStack).toHaveResourceLike('AWS::SSM::Parameter', { + Template.fromStack(secondFnStack).hasResourceProperties('AWS::SSM::Parameter', { Name: '/cdk/EdgeFunctionArn/testregion/SecondStack/MyFn', }); }); @@ -235,7 +233,7 @@ test('addAlias() creates alias in function stack', () => { fn.addAlias('MyCurrentAlias'); const fnStack = getFnStack(); - expect(fnStack).toHaveResourceLike('AWS::Lambda::Alias', { + Template.fromStack(fnStack).hasResourceProperties('AWS::Lambda::Alias', { Name: 'MyCurrentAlias', }); }); @@ -247,8 +245,8 @@ test('mutliple aliases with the same name can be added to the same stack', () => fn2.addAlias('live'); const fnStack = getFnStack(); - expect(fnStack).toCountResources('AWS::Lambda::Function', 2); - expect(fnStack).toCountResources('AWS::Lambda::Alias', 2); + Template.fromStack(fnStack).resourceCountIs('AWS::Lambda::Function', 2); + Template.fromStack(fnStack).resourceCountIs('AWS::Lambda::Alias', 2); }); test('addPermission() creates permissions in function stack', () => { @@ -260,7 +258,7 @@ test('addPermission() creates permissions in function stack', () => { }); const fnStack = getFnStack(); - expect(fnStack).toHaveResourceLike('AWS::Lambda::Permission', { + Template.fromStack(fnStack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', Principal: '123456789012', }); @@ -303,7 +301,7 @@ test('SSM parameter name is sanitized to remove disallowed characters', () => { const fnStack = getFnStack(); - expect(fnStack).toHaveResourceLike('AWS::SSM::Parameter', { + Template.fromStack(fnStack).hasResourceProperties('AWS::SSM::Parameter', { Name: '/cdk/EdgeFunctionArn/testregion/Stack/My_Bad_Fn_Name-With.Bonus', }); }); diff --git a/packages/@aws-cdk/aws-cloudfront/test/function.test.ts b/packages/@aws-cdk/aws-cloudfront/test/function.test.ts index c2466a71c8ff2..12e3d1d525433 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/function.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/function.test.ts @@ -1,6 +1,5 @@ import * as path from 'path'; -import '@aws-cdk/assert-internal/jest'; -import { expect as expectStack } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import { App, Stack } from '@aws-cdk/core'; import { Function, FunctionCode } from '../lib'; @@ -15,7 +14,7 @@ describe('CloudFront Function', () => { code: FunctionCode.fromInline('code'), }); - expectStack(stack).toMatch({ + Template.fromStack(stack).templateMatches({ Resources: { CF2D7241DD7: { Type: 'AWS::CloudFront::Function', @@ -40,7 +39,7 @@ describe('CloudFront Function', () => { code: FunctionCode.fromInline('code'), }); - expectStack(stack).toMatch({ + Template.fromStack(stack).templateMatches({ Resources: { CF2D7241DD7: { Type: 'AWS::CloudFront::Function', @@ -89,7 +88,7 @@ describe('CloudFront Function', () => { functionName: 'FunctionName', }); - expectStack(stack).toMatch({ + Template.fromStack(stack).templateMatches({ Resources: { CF2D7241DD7: { Type: 'AWS::CloudFront::Function', @@ -116,7 +115,7 @@ describe('CloudFront Function', () => { code: FunctionCode.fromFile({ filePath: path.join(__dirname, 'function-code.js') }), }); - expectStack(stack).toMatch({ + Template.fromStack(stack).templateMatches({ Resources: { CF2D7241DD7: { Type: 'AWS::CloudFront::Function', @@ -133,4 +132,4 @@ describe('CloudFront Function', () => { }, }); }); -}); \ No newline at end of file +}); diff --git a/packages/@aws-cdk/aws-cloudfront/test/geo-restriction.test.ts b/packages/@aws-cdk/aws-cloudfront/test/geo-restriction.test.ts index c9fb509d29d1d..5ba9ff2fbd25c 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/geo-restriction.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/geo-restriction.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import { GeoRestriction } from '../lib'; describe.each([ diff --git a/packages/@aws-cdk/aws-cloudfront/test/key-group.test.ts b/packages/@aws-cdk/aws-cloudfront/test/key-group.test.ts index f27f2ad7e0e42..6a1dfbdd90a5d 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/key-group.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/key-group.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { expect as expectStack } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import { App, Stack } from '@aws-cdk/core'; import { KeyGroup, PublicKey } from '../lib'; @@ -49,7 +48,7 @@ describe('KeyGroup', () => { ], }); - expectStack(stack).toMatch({ + Template.fromStack(stack).templateMatches({ Resources: { MyPublicKey78071F3D: { Type: 'AWS::CloudFront::PublicKey', @@ -91,7 +90,7 @@ describe('KeyGroup', () => { ], }); - expectStack(stack).toMatch({ + Template.fromStack(stack).templateMatches({ Resources: { MyPublicKey78071F3D: { Type: 'AWS::CloudFront::PublicKey', @@ -140,7 +139,7 @@ describe('KeyGroup', () => { ], }); - expectStack(stack).toMatch({ + Template.fromStack(stack).templateMatches({ Resources: { BingoKeyCBEC786C: { Type: 'AWS::CloudFront::PublicKey', @@ -184,4 +183,4 @@ describe('KeyGroup', () => { }, }); }); -}); \ No newline at end of file +}); diff --git a/packages/@aws-cdk/aws-cloudfront/test/oai.test.ts b/packages/@aws-cdk/aws-cloudfront/test/oai.test.ts index e9e04fa6e3003..ad579cca681b0 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/oai.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/oai.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import { OriginAccessIdentity } from '../lib'; @@ -8,7 +8,7 @@ describe('Origin Access Identity', () => { new OriginAccessIdentity(stack, 'OAI'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( { Resources: { OAIE1EFC67F: { @@ -31,7 +31,7 @@ describe('Origin Access Identity', () => { comment: 'test comment', }); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( { Resources: { OAIE1EFC67F: { @@ -54,7 +54,7 @@ describe('Origin Access Identity', () => { comment: 'This is a really long comment. Auto-generated comments based on ids of origins might sometimes be this long or even longer and that will break', }); - expect(stack).toHaveResourceLike('AWS::CloudFront::CloudFrontOriginAccessIdentity', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::CloudFrontOriginAccessIdentity', { CloudFrontOriginAccessIdentityConfig: { Comment: 'This is a really long comment. Auto-generated comments based on ids of origins might sometimes be this long or even longer and t', }, diff --git a/packages/@aws-cdk/aws-cloudfront/test/origin-groups.test.ts b/packages/@aws-cdk/aws-cloudfront/test/origin-groups.test.ts index 891fd10eaff37..8d6ffa55f7db1 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/origin-groups.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/origin-groups.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; import { CloudFrontWebDistribution, FailoverStatusCode } from '../lib'; @@ -28,85 +28,82 @@ describe('origin group', () => { ], }); - expect(stack) - .toHaveResourceLike('AWS::CloudFront::Distribution', { - DistributionConfig: { - OriginGroups: { - Items: [ - { - FailoverCriteria: { - StatusCodes: { - Items: [ - 500, - 502, - 503, - 504, - ], - Quantity: 4, - }, - }, - Id: 'OriginGroup1', - Members: { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { + DistributionConfig: { + OriginGroups: { + Items: [ + { + FailoverCriteria: { + StatusCodes: { Items: [ - { - OriginId: 'origin1', - }, - { - OriginId: 'originSecondary1', - }, + 500, + 502, + 503, + 504, ], - Quantity: 2, + Quantity: 4, }, }, - ], - Quantity: 1, - }, - Origins: [ - { - CustomOriginConfig: { - HTTPPort: 80, - HTTPSPort: 443, - OriginKeepaliveTimeout: 5, - OriginProtocolPolicy: 'https-only', - OriginReadTimeout: 30, - OriginSSLProtocols: [ - 'TLSv1.2', + Id: 'OriginGroup1', + Members: { + Items: [ + { + OriginId: 'origin1', + }, + { + OriginId: 'originSecondary1', + }, ], + Quantity: 2, }, - DomainName: 'myoriginfallback.com', - Id: 'originSecondary1', - OriginCustomHeaders: [ - { - HeaderName: 'X-Custom-Header', - HeaderValue: 'somevalue', - }, + }, + ], + Quantity: 1, + }, + Origins: [ + Match.objectLike({ + CustomOriginConfig: { + HTTPPort: 80, + HTTPSPort: 443, + OriginKeepaliveTimeout: 5, + OriginProtocolPolicy: 'https-only', + OriginReadTimeout: 30, + OriginSSLProtocols: [ + 'TLSv1.2', ], }, - { - CustomOriginConfig: { - HTTPPort: 80, - HTTPSPort: 443, - OriginKeepaliveTimeout: 5, - OriginProtocolPolicy: 'https-only', - OriginReadTimeout: 30, - OriginSSLProtocols: [ - 'TLSv1.2', - ], + DomainName: 'myoriginfallback.com', + Id: 'originSecondary1', + OriginCustomHeaders: [ + { + HeaderName: 'X-Custom-Header', + HeaderValue: 'somevalue', }, - DomainName: 'myorigin.com', - Id: 'origin1', - OriginCustomHeaders: [ - { - HeaderName: 'X-Custom-Header', - HeaderValue: 'somevalue', - }, + ], + }), + Match.objectLike({ + CustomOriginConfig: { + HTTPPort: 80, + HTTPSPort: 443, + OriginKeepaliveTimeout: 5, + OriginProtocolPolicy: 'https-only', + OriginReadTimeout: 30, + OriginSSLProtocols: [ + 'TLSv1.2', ], }, - ], - }, - }); - - + DomainName: 'myorigin.com', + Id: 'origin1', + OriginCustomHeaders: [ + { + HeaderName: 'X-Custom-Header', + HeaderValue: 'somevalue', + }, + ], + }), + ], + }, + }); }); test('Distribution with s3 origin failover', () => { @@ -139,92 +136,91 @@ describe('origin group', () => { ], }); - expect(stack) - .toHaveResourceLike('AWS::CloudFront::Distribution', { - DistributionConfig: { - OriginGroups: { - Items: [ - { - FailoverCriteria: { - StatusCodes: { - Items: [ - 500, - ], - Quantity: 1, - }, - }, - Id: 'OriginGroup1', - Members: { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { + DistributionConfig: { + OriginGroups: { + Items: [ + { + FailoverCriteria: { + StatusCodes: { Items: [ - { - OriginId: 'origin1', - }, - { - OriginId: 'originSecondary1', - }, + 500, ], - Quantity: 2, + Quantity: 1, }, }, - ], - Quantity: 1, - }, - Origins: [ - { - DomainName: { - 'Fn::Join': [ - '', - [ - 'myoriginbucketfallback.s3.', - { - Ref: 'AWS::Region', - }, - '.', - { - Ref: 'AWS::URLSuffix', - }, - ], + Id: 'OriginGroup1', + Members: { + Items: [ + { + OriginId: 'origin1', + }, + { + OriginId: 'originSecondary1', + }, ], + Quantity: 2, }, - Id: 'originSecondary1', - OriginCustomHeaders: [ - { - HeaderName: 'myHeader2', - HeaderValue: '21', - }, - ], - OriginPath: '/somwhere', - S3OriginConfig: {}, }, - { - DomainName: { - 'Fn::Join': [ - '', - [ - 'myoriginbucket.s3.', - { - Ref: 'AWS::Region', - }, - '.', - { - Ref: 'AWS::URLSuffix', - }, - ], + ], + Quantity: 1, + }, + Origins: [ + Match.objectLike({ + DomainName: { + 'Fn::Join': [ + '', + [ + 'myoriginbucketfallback.s3.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, ], + ], + }, + Id: 'originSecondary1', + OriginCustomHeaders: [ + { + HeaderName: 'myHeader2', + HeaderValue: '21', }, - Id: 'origin1', - OriginCustomHeaders: [ - { - HeaderName: 'myHeader', - HeaderValue: '42', - }, + ], + OriginPath: '/somwhere', + S3OriginConfig: {}, + }), + Match.objectLike({ + DomainName: { + 'Fn::Join': [ + '', + [ + 'myoriginbucket.s3.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + ], ], - OriginPath: '/', - S3OriginConfig: {}, }, - ], - }, - }); + Id: 'origin1', + OriginCustomHeaders: [ + { + HeaderName: 'myHeader', + HeaderValue: '42', + }, + ], + OriginPath: '/', + S3OriginConfig: {}, + }), + ], + }, + }); }); diff --git a/packages/@aws-cdk/aws-cloudfront/test/origin-request-policy.test.ts b/packages/@aws-cdk/aws-cloudfront/test/origin-request-policy.test.ts index b2b04af43f18a..bda5a09058e67 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/origin-request-policy.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/origin-request-policy.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { App, Aws, Stack } from '@aws-cdk/core'; import { OriginRequestPolicy, OriginRequestCookieBehavior, OriginRequestHeaderBehavior, OriginRequestQueryStringBehavior } from '../lib'; @@ -22,7 +22,7 @@ describe('OriginRequestPolicy', () => { test('minimal example', () => { new OriginRequestPolicy(stack, 'OriginRequestPolicy'); - expect(stack).toHaveResource('AWS::CloudFront::OriginRequestPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::OriginRequestPolicy', { OriginRequestPolicyConfig: { Name: 'StackOriginRequestPolicy6B17D9ED', CookiesConfig: { @@ -47,7 +47,7 @@ describe('OriginRequestPolicy', () => { queryStringBehavior: OriginRequestQueryStringBehavior.allowList('username'), }); - expect(stack).toHaveResource('AWS::CloudFront::OriginRequestPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::OriginRequestPolicy', { OriginRequestPolicyConfig: { Name: 'MyPolicy', Comment: 'A default policy', diff --git a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts index e6a59ff6179d6..418bac1f3d275 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import { App, Stack, Duration } from '@aws-cdk/core'; import { TestOrigin } from './test-origin'; diff --git a/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts b/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts index 326b33490271e..842f42e494157 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import * as lambda from '@aws-cdk/aws-lambda'; import { App, Stack } from '@aws-cdk/core'; import { AllowedMethods, CachedMethods, CachePolicy, KeyGroup, LambdaEdgeEventType, OriginRequestPolicy, PublicKey, ViewerProtocolPolicy } from '../../lib'; diff --git a/packages/@aws-cdk/aws-cloudfront/test/public-key.test.ts b/packages/@aws-cdk/aws-cloudfront/test/public-key.test.ts index 5fbd8da23ddc8..75ae100ab9bfd 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/public-key.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/public-key.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { expect as expectStack } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import { App, Stack } from '@aws-cdk/core'; import { PublicKey } from '../lib'; @@ -35,7 +34,7 @@ describe('PublicKey', () => { encodedKey: publicKey, }); - expectStack(stack).toMatch({ + Template.fromStack(stack).templateMatches({ Resources: { MyPublicKey78071F3D: { Type: 'AWS::CloudFront::PublicKey', @@ -58,7 +57,7 @@ describe('PublicKey', () => { comment: 'Key expiring on 1/1/1984', }); - expectStack(stack).toMatch({ + Template.fromStack(stack).templateMatches({ Resources: { MyPublicKey78071F3D: { Type: 'AWS::CloudFront::PublicKey', @@ -82,4 +81,4 @@ describe('PublicKey', () => { comment: 'Key expiring on 1/1/1984', })).toThrow(/Public key must be in PEM format [(]with the BEGIN\/END PUBLIC KEY lines[)]; got (.*?)/); }); -}); \ No newline at end of file +}); diff --git a/packages/@aws-cdk/aws-cloudfront/test/response-headers-policy.test.ts b/packages/@aws-cdk/aws-cloudfront/test/response-headers-policy.test.ts index 926b17d85a219..7160fd8c3d6c1 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/response-headers-policy.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/response-headers-policy.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { App, Duration, Stack } from '@aws-cdk/core'; import { HeadersFrameOption, HeadersReferrerPolicy, ResponseHeadersPolicy } from '../lib'; @@ -30,7 +30,7 @@ describe('ResponseHeadersPolicy', () => { test('minimal example', () => { new ResponseHeadersPolicy(stack, 'ResponseHeadersPolicy'); - expect(stack).toHaveResource('AWS::CloudFront::ResponseHeadersPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::ResponseHeadersPolicy', { ResponseHeadersPolicyConfig: { Name: 'StackResponseHeadersPolicy7B76F936', }, @@ -66,7 +66,7 @@ describe('ResponseHeadersPolicy', () => { }, }); - expect(stack).toHaveResource('AWS::CloudFront::ResponseHeadersPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::ResponseHeadersPolicy', { ResponseHeadersPolicyConfig: { Comment: 'A default policy', CorsConfig: { diff --git a/packages/@aws-cdk/aws-cloudfront/test/web-distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/web-distribution.test.ts index df1d401ae7de0..ef7487bac0076 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/web-distribution.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/web-distribution.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as certificatemanager from '@aws-cdk/aws-certificatemanager'; import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; @@ -58,7 +57,7 @@ describe('web distribution', () => { ], }); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( { 'Resources': { 'AnAmazingWebsiteProbablyCFDistribution47E3983B': { @@ -149,7 +148,7 @@ describe('web distribution', () => { ], }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'Bucket83908E77': { 'Type': 'AWS::S3::Bucket', @@ -227,7 +226,7 @@ describe('web distribution', () => { ], }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'Bucket83908E77': { 'Type': 'AWS::S3::Bucket', @@ -290,7 +289,7 @@ describe('web distribution', () => { const sourceBucket = new s3.Bucket(stack, 'Bucket'); new CloudFrontWebDistribution(stack, 'AnAmazingWebsiteProbably', { - comment: `Adding a comment longer than 128 characters should be trimmed and + comment: `Adding a comment longer than 128 characters should be trimmed and added the ellipsis so a user would know there was more to read and everything beyond this point should not show up`, originConfigs: [ { @@ -306,7 +305,7 @@ added the ellipsis so a user would know there was more to read and everything be ], }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { Bucket83908E77: { Type: 'AWS::S3::Bucket', @@ -344,8 +343,8 @@ added the ellipsis so a user would know there was more to read and everything be }, Compress: true, }, - Comment: `Adding a comment longer than 128 characters should be trimmed and -added the ellipsis so a user would know there was more to ...`, + Comment: `Adding a comment longer than 128 characters should be trimmed and +added the ellipsis so a user would know there was more to r...`, Enabled: true, IPV6Enabled: true, HttpVersion: 'http2', @@ -369,7 +368,7 @@ added the ellipsis so a user would know there was more to ...`, }], }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { Origins: [ { @@ -392,10 +391,11 @@ added the ellipsis so a user would know there was more to ...`, }, }); - expect(stack).toHaveResourceLike('AWS::S3::BucketPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { PolicyDocument: { Statement: [{ Action: 's3:GetObject', + Effect: 'Allow', Principal: { CanonicalUser: { 'Fn::GetAtt': ['OAIE1EFC67F', 'S3CanonicalUserId'] }, }, @@ -441,7 +441,7 @@ added the ellipsis so a user would know there was more to ...`, ], }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'Bucket83908E77': { 'Type': 'AWS::S3::Bucket', @@ -548,7 +548,7 @@ added the ellipsis so a user would know there was more to ...`, ], }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'Bucket83908E77': { 'Type': 'AWS::S3::Bucket', @@ -630,7 +630,7 @@ added the ellipsis so a user would know there was more to ...`, ], }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'Bucket83908E77': { 'Type': 'AWS::S3::Bucket', @@ -729,7 +729,7 @@ added the ellipsis so a user would know there was more to ...`, ], }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'Bucket83908E77': { 'Type': 'AWS::S3::Bucket', @@ -812,7 +812,7 @@ added the ellipsis so a user would know there was more to ...`, ], }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { 'DistributionConfig': { 'DefaultCacheBehavior': { 'FunctionAssociations': [ @@ -863,7 +863,7 @@ added the ellipsis so a user would know there was more to ...`, ], }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { 'DistributionConfig': { 'DefaultCacheBehavior': { 'LambdaFunctionAssociations': [ @@ -913,8 +913,8 @@ added the ellipsis so a user would know there was more to ...`, ], }); - expect(stack).toHaveResource('AWS::Lambda::Function', { - Environment: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { + Environment: Match.absent(), }); @@ -1027,7 +1027,7 @@ added the ellipsis so a user would know there was more to ...`, aliasConfiguration: { acmCertRef: 'another_acm_ref', names: ['ftp.example.com'] }, }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { 'DistributionConfig': { 'Aliases': ['www.example.com'], 'ViewerCertificate': { @@ -1037,7 +1037,7 @@ added the ellipsis so a user would know there was more to ...`, }, }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { 'DistributionConfig': { 'Aliases': ['ftp.example.com'], 'ViewerCertificate': { @@ -1067,7 +1067,7 @@ added the ellipsis so a user would know there was more to ...`, viewerCertificate: ViewerCertificate.fromAcmCertificate(certificate), }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { 'DistributionConfig': { 'Aliases': [], 'ViewerCertificate': { @@ -1097,7 +1097,7 @@ added the ellipsis so a user would know there was more to ...`, viewerCertificate: ViewerCertificate.fromAcmCertificate(certificate), }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { 'DistributionConfig': { 'Aliases': [], 'ViewerCertificate': { @@ -1129,7 +1129,7 @@ added the ellipsis so a user would know there was more to ...`, }), }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { 'DistributionConfig': { 'Aliases': ['example.com', 'www.example.com'], 'ViewerCertificate': { @@ -1158,7 +1158,7 @@ added the ellipsis so a user would know there was more to ...`, viewerCertificate: ViewerCertificate.fromIamCertificate('test'), }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { 'DistributionConfig': { 'Aliases': [], 'ViewerCertificate': { @@ -1186,7 +1186,7 @@ added the ellipsis so a user would know there was more to ...`, }), }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { 'DistributionConfig': { 'Aliases': ['example.com'], 'ViewerCertificate': { @@ -1213,7 +1213,7 @@ added the ellipsis so a user would know there was more to ...`, viewerCertificate: ViewerCertificate.fromCloudFrontDefaultCertificate(), }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { 'DistributionConfig': { 'Aliases': [], 'ViewerCertificate': { @@ -1236,7 +1236,7 @@ added the ellipsis so a user would know there was more to ...`, viewerCertificate: ViewerCertificate.fromCloudFrontDefaultCertificate('example.com', 'www.example.com'), }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { 'DistributionConfig': { 'Aliases': ['example.com', 'www.example.com'], 'ViewerCertificate': { @@ -1302,7 +1302,7 @@ added the ellipsis so a user would know there was more to ...`, viewerCertificate: ViewerCertificate.fromAcmCertificate(certificate), }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { 'DistributionConfig': { 'Aliases': [], 'ViewerCertificate': { @@ -1349,7 +1349,7 @@ added the ellipsis so a user would know there was more to ...`, }); // THEN - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { 'Statement': [ { @@ -1399,7 +1399,7 @@ added the ellipsis so a user would know there was more to ...`, ], }); - expect(stack).not.toHaveResourceLike('AWS::IAM::Role'); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 0); }); @@ -1417,7 +1417,7 @@ added the ellipsis so a user would know there was more to ...`, geoRestriction: GeoRestriction.allowlist('US', 'UK'), }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'Bucket83908E77': { 'Type': 'AWS::S3::Bucket', @@ -1493,7 +1493,7 @@ added the ellipsis so a user would know there was more to ...`, geoRestriction: GeoRestriction.denylist('US'), }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'Bucket83908E77': { 'Type': 'AWS::S3::Bucket', diff --git a/packages/@aws-cdk/aws-cloudtrail/README.md b/packages/@aws-cdk/aws-cloudtrail/README.md index 3deccd47545ea..46c5c0a95bd22 100644 --- a/packages/@aws-cdk/aws-cloudtrail/README.md +++ b/packages/@aws-cdk/aws-cloudtrail/README.md @@ -68,6 +68,8 @@ default retention setting. The following code enables sending CloudWatch logs bu period for the created Log Group. ```ts +import * as logs from '@aws-cdk/aws-logs'; + const trail = new cloudtrail.Trail(this, 'CloudTrail', { sendToCloudWatchLogs: true, cloudWatchLogsRetention: logs.RetentionDays.FOUR_MONTHS, @@ -88,18 +90,18 @@ The following code filters events for S3 from a specific AWS account and trigger ```ts const myFunctionHandler = new lambda.Function(this, 'MyFunction', { - code: lambda.Code.fromAsset('resource/myfunction'); + code: lambda.Code.fromAsset('resource/myfunction'), runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', }); -const eventRule = Trail.onEvent(this, 'MyCloudWatchEvent', { - target: new eventTargets.LambdaFunction(myFunctionHandler), +const eventRule = cloudtrail.Trail.onEvent(this, 'MyCloudWatchEvent', { + target: new targets.LambdaFunction(myFunctionHandler), }); eventRule.addEventPattern({ - account: '123456789012', - source: 'aws.s3', + account: ['123456789012'], + source: ['aws.s3'], }); ``` @@ -141,7 +143,7 @@ The following code configures the `Trail` to only track management events that a ```ts const trail = new cloudtrail.Trail(this, 'CloudTrail', { // ... - managementEvents: ReadWriteType.READ_ONLY, + managementEvents: cloudtrail.ReadWriteType.READ_ONLY, }); ``` @@ -157,13 +159,14 @@ be used to configure logging of S3 data events for specific buckets and specific configures logging of S3 data events for `fooBucket` and with object prefix `bar/`. ```ts -import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; +import * as s3 from '@aws-cdk/aws-s3'; const trail = new cloudtrail.Trail(this, 'MyAmazingCloudTrail'); +declare const bucket: s3.Bucket; // Adds an event selector to the bucket foo trail.addS3EventSelector([{ - bucket: fooBucket, // 'fooBucket' is of type s3.IBucket + bucket, objectPrefix: 'bar/', }]); ``` @@ -174,12 +177,12 @@ configures logging of Lambda data events for a specific Function. ```ts const trail = new cloudtrail.Trail(this, 'MyAmazingCloudTrail'); -const amazingFunction = new lambda.Function(stack, 'AnAmazingFunction', { +const amazingFunction = new lambda.Function(this, 'AnAmazingFunction', { runtime: lambda.Runtime.NODEJS_12_X, handler: "hello.handler", code: lambda.Code.fromAsset("lambda"), }); // Add an event selector to log data events for the provided Lambda functions. -trail.addLambdaEventSelector([ lambdaFunction ]); +trail.addLambdaEventSelector([ amazingFunction ]); ``` diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts index 12cf079c5279e..76f6603652dcf 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts @@ -209,7 +209,7 @@ export class Trail extends Resource { const cloudTrailPrincipal = new iam.ServicePrincipal('cloudtrail.amazonaws.com'); - this.s3bucket = props.bucket || new s3.Bucket(this, 'S3', { encryption: s3.BucketEncryption.UNENCRYPTED }); + this.s3bucket = props.bucket || new s3.Bucket(this, 'S3', { encryption: s3.BucketEncryption.UNENCRYPTED, enforceSSL: true }); this.s3bucket.addToResourcePolicy(new iam.PolicyStatement({ resources: [this.s3bucket.bucketArn], diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index d94555d73ae26..d5b30cb2c16c8 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -72,15 +79,14 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "aws-sdk": "^2.848.0", - "colors": "^1.4.0", - "jest": "^27.4.5" + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-events": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudtrail/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-cloudtrail/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..b6440cd045f44 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudtrail/rosetta/default.ts-fixture @@ -0,0 +1,14 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; +import * as sns from '@aws-cdk/aws-sns'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as targets from '@aws-cdk/aws-events-targets'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts b/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts index a53a45b2c97ce..1492a3c1377b1 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts @@ -1,5 +1,4 @@ -import { ABSENT, SynthUtils } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as lambda from '@aws-cdk/aws-lambda'; @@ -13,6 +12,36 @@ import { ManagementEventSources, ReadWriteType, Trail } from '../lib'; const ExpectedBucketPolicyProperties = { PolicyDocument: { Statement: [ + { + Action: 's3:*', + Condition: { + Bool: { 'aws:SecureTransport': 'false' }, + }, + Effect: 'Deny', + Principal: { + AWS: '*', + }, + Resource: [ + { + 'Fn::GetAtt': [ + 'MyAmazingCloudTrailS3A580FE27', + 'Arn', + ], + }, + { + 'Fn::Join': [ + '', + [{ + 'Fn::GetAtt': [ + 'MyAmazingCloudTrailS3A580FE27', + 'Arn', + ], + }, + '/*'], + ], + }, + ], + }, { Action: 's3:GetBucketAcl', Effect: 'Allow', @@ -69,11 +98,11 @@ describe('cloudtrail', () => { test('with no properties', () => { const stack = getTestStack(); new Trail(stack, 'MyAmazingCloudTrail'); - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).toHaveResource('AWS::S3::Bucket'); - expect(stack).toHaveResource('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; + Template.fromStack(stack).resourceCountIs('AWS::CloudTrail::Trail', 1); + Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 1); + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties); + Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 0); + const trail: any = Template.fromStack(stack).toJSON().Resources.MyAmazingCloudTrail54516E8D; expect(trail.DependsOn).toEqual(['MyAmazingCloudTrailS3Policy39C120B0']); }); @@ -98,10 +127,10 @@ describe('cloudtrail', () => { new Trail(stack, 'Trail', { bucket: Trailbucket }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).toHaveResource('AWS::S3::Bucket'); - expect(stack).toHaveResource('AWS::S3::BucketPolicy'); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); + Template.fromStack(stack).resourceCountIs('AWS::CloudTrail::Trail', 1); + Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 1); + Template.fromStack(stack).resourceCountIs('AWS::S3::BucketPolicy', 1); + Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 0); }); test('with sns topic', () => { @@ -111,9 +140,9 @@ describe('cloudtrail', () => { new Trail(stack, 'Trail', { snsTopic: topic }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); - expect(stack).toHaveResource('AWS::SNS::TopicPolicy', { + Template.fromStack(stack).resourceCountIs('AWS::CloudTrail::Trail', 1); + Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 0); + Template.fromStack(stack).hasResourceProperties('AWS::SNS::TopicPolicy', { PolicyDocument: { Statement: [ { @@ -137,7 +166,7 @@ describe('cloudtrail', () => { // WHEN new Trail(stack, 'Trail', { bucket }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { S3BucketName: 'somebucket', }); }); @@ -149,12 +178,46 @@ describe('cloudtrail', () => { // WHEN new Trail(stack, 'Trail', { s3KeyPrefix: 'someprefix' }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).toHaveResource('AWS::S3::Bucket'); - expect(stack).toHaveResource('AWS::S3::BucketPolicy', { + Template.fromStack(stack).resourceCountIs('AWS::CloudTrail::Trail', 1); + Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 1); + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { Bucket: { Ref: 'TrailS30071F172' }, PolicyDocument: { Statement: [ + { + Action: 's3:*', + Condition: { + Bool: { + 'aws:SecureTransport': 'false', + }, + }, + Effect: 'Deny', + Principal: { + AWS: '*', + }, + Resource: [ + { + 'Fn::GetAtt': [ + 'TrailS30071F172', + 'Arn', + ], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'TrailS30071F172', + 'Arn', + ], + }, + '/*', + ], + ], + }, + ], + }, { Action: 's3:GetBucketAcl', Effect: 'Allow', @@ -199,21 +262,21 @@ describe('cloudtrail', () => { trailName: 'UnencryptedTrail', }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { TrailName: 'EncryptionKeyTrail', KMSKeyId: { 'Fn::GetAtt': ['keyFEDD6EC0', 'Arn'], }, }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { TrailName: 'KmsKeyTrail', KMSKeyId: { 'Fn::GetAtt': ['keyFEDD6EC0', 'Arn'], }, }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { TrailName: 'UnencryptedTrail', - KMSKeyId: ABSENT, + KMSKeyId: Match.absent(), }); }); @@ -235,13 +298,13 @@ describe('cloudtrail', () => { sendToCloudWatchLogs: true, }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).toHaveResource('AWS::S3::Bucket'); - expect(stack).toHaveResource('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties); - expect(stack).toHaveResource('AWS::Logs::LogGroup'); - expect(stack).toHaveResource('AWS::IAM::Role'); - expect(stack).toHaveResource('AWS::Logs::LogGroup', { RetentionInDays: 365 }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).resourceCountIs('AWS::CloudTrail::Trail', 1); + Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 1); + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties); + Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 1); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogGroup', { RetentionInDays: 365 }); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [{ @@ -255,7 +318,7 @@ describe('cloudtrail', () => { PolicyName: logsRolePolicyName, Roles: [{ Ref: 'MyAmazingCloudTrailLogsRoleF2CCF977' }], }); - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; + const trail: any = Template.fromStack(stack).toJSON().Resources.MyAmazingCloudTrail54516E8D; expect(trail.DependsOn).toEqual([logsRolePolicyName, logsRoleName, 'MyAmazingCloudTrailS3Policy39C120B0']); }); @@ -266,15 +329,15 @@ describe('cloudtrail', () => { cloudWatchLogsRetention: RetentionDays.ONE_WEEK, }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).toHaveResource('AWS::S3::Bucket'); - expect(stack).toHaveResource('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties); - expect(stack).toHaveResource('AWS::Logs::LogGroup'); - expect(stack).toHaveResource('AWS::IAM::Role'); - expect(stack).toHaveResource('AWS::Logs::LogGroup', { + Template.fromStack(stack).resourceCountIs('AWS::CloudTrail::Trail', 1); + Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 1); + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties); + Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 1); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogGroup', { RetentionInDays: 7, }); - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; + const trail: any = Template.fromStack(stack).toJSON().Resources.MyAmazingCloudTrail54516E8D; expect(trail.DependsOn).toEqual([logsRolePolicyName, logsRoleName, 'MyAmazingCloudTrailS3Policy39C120B0']); }); @@ -289,19 +352,19 @@ describe('cloudtrail', () => { cloudWatchLogGroup, }); - expect(stack).toHaveResource('AWS::Logs::LogGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogGroup', { RetentionInDays: 5, }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { CloudWatchLogsLogGroupArn: stack.resolve(cloudWatchLogGroup.logGroupArn), }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: [{ + Statement: [Match.objectLike({ Resource: stack.resolve(cloudWatchLogGroup.logGroupArn), - }], + })], }, }); }); @@ -313,7 +376,7 @@ describe('cloudtrail', () => { cloudWatchLogsRetention: RetentionDays.ONE_WEEK, }); expect(t.logGroup).toBeUndefined(); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); + Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 0); }); }); @@ -324,7 +387,7 @@ describe('cloudtrail', () => { const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); cloudTrail.logAllS3DataEvents(); - expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { EventSelectors: [ { DataResources: [{ @@ -344,8 +407,8 @@ describe('cloudtrail', () => { }, ], }], - IncludeManagementEvents: ABSENT, - ReadWriteType: ABSENT, + IncludeManagementEvents: Match.absent(), + ReadWriteType: Match.absent(), }, ], }); @@ -362,7 +425,7 @@ describe('cloudtrail', () => { objectPrefix: 'prefix-1/prefix-2', }]); - expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { EventSelectors: [ { DataResources: [{ @@ -400,7 +463,7 @@ describe('cloudtrail', () => { const stack = getTestStack(); const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); cloudTrail.addS3EventSelector([]); - expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { EventSelectors: [], }); }); @@ -411,7 +474,7 @@ describe('cloudtrail', () => { const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); cloudTrail.logAllS3DataEvents({ includeManagementEvents: false, readWriteType: ReadWriteType.READ_ONLY }); - expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { EventSelectors: [ { DataResources: [{ @@ -443,7 +506,7 @@ describe('cloudtrail', () => { new Trail(stack, 'MyAmazingCloudTrail', { managementEvents: ReadWriteType.WRITE_ONLY }); - expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { EventSelectors: [ { IncludeManagementEvents: true, @@ -467,7 +530,7 @@ describe('cloudtrail', () => { excludeManagementEventSources: [], }); - expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { EventSelectors: [ { DataResources: [{ @@ -517,7 +580,7 @@ describe('cloudtrail', () => { const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); cloudTrail.addLambdaEventSelector([lambdaFunction]); - expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { EventSelectors: [ { DataResources: [{ @@ -537,7 +600,7 @@ describe('cloudtrail', () => { const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); cloudTrail.logAllLambdaDataEvents(); - expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { EventSelectors: [ { DataResources: [{ @@ -569,7 +632,7 @@ describe('cloudtrail', () => { managementEvents: ReadWriteType.NONE, }); - expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { EventSelectors: [ { IncludeManagementEvents: false, @@ -596,7 +659,7 @@ describe('cloudtrail', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { EventPattern: { 'detail-type': [ 'AWS API Call via CloudTrail', @@ -612,4 +675,4 @@ describe('cloudtrail', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.expected.json b/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.expected.json index 5b90761ade1ff..90c9dd9724771 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.expected.json +++ b/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.expected.json @@ -97,6 +97,40 @@ }, "PolicyDocument": { "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "TrailS30071F172", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "TrailS30071F172", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, { "Action": "s3:GetBucketAcl", "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/README.md b/packages/@aws-cdk/aws-cloudwatch-actions/README.md index f13861a8c1555..7788dc363a5ec 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/README.md +++ b/packages/@aws-cdk/aws-cloudwatch-actions/README.md @@ -11,18 +11,30 @@ This library contains a set of classes which can be used as CloudWatch Alarm actions. -The currently implemented actions are: EC2 Actions, SNS Actions, Autoscaling Actions and Aplication Autoscaling Actions +The currently implemented actions are: EC2 Actions, SNS Actions, SSM OpsCenter Actions, Autoscaling Actions and Application Autoscaling Actions ## EC2 Action Example ```ts -import * as cw from "@aws-cdk/aws-cloudwatch"; // Alarm must be configured with an EC2 per-instance metric -let alarm: cw.Alarm; +declare const alarm: cloudwatch.Alarm; // Attach a reboot when alarm triggers alarm.addAlarmAction( - new Ec2Action(Ec2InstanceActions.REBOOT) + new actions.Ec2Action(actions.Ec2InstanceAction.REBOOT), +); +``` + +## SSM OpsCenter Action Example + +```ts +declare const alarm: cloudwatch.Alarm; +// Create an OpsItem with specific severity and category when alarm triggers +alarm.addAlarmAction( + new actions.SsmAction( + actions.OpsItemSeverity.CRITICAL, + actions.OpsItemCategory.PERFORMANCE // category is optional + ) ); ``` diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/lib/index.ts b/packages/@aws-cdk/aws-cloudwatch-actions/lib/index.ts index 5a384eba01247..191da008b23a9 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudwatch-actions/lib/index.ts @@ -2,3 +2,4 @@ export * from './appscaling'; export * from './autoscaling'; export * from './sns'; export * from './ec2'; +export * from './ssm'; diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/lib/ssm.ts b/packages/@aws-cdk/aws-cloudwatch-actions/lib/ssm.ts new file mode 100644 index 0000000000000..0d26c4258c0d5 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch-actions/lib/ssm.ts @@ -0,0 +1,79 @@ +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import { Stack } from '@aws-cdk/core'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +/** + * Types of OpsItem severity available + */ +export enum OpsItemSeverity { + /** + * Set the severity to critical + */ + CRITICAL = '1', + /** + * Set the severity to high + */ + HIGH = '2', + /** + * Set the severity to medium + */ + MEDIUM = '3', + /** + * Set the severity to low + */ + LOW = '4' +} + +/** + * Types of OpsItem category available + */ +export enum OpsItemCategory { + /** + * Set the category to availability + */ + AVAILABILITY = 'Availability', + /** + * Set the category to cost + */ + COST = 'Cost', + /** + * Set the category to performance + */ + PERFORMANCE = 'Performance', + /** + * Set the category to recovery + */ + RECOVERY = 'Recovery', + /** + * Set the category to security + */ + SECURITY = 'Security' +} + +/** + * Use an SSM OpsItem action as an Alarm action + */ +export class SsmAction implements cloudwatch.IAlarmAction { + private severity: OpsItemSeverity; + private category?: OpsItemCategory; + + constructor(severity: OpsItemSeverity, category?: OpsItemCategory) { + this.severity = severity; + this.category = category; + } + + /** + * Returns an alarm action configuration to use an SSM OpsItem action as an alarm action + */ + bind(_scope: Construct, _alarm: cloudwatch.IAlarm): cloudwatch.AlarmActionConfig { + if (this.category === undefined) { + return { alarmActionArn: `arn:aws:ssm:${Stack.of(_scope).region}:${Stack.of(_scope).account}:opsitem:${this.severity}` }; + } else { + return { alarmActionArn: `arn:aws:ssm:${Stack.of(_scope).region}:${Stack.of(_scope).account}:opsitem:${this.severity}#CATEGORY=${this.category}` }; + } + } +} + diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/package.json b/packages/@aws-cdk/aws-cloudwatch-actions/package.json index 7e0694a42d8e9..a54a582f66548 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/package.json +++ b/packages/@aws-cdk/aws-cloudwatch-actions/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -64,14 +71,14 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-applicationautoscaling": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-cloudwatch-actions/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..c473f965be130 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch-actions/rosetta/default.ts-fixture @@ -0,0 +1,12 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as actions from '@aws-cdk/aws-cloudwatch-actions'; +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/test/appscaling.test.ts b/packages/@aws-cdk/aws-cloudwatch-actions/test/appscaling.test.ts index f355290aaa553..066df92d34b45 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/test/appscaling.test.ts +++ b/packages/@aws-cdk/aws-cloudwatch-actions/test/appscaling.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import { Stack } from '@aws-cdk/core'; @@ -27,7 +27,7 @@ test('can use topic as alarm action', () => { alarm.addAlarmAction(new actions.ApplicationScalingAction(action)); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { AlarmActions: [ { Ref: 'Action62AD07C0' }, ], diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/test/ec2.test.ts b/packages/@aws-cdk/aws-cloudwatch-actions/test/ec2.test.ts index 1d6dd47793796..ed4c4ba66bc85 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/test/ec2.test.ts +++ b/packages/@aws-cdk/aws-cloudwatch-actions/test/ec2.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import { Stack } from '@aws-cdk/core'; import * as actions from '../lib'; @@ -22,7 +22,7 @@ test('can use instance reboot as alarm action', () => { alarm.addAlarmAction(new actions.Ec2Action(actions.Ec2InstanceAction.REBOOT)); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { AlarmActions: [ { 'Fn::Join': [ diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/test/scaling.test.ts b/packages/@aws-cdk/aws-cloudwatch-actions/test/scaling.test.ts index 61561cd412e15..d3b16268e03bf 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/test/scaling.test.ts +++ b/packages/@aws-cdk/aws-cloudwatch-actions/test/scaling.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; @@ -29,7 +29,7 @@ test('can use topic as alarm action', () => { alarm.addAlarmAction(new actions.AutoScalingAction(action)); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { AlarmActions: [ { Ref: 'Action62AD07C0' }, ], diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/test/sns.test.ts b/packages/@aws-cdk/aws-cloudwatch-actions/test/sns.test.ts index 3cdc6e71ae9c2..ba379902b653f 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/test/sns.test.ts +++ b/packages/@aws-cdk/aws-cloudwatch-actions/test/sns.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as sns from '@aws-cdk/aws-sns'; import { Stack } from '@aws-cdk/core'; @@ -18,7 +18,7 @@ test('can use topic as alarm action', () => { alarm.addAlarmAction(new actions.SnsAction(topic)); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { AlarmActions: [ { Ref: 'TopicBFC7AF6E' }, ], diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/test/ssm.test.ts b/packages/@aws-cdk/aws-cloudwatch-actions/test/ssm.test.ts new file mode 100644 index 0000000000000..a4bb582005b22 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch-actions/test/ssm.test.ts @@ -0,0 +1,82 @@ +import { Template } from '@aws-cdk/assertions'; +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import { Stack } from '@aws-cdk/core'; +import * as actions from '../lib'; + +test('can use ssm with critical severity and performance category as alarm action', () => { + // GIVEN + const stack = new Stack(); + const alarm = new cloudwatch.Alarm(stack, 'Alarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS', + metricName: 'Test', + }), + evaluationPeriods: 3, + threshold: 100, + }); + + // WHEN + alarm.addAlarmAction(new actions.SsmAction(actions.OpsItemSeverity.CRITICAL, actions.OpsItemCategory.PERFORMANCE)); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { + AlarmActions: [ + { + 'Fn::Join': [ + '', + [ + 'arn:aws:ssm:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':opsitem:1#CATEGORY=Performance', + ], + ], + }, + ], + }); +}); + + +test('can use ssm with meduim severity and no category as alarm action', () => { + // GIVEN + const stack = new Stack(); + const alarm = new cloudwatch.Alarm(stack, 'Alarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS', + metricName: 'Test', + }), + evaluationPeriods: 3, + threshold: 100, + }); + + // WHEN + alarm.addAlarmAction(new actions.SsmAction(actions.OpsItemSeverity.MEDIUM)); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { + AlarmActions: [ + { + 'Fn::Join': [ + '', + [ + 'arn:aws:ssm:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':opsitem:3', + ], + ], + }, + ], + }); +}); + diff --git a/packages/@aws-cdk/aws-cloudwatch/package.json b/packages/@aws-cdk/aws-cloudwatch/package.json index 68f549795a945..f263def7dfcd2 100644 --- a/packages/@aws-cdk/aws-cloudwatch/package.json +++ b/packages/@aws-cdk/aws-cloudwatch/package.json @@ -84,8 +84,8 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-iam": "0.0.0", diff --git a/packages/@aws-cdk/aws-codeartifact/package.json b/packages/@aws-cdk/aws-codeartifact/package.json index d6ff28dc1b6d3..9303cee955029 100644 --- a/packages/@aws-cdk/aws-codeartifact/package.json +++ b/packages/@aws-cdk/aws-codeartifact/package.json @@ -28,6 +28,13 @@ "distName": "aws-cdk.aws-codeartifact", "module": "aws_cdk.aws_codeartifact" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-codebuild/package.json b/packages/@aws-cdk/aws-codebuild/package.json index 3c21b3009f6e8..5f43a603aaa62 100644 --- a/packages/@aws-cdk/aws-codebuild/package.json +++ b/packages/@aws-cdk/aws-codebuild/package.json @@ -83,16 +83,16 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "aws-sdk": "^2.848.0", - "jest": "^27.4.5" + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/assets": "0.0.0", diff --git a/packages/@aws-cdk/aws-codebuild/test/codebuild.test.ts b/packages/@aws-cdk/aws-codebuild/test/codebuild.test.ts index 1cc66df6b236f..5a058625649c3 100644 --- a/packages/@aws-cdk/aws-codebuild/test/codebuild.test.ts +++ b/packages/@aws-cdk/aws-codebuild/test/codebuild.test.ts @@ -1,5 +1,4 @@ -import { ABSENT, SynthUtils } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as kms from '@aws-cdk/aws-kms'; @@ -17,7 +16,7 @@ describe('default properties', () => { new codebuild.PipelineProject(stack, 'MyProject'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyProjectRole9BBE5233': { 'Type': 'AWS::IAM::Role', @@ -177,7 +176,7 @@ describe('default properties', () => { source, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyRepoF4F48043': { 'Type': 'AWS::CodeCommit::Repository', @@ -361,7 +360,7 @@ describe('default properties', () => { }, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -572,7 +571,7 @@ describe('default properties', () => { }), }); - expect(stack).toHaveResource('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Source: { Type: 'GITHUB', Location: 'https://github.com/testowner/testrepo.git', @@ -584,7 +583,7 @@ describe('default properties', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Triggers: { Webhook: true, FilterGroups: [ @@ -620,7 +619,7 @@ describe('default properties', () => { }), }); - expect(stack).toHaveResource('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Source: { Type: 'GITHUB_ENTERPRISE', InsecureSsl: true, @@ -630,7 +629,7 @@ describe('default properties', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Triggers: { Webhook: true, FilterGroups: [ @@ -672,7 +671,7 @@ describe('default properties', () => { }), }); - expect(stack).toHaveResource('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Source: { Type: 'BITBUCKET', Location: 'https://bitbucket.org/testowner/testrepo.git', @@ -681,7 +680,7 @@ describe('default properties', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Triggers: { Webhook: true, FilterGroups: [ @@ -710,7 +709,7 @@ describe('default properties', () => { }), }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Triggers: { Webhook: true, BuildType: 'BUILD_BATCH', @@ -725,7 +724,7 @@ describe('default properties', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -740,7 +739,7 @@ describe('default properties', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -809,7 +808,7 @@ describe('default properties', () => { securityGroups: [securityGroup], }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'VpcConfig': { 'SecurityGroupIds': [ { @@ -900,7 +899,7 @@ describe('default properties', () => { new codebuild.PipelineProject(stack, 'MyProject'); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { EncryptionKey: 'alias/aws/s3', }); }); @@ -915,7 +914,7 @@ describe('default properties', () => { grantReportGroupPermissions: false, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ {}, // CloudWatch logs @@ -960,7 +959,7 @@ test('using timeout and path in S3 artifacts sets it correctly', () => { timeout: cdk.Duration.minutes(123), }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Artifacts': { 'Path': 'some/path', 'Name': 'some_name', @@ -999,7 +998,7 @@ describe('secondary sources', () => { identifier: 'id', })); - expect(() => SynthUtils.synthesize(stack)).toThrow(/secondary sources/); + expect(() => Template.fromStack(stack)).toThrow(/secondary sources/); }); test('added with an identifer after the Project has been created are rendered in the template', () => { @@ -1018,7 +1017,7 @@ describe('secondary sources', () => { identifier: 'source1', })); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'SecondarySources': [ { 'SourceIdentifier': 'source1', @@ -1047,7 +1046,7 @@ describe('secondary source versions', () => { version: 'someversion', })); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'SecondarySources': [ { 'SourceIdentifier': 'source1', @@ -1079,7 +1078,7 @@ describe('secondary source versions', () => { identifier: 'source1', })); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'SecondarySources': [ { 'SourceIdentifier': 'source1', @@ -1105,7 +1104,7 @@ describe('fileSystemLocations', () => { })], }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'FileSystemLocations': [ { 'Identifier': 'myidentifier2', @@ -1132,7 +1131,7 @@ describe('fileSystemLocations', () => { mountOptions: 'opts', })); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'FileSystemLocations': [ { 'Identifier': 'myidentifier3', @@ -1177,7 +1176,7 @@ describe('secondary artifacts', () => { identifier: 'id', })); - expect(() => SynthUtils.synthesize(stack)).toThrow(/secondary artifacts/); + expect(() => Template.fromStack(stack)).toThrow(/secondary artifacts/); }); test('added with an identifier after the Project has been created are rendered in the template', () => { @@ -1197,7 +1196,7 @@ describe('secondary artifacts', () => { identifier: 'artifact1', })); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'SecondaryArtifacts': [ { 'ArtifactIdentifier': 'artifact1', @@ -1225,7 +1224,7 @@ describe('secondary artifacts', () => { encryption: false, })); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'SecondaryArtifacts': [ { 'ArtifactIdentifier': 'artifact1', @@ -1243,7 +1242,7 @@ describe('artifacts', () => { new codebuild.PipelineProject(stack, 'MyProject'); - expect(stack).toHaveResource('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Source': { 'Type': 'CODEPIPELINE', }, @@ -1283,9 +1282,9 @@ describe('artifacts', () => { }), }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Artifacts': { - 'Name': ABSENT, + 'Name': Match.absent(), 'ArtifactIdentifier': 'artifact1', 'OverrideArtifactName': true, }, @@ -1308,11 +1307,11 @@ describe('artifacts', () => { }), }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Artifacts': { 'ArtifactIdentifier': 'artifact1', 'Name': 'specificname', - 'OverrideArtifactName': ABSENT, + 'OverrideArtifactName': Match.absent(), }, }); }); @@ -1334,7 +1333,7 @@ test('events', () => { project.onStateChange('OnStateChange', { target: { bind: () => ({ arn: 'ARN', id: 'ID' }) } }); project.onBuildStarted('OnBuildStarted', { target: { bind: () => ({ arn: 'ARN', id: 'ID' }) } }); - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.codebuild', @@ -1356,7 +1355,7 @@ test('events', () => { 'State': 'ENABLED', }); - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.codebuild', @@ -1378,7 +1377,7 @@ test('events', () => { 'State': 'ENABLED', }); - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.codebuild', @@ -1397,7 +1396,7 @@ test('events', () => { 'State': 'ENABLED', }); - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.codebuild', @@ -1416,7 +1415,7 @@ test('events', () => { 'State': 'ENABLED', }); - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.codebuild', @@ -1455,7 +1454,7 @@ test('environment variables can be overridden at the project level', () => { }, }); - expect(stack).toHaveResource('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Source': { 'Type': 'CODEPIPELINE', }, @@ -1554,7 +1553,7 @@ test('fromCodebuildImage', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'Image': 'aws/codebuild/standard:4.0', }, @@ -1571,7 +1570,7 @@ describe('Windows2019 image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'Type': 'WINDOWS_SERVER_2019_CONTAINER', 'ComputeType': 'BUILD_GENERAL1_MEDIUM', @@ -1591,7 +1590,7 @@ describe('ARM image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'Type': 'ARM_CONTAINER', 'ComputeType': 'BUILD_GENERAL1_LARGE', @@ -1608,7 +1607,7 @@ describe('ARM image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'Type': 'ARM_CONTAINER', 'ComputeType': 'BUILD_GENERAL1_SMALL', @@ -1638,7 +1637,7 @@ describe('ARM image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'Type': 'ARM_CONTAINER', 'ComputeType': 'BUILD_GENERAL1_LARGE', @@ -1867,7 +1866,7 @@ test('enableBatchBuilds()', () => { throw new Error('Expecting return value with role'); } - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { BuildBatchConfig: { ServiceRole: { 'Fn::GetAtt': [ @@ -1878,7 +1877,7 @@ test('enableBatchBuilds()', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -1893,7 +1892,7 @@ test('enableBatchBuilds()', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.caching.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.caching.expected.json index c1680ca40cea9..d98388270cec6 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.caching.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.caching.expected.json @@ -34,6 +34,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -171,4 +175,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-buildspec-artifacts.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.project-buildspec-artifacts.expected.json index 17cec3ba454e2..011e192831fb9 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.project-buildspec-artifacts.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-buildspec-artifacts.expected.json @@ -34,6 +34,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -166,4 +170,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-logging.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.project-logging.expected.json index 6dbe8a8127224..03ffa27190981 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.project-logging.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-logging.expected.json @@ -39,6 +39,10 @@ "Action": [ "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -223,4 +227,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-secondary-sources-artifacts.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.project-secondary-sources-artifacts.expected.json index a8c439a0eaad1..f8c74e1af60e6 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.project-secondary-sources-artifacts.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-secondary-sources-artifacts.expected.json @@ -64,6 +64,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -218,4 +222,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codebuild/test/linux-gpu-build-image.test.ts b/packages/@aws-cdk/aws-codebuild/test/linux-gpu-build-image.test.ts index d3b0d44dde984..106836acdac21 100644 --- a/packages/@aws-cdk/aws-codebuild/test/linux-gpu-build-image.test.ts +++ b/packages/@aws-cdk/aws-codebuild/test/linux-gpu-build-image.test.ts @@ -1,5 +1,4 @@ -import { arrayWith, objectLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ecr from '@aws-cdk/aws-ecr'; import * as cdk from '@aws-cdk/core'; import * as codebuild from '../lib'; @@ -22,7 +21,7 @@ describe('Linux GPU build image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { ComputeType: 'BUILD_GENERAL1_LARGE', Image: { @@ -37,9 +36,9 @@ describe('Linux GPU build image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith(objectLike({ + Statement: Match.arrayWith([Match.objectLike({ Action: [ 'ecr:BatchCheckLayerAvailability', 'ecr:GetDownloadUrlForLayer', @@ -54,7 +53,7 @@ describe('Linux GPU build image', () => { ':123456789012:repository/my-repo', ]], }, - })), + })]), }, }); }); @@ -78,7 +77,7 @@ describe('Linux GPU build image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { ComputeType: 'BUILD_GENERAL1_LARGE', Image: { @@ -96,9 +95,9 @@ describe('Linux GPU build image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith(objectLike({ + Statement: Match.arrayWith([Match.objectLike({ Action: [ 'ecr:BatchCheckLayerAvailability', 'ecr:GetDownloadUrlForLayer', @@ -116,7 +115,7 @@ describe('Linux GPU build image', () => { { Ref: 'myrepo5DFA62E5' }, ]], }, - })), + })]), }, }); }); @@ -138,7 +137,7 @@ describe('Linux GPU build image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { ComputeType: 'BUILD_GENERAL1_LARGE', Image: { @@ -154,9 +153,9 @@ describe('Linux GPU build image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith(objectLike({ + Statement: Match.arrayWith([Match.objectLike({ Action: [ 'ecr:BatchCheckLayerAvailability', 'ecr:GetDownloadUrlForLayer', @@ -173,7 +172,7 @@ describe('Linux GPU build image', () => { ':repository/test-repo', ]], }, - })), + })]), }, }); }); @@ -195,7 +194,7 @@ describe('Linux GPU build image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { ComputeType: 'BUILD_GENERAL1_LARGE', Image: { @@ -210,9 +209,9 @@ describe('Linux GPU build image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith(objectLike({ + Statement: Match.arrayWith([Match.objectLike({ Action: [ 'ecr:BatchCheckLayerAvailability', 'ecr:GetDownloadUrlForLayer', @@ -227,7 +226,7 @@ describe('Linux GPU build image', () => { ':585695036304:repository/foo/bar/foo/fooo', ]], }, - })), + })]), }, }); }); diff --git a/packages/@aws-cdk/aws-codebuild/test/notification-rule.test.ts b/packages/@aws-cdk/aws-codebuild/test/notification-rule.test.ts index 45d6a65d76ce4..e0dc908185427 100644 --- a/packages/@aws-cdk/aws-codebuild/test/notification-rule.test.ts +++ b/packages/@aws-cdk/aws-codebuild/test/notification-rule.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as sns from '@aws-cdk/aws-sns'; import * as cdk from '@aws-cdk/core'; import * as codebuild from '../lib'; @@ -24,7 +24,7 @@ test('notifications rule', () => { project.notifyOnBuildFailed('NotifyOnBuildFailed', target); - expect(stack).toHaveResource('AWS::CodeStarNotifications::NotificationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeStarNotifications::NotificationRule', { Name: 'MyCodebuildProjectNotifyOnBuildSucceeded77719592', DetailType: 'FULL', EventTypeIds: [ @@ -46,7 +46,7 @@ test('notifications rule', () => { ], }); - expect(stack).toHaveResource('AWS::CodeStarNotifications::NotificationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeStarNotifications::NotificationRule', { Name: 'MyCodebuildProjectNotifyOnBuildFailedF680E310', DetailType: 'FULL', EventTypeIds: [ diff --git a/packages/@aws-cdk/aws-codebuild/test/project.test.ts b/packages/@aws-cdk/aws-codebuild/test/project.test.ts index bf93885378ac5..5c7a17d7eb40d 100644 --- a/packages/@aws-cdk/aws-codebuild/test/project.test.ts +++ b/packages/@aws-cdk/aws-codebuild/test/project.test.ts @@ -1,5 +1,4 @@ -import { ABSENT, objectLike, ResourcePart, arrayWith } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as logs from '@aws-cdk/aws-logs'; @@ -24,7 +23,7 @@ test('can use filename as buildspec', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Source: { BuildSpec: 'hello.yml', }, @@ -41,7 +40,7 @@ test('can use buildspec literal', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Source: { BuildSpec: '{\n "phases": [\n "say hi"\n ]\n}', }, @@ -67,7 +66,7 @@ test('can use yamlbuildspec literal', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Source: { BuildSpec: 'text: text\ndecimal: 10\nlist:\n - say hi\nobj:\n text: text\n decimal: 10\n list:\n - say hi\n', }, @@ -110,7 +109,7 @@ describe('GitHub source', () => { }); // THEN - expect(stack).toHaveResource('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Source: { Type: 'GITHUB', Location: 'https://github.com/testowner/testrepo.git', @@ -134,7 +133,7 @@ describe('GitHub source', () => { }); // THEN - expect(stack).toHaveResource('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { SourceVersion: 'testbranch', }); }); @@ -153,7 +152,7 @@ describe('GitHub source', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Source: { ReportBuildStatus: false, }, @@ -174,7 +173,7 @@ describe('GitHub source', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Triggers: { Webhook: true, }, @@ -207,7 +206,7 @@ describe('GitHub source', () => { }); // THEN - expect(stack).toHaveResource('AWS::CodeBuild::SourceCredential', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::SourceCredential', { 'ServerType': 'GITHUB', 'AuthType': 'PERSONAL_ACCESS_TOKEN', 'Token': 'my-access-token', @@ -229,7 +228,7 @@ describe('GitHub Enterprise source', () => { }); // THEN - expect(stack).toHaveResource('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { SourceVersion: 'testbranch', }); }); @@ -244,7 +243,7 @@ describe('GitHub Enterprise source', () => { }); // THEN - expect(stack).toHaveResource('AWS::CodeBuild::SourceCredential', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::SourceCredential', { 'ServerType': 'GITHUB_ENTERPRISE', 'AuthType': 'PERSONAL_ACCESS_TOKEN', 'Token': 'my-access-token', @@ -267,7 +266,7 @@ describe('BitBucket source', () => { }); // THEN - expect(stack).toHaveResource('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { SourceVersion: 'testbranch', }); }); @@ -283,7 +282,7 @@ describe('BitBucket source', () => { }); // THEN - expect(stack).toHaveResource('AWS::CodeBuild::SourceCredential', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::SourceCredential', { 'ServerType': 'BITBUCKET', 'AuthType': 'BASIC_AUTH', 'Username': 'my-username', @@ -303,10 +302,10 @@ describe('caching', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Cache: { Type: 'NO_CACHE', - Location: ABSENT, + Location: Match.absent(), }, }); }); @@ -327,7 +326,7 @@ describe('caching', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Cache: { Type: 'S3', Location: { @@ -361,7 +360,7 @@ describe('caching', () => { }); // THEN - expect(stack).toHaveResource('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { SourceVersion: 's3version', }); }); @@ -381,7 +380,7 @@ describe('caching', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Cache: { Type: 'LOCAL', Modes: [ @@ -406,10 +405,10 @@ describe('caching', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Cache: { Type: 'NO_CACHE', - Location: ABSENT, + Location: Match.absent(), }, }); }); @@ -434,18 +433,18 @@ test('if a role is shared between projects in a VPC, the VPC Policy is only atta // - 1 is for `ec2:CreateNetworkInterfacePermission`, deduplicated as they're part of a single policy // - 1 is for `ec2:CreateNetworkInterface`, this is the separate Policy we're deduplicating // We would have found 3 if the deduplication didn't work. - expect(stack).toCountResources('AWS::IAM::Policy', 2); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 2); // THEN - both Projects have a DependsOn on the same policy - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResource('AWS::CodeBuild::Project', { Properties: { Name: 'P1' }, DependsOn: ['Project1PolicyDocumentF9761562'], - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResource('AWS::CodeBuild::Project', { Properties: { Name: 'P1' }, DependsOn: ['Project1PolicyDocumentF9761562'], - }, ResourcePart.CompleteDefinition); + }); }); test('can use an imported Role for a Project within a VPC', () => { @@ -462,7 +461,7 @@ test('can use an imported Role for a Project within a VPC', () => { vpc, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { // no need to do any assertions }); }); @@ -484,15 +483,15 @@ test('can use an imported Role with mutable = false for a Project within a VPC', vpc, }); - expect(stack).toCountResources('AWS::IAM::Policy', 0); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 0); // Check that the CodeBuild project does not have a DependsOn - expect(stack).toHaveResource('AWS::CodeBuild::Project', (res: any) => { + Template.fromStack(stack).hasResource('AWS::CodeBuild::Project', (res: any) => { if (res.DependsOn && res.DependsOn.length > 0) { throw new Error(`CodeBuild project should have no DependsOn, but got: ${JSON.stringify(res, undefined, 2)}`); } return true; - }, ResourcePart.CompleteDefinition); + }); }); test('can use an ImmutableRole for a Project within a VPC', () => { @@ -512,15 +511,15 @@ test('can use an ImmutableRole for a Project within a VPC', () => { vpc, }); - expect(stack).toCountResources('AWS::IAM::Policy', 0); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 0); // Check that the CodeBuild project does not have a DependsOn - expect(stack).toHaveResource('AWS::CodeBuild::Project', (res: any) => { + Template.fromStack(stack).hasResource('AWS::CodeBuild::Project', (res: any) => { if (res.DependsOn && res.DependsOn.length > 0) { throw new Error(`CodeBuild project should have no DependsOn, but got: ${JSON.stringify(res, undefined, 2)}`); } return true; - }, ResourcePart.CompleteDefinition); + }); }); test('metric method generates a valid CloudWatch metric', () => { @@ -557,7 +556,7 @@ describe('CodeBuild test reports group', () => { }); reportGroup.grantWrite(project); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ {}, @@ -598,8 +597,8 @@ describe('Environment', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: objectLike({ + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { + Environment: Match.objectLike({ RegistryCredential: { CredentialProvider: 'SECRETS_MANAGER', Credential: { 'Ref': 'SecretA720EF05' }, @@ -625,8 +624,8 @@ describe('Environment', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: objectLike({ + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { + Environment: Match.objectLike({ RegistryCredential: { CredentialProvider: 'SECRETS_MANAGER', Credential: 'MySecretName', @@ -655,8 +654,8 @@ describe('Environment', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { - LogsConfig: objectLike({ + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { + LogsConfig: Match.objectLike({ CloudWatchLogs: { GroupName: 'MyLogGroupName', Status: 'ENABLED', @@ -684,8 +683,8 @@ describe('Environment', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { - LogsConfig: objectLike({ + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { + LogsConfig: Match.objectLike({ CloudWatchLogs: { Status: 'DISABLED', }, @@ -713,8 +712,8 @@ describe('Environment', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { - LogsConfig: objectLike({ + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { + LogsConfig: Match.objectLike({ S3Logs: { Location: 'mybucketname/my-logs', Status: 'ENABLED', @@ -746,8 +745,8 @@ describe('Environment', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { - LogsConfig: objectLike({ + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { + LogsConfig: Match.objectLike({ CloudWatchLogs: { GroupName: 'MyLogGroupName', Status: 'ENABLED', @@ -780,8 +779,8 @@ describe('Environment', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: objectLike({ + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { + Environment: Match.objectLike({ Certificate: { 'Fn::Join': ['', [ 'arn:', @@ -818,8 +817,8 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: objectLike({ + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { + Environment: Match.objectLike({ EnvironmentVariables: [{ Name: 'ENV_VAR1', Type: 'PARAMETER_STORE', @@ -855,9 +854,9 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith(objectLike({ + 'Statement': Match.arrayWith([Match.objectLike({ 'Action': 'ssm:GetParameters', 'Effect': 'Allow', 'Resource': [{ @@ -900,7 +899,7 @@ describe('EnvironmentVariables', () => { ], ], }], - })), + })]), }, }); }); @@ -928,14 +927,14 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).not.toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', Match.not({ 'PolicyDocument': { - 'Statement': arrayWith(objectLike({ + 'Statement': Match.arrayWith([Match.objectLike({ 'Action': 'ssm:GetParameters', 'Effect': 'Allow', - })), + })]), }, - }); + })); }); }); @@ -955,7 +954,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -968,9 +967,9 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': { @@ -984,7 +983,7 @@ describe('EnvironmentVariables', () => { ':secret:my-secret-??????', ]], }, - }), + }]), }, }); }); @@ -1004,7 +1003,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1017,9 +1016,9 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': { @@ -1033,7 +1032,7 @@ describe('EnvironmentVariables', () => { ':secret:my-secret-??????', ]], }, - }), + }]), }, }); }); @@ -1053,7 +1052,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1066,26 +1065,26 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': 'arn:aws:secretsmanager:us-west-2:123456789012:secret:my-secret-123456*', - }), + }]), }, }); // THEN - expect(stack).not.toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', Match.not({ 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'kms:Decrypt', 'Effect': 'Allow', 'Resource': 'arn:aws:kms:us-west-2:123456789012:key/*', - }), + }]), }, - }); + })); }); test('can be provided as a verbatim partial secret ARN', () => { @@ -1103,7 +1102,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1116,26 +1115,26 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': 'arn:aws:secretsmanager:us-west-2:123456789012:secret:mysecret*', - }), + }]), }, }); // THEN - expect(stack).not.toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', Match.not({ 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'kms:Decrypt', 'Effect': 'Allow', 'Resource': 'arn:aws:kms:us-west-2:123456789012:key/*', - }), + }]), }, - }); + })); }); test("when provided as a verbatim partial secret ARN from another account, adds permission to decrypt keys in the Secret's account", () => { @@ -1156,13 +1155,13 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'kms:Decrypt', 'Effect': 'Allow', 'Resource': 'arn:aws:kms:us-west-2:901234567890:key/*', - }), + }]), }, }); }); @@ -1189,13 +1188,13 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'kms:Decrypt', 'Effect': 'Allow', 'Resource': 'arn:aws:kms:us-west-2:901234567890:key/*', - }), + }]), }, }); }); @@ -1216,7 +1215,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1229,13 +1228,13 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': { 'Ref': 'SecretA720EF05' }, - }), + }]), }, }); }); @@ -1260,7 +1259,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1278,13 +1277,13 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': { 'Ref': 'SecretA720EF05' }, - }), + }]), }, }); }); @@ -1305,7 +1304,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1323,13 +1322,13 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': { 'Ref': 'SecretA720EF05' }, - }), + }]), }, }); }); @@ -1350,7 +1349,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1363,9 +1362,9 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': { @@ -1379,7 +1378,7 @@ describe('EnvironmentVariables', () => { ':secret:mysecret-??????', ]], }, - }), + }]), }, }); }); @@ -1401,7 +1400,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1414,13 +1413,13 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': 'arn:aws:secretsmanager:us-west-2:123456789012:secret:mysecret*', - }), + }]), }, }); }); @@ -1442,7 +1441,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1455,13 +1454,13 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': 'arn:aws:secretsmanager:us-west-2:123456789012:secret:mysecret-123456*', - }), + }]), }, }); }); @@ -1488,7 +1487,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1508,9 +1507,9 @@ describe('EnvironmentVariables', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': { @@ -1522,13 +1521,13 @@ describe('EnvironmentVariables', () => { ':012345678912:secret:secret-name-??????', ]], }, - }), + }]), }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'kms:Decrypt', 'Effect': 'Allow', 'Resource': { @@ -1540,7 +1539,7 @@ describe('EnvironmentVariables', () => { ':012345678912:key/*', ]], }, - }), + }]), }, }); }); @@ -1567,7 +1566,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1587,9 +1586,9 @@ describe('EnvironmentVariables', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': { @@ -1601,13 +1600,13 @@ describe('EnvironmentVariables', () => { ':012345678912:secret:secret-name*', ]], }, - }), + }]), }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'kms:Decrypt', 'Effect': 'Allow', 'Resource': { @@ -1619,7 +1618,7 @@ describe('EnvironmentVariables', () => { ':012345678912:key/*', ]], }, - }), + }]), }, }); }); @@ -1644,7 +1643,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1656,23 +1655,23 @@ describe('EnvironmentVariables', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': `${secretArn}*`, - }), + }]), }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'kms:Decrypt', 'Effect': 'Allow', 'Resource': 'arn:aws:kms:us-west-2:901234567890:key/*', - }), + }]), }, }); }); @@ -1744,7 +1743,7 @@ describe('Timeouts', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { QueuedTimeoutInMinutes: 30, }); }); @@ -1763,7 +1762,7 @@ describe('Timeouts', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { TimeoutInMinutes: 30, }); }); @@ -1784,7 +1783,7 @@ describe('Maximum concurrency', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { ConcurrentBuildLimit: 1, }); }); diff --git a/packages/@aws-cdk/aws-codebuild/test/report-group.test.ts b/packages/@aws-cdk/aws-codebuild/test/report-group.test.ts index 4459ef4672f0c..6e653d891f3fb 100644 --- a/packages/@aws-cdk/aws-codebuild/test/report-group.test.ts +++ b/packages/@aws-cdk/aws-codebuild/test/report-group.test.ts @@ -1,5 +1,4 @@ -import { ABSENT, ResourcePart } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; @@ -15,11 +14,11 @@ describe('Test Reports Groups', () => { new codebuild.ReportGroup(stack, 'ReportGroup'); - expect(stack).toHaveResourceLike('AWS::CodeBuild::ReportGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::ReportGroup', { "Type": "TEST", "ExportConfig": { "ExportConfigType": "NO_EXPORT", - "S3Destination": ABSENT, + "S3Destination": Match.absent(), }, }); }); @@ -31,7 +30,7 @@ describe('Test Reports Groups', () => { reportGroupName: 'my-report-group', }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::ReportGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::ReportGroup', { "Name": 'my-report-group', }); }); @@ -51,7 +50,7 @@ describe('Test Reports Groups', () => { })); expect(reportGroup.reportGroupName).toEqual('my-report-group'); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { "PolicyDocument": { "Statement": [ { @@ -80,15 +79,15 @@ describe('Test Reports Groups', () => { exportBucket: s3.Bucket.fromBucketName(stack, 'Bucket', 'my-bucket'), }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::ReportGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::ReportGroup', { "Type": "TEST", "ExportConfig": { "ExportConfigType": "S3", "S3Destination": { "Bucket": "my-bucket", - "EncryptionKey": ABSENT, - "EncryptionDisabled": ABSENT, - "Packaging": ABSENT, + "EncryptionKey": Match.absent(), + "EncryptionDisabled": Match.absent(), + "Packaging": Match.absent(), }, }, }); @@ -106,7 +105,7 @@ describe('Test Reports Groups', () => { zipExport: true, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::ReportGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::ReportGroup', { "Type": "TEST", "ExportConfig": { "ExportConfigType": "S3", @@ -125,10 +124,10 @@ describe('Test Reports Groups', () => { new codebuild.ReportGroup(stack, 'ReportGroup'); - expect(stack).toHaveResourceLike('AWS::CodeBuild::ReportGroup', { + Template.fromStack(stack).hasResource('AWS::CodeBuild::ReportGroup', { "DeletionPolicy": "Retain", "UpdateReplacePolicy": "Retain", - }, ResourcePart.CompleteDefinition); + }); }); test('can be created with RemovalPolicy.DESTROY', () => { @@ -138,9 +137,9 @@ describe('Test Reports Groups', () => { removalPolicy: cdk.RemovalPolicy.DESTROY, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::ReportGroup', { + Template.fromStack(stack).hasResource('AWS::CodeBuild::ReportGroup', { "DeletionPolicy": "Delete", "UpdateReplacePolicy": "Delete", - }, ResourcePart.CompleteDefinition); + }); }); }); diff --git a/packages/@aws-cdk/aws-codebuild/test/untrusted-code-boundary.test.ts b/packages/@aws-cdk/aws-codebuild/test/untrusted-code-boundary.test.ts index 89933d11a7ab8..af1f4fdfb328f 100644 --- a/packages/@aws-cdk/aws-codebuild/test/untrusted-code-boundary.test.ts +++ b/packages/@aws-cdk/aws-codebuild/test/untrusted-code-boundary.test.ts @@ -1,5 +1,4 @@ -import { arrayWith } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as codebuild from '../lib'; @@ -15,7 +14,7 @@ test('can attach permissions boundary to Project', () => { iam.PermissionsBoundary.of(project).apply(new codebuild.UntrustedCodeBoundaryPolicy(stack, 'Boundary')); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { PermissionsBoundary: { Ref: 'BoundaryEA298153' }, }); }); @@ -38,13 +37,13 @@ test('can add additional statements Boundary', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::ManagedPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::ManagedPolicy', { PolicyDocument: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Effect: 'Allow', Action: 'a:a', Resource: 'b', - }), + }]), }, }); -}); \ No newline at end of file +}); diff --git a/packages/@aws-cdk/aws-codecommit/README.md b/packages/@aws-cdk/aws-codecommit/README.md index ea263c6d3ab3c..7575b5cba68ec 100644 --- a/packages/@aws-cdk/aws-codecommit/README.md +++ b/packages/@aws-cdk/aws-codecommit/README.md @@ -19,11 +19,9 @@ see the [AWS CodeCommit documentation](https://docs.aws.amazon.com/codecommit). To add a CodeCommit Repository to your stack: ```ts -import * as codecommit from '@aws-cdk/aws-codecommit'; - const repo = new codecommit.Repository(this, 'Repository', { - repositoryName: 'MyRepositoryName', - description: 'Some description.', // optional property + repositoryName: 'MyRepositoryName', + description: 'Some description.', // optional property }); ``` @@ -33,6 +31,8 @@ property to clone your repository. To add an Amazon SNS trigger to your repository: ```ts +declare const repo: codecommit.Repository; + // trigger is established for all repository actions on all branches by default. repo.notify('arn:aws:sns:*:123456789012:my_topic'); ``` @@ -45,12 +45,9 @@ It provides methods for loading code from a directory, `.zip` file and from a pr Example: ```ts -import * as codecommit from '@aws-cdk/aws-codecommit'; -import * as path from 'path'; - const repo = new codecommit.Repository(this, 'Repository', { - repositoryName: 'MyRepositoryName', - code: codecommit.Code.fromDirectory(path.join(__dirname, 'directory/'), 'develop'), // optional property, branch parameter can be omitted + repositoryName: 'MyRepositoryName', + code: codecommit.Code.fromDirectory(path.join(__dirname, 'directory/'), 'develop'), // optional property, branch parameter can be omitted }); ``` @@ -61,15 +58,22 @@ Use the `repo.onXxx` methods to define rules that trigger on these events and invoke targets as a result: ```ts +import * as sns from '@aws-cdk/aws-sns'; +import * as targets from '@aws-cdk/aws-events-targets'; + +declare const repo: codecommit.Repository; +declare const project: codebuild.PipelineProject; +declare const myTopic: sns.Topic; + // starts a CodeBuild project when a commit is pushed to the "master" branch of the repo repo.onCommit('CommitToMaster', { - target: new targets.CodeBuildProject(project), - branches: ['master'], + target: new targets.CodeBuildProject(project), + branches: ['master'], }); // publishes a message to an Amazon SNS topic when a comment is made on a pull request const rule = repo.onCommentOnPullRequest('CommentOnPullRequest', { - target: new targets.SnsTopic(myTopic), + target: new targets.SnsTopic(myTopic), }); ``` @@ -79,9 +83,13 @@ To define CodeStar Notification rules for Repositories, use one of the `notifyOn They are very similar to `onXxx()` methods for CloudWatch events: ```ts -const target = new chatbot.SlackChannelConfiguration(stack, 'MySlackChannel', { +import * as chatbot from '@aws-cdk/aws-chatbot'; + +declare const repository: codecommit.Repository; +const target = new chatbot.SlackChannelConfiguration(this, '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/package.json b/packages/@aws-cdk/aws-codecommit/package.json index 5af28b2a5b1bb..108604988db31 100644 --- a/packages/@aws-cdk/aws-codecommit/package.json +++ b/packages/@aws-cdk/aws-codecommit/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -77,16 +84,16 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "aws-sdk": "^2.848.0", - "jest": "^27.4.5" + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-codestarnotifications": "0.0.0", diff --git a/packages/@aws-cdk/aws-codecommit/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-codecommit/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..f3da3c09182a1 --- /dev/null +++ b/packages/@aws-cdk/aws-codecommit/rosetta/default.ts-fixture @@ -0,0 +1,13 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as codecommit from '@aws-cdk/aws-codecommit'; +import * as codebuild from '@aws-cdk/aws-codebuild'; +import * as path from 'path'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codecommit/test/codecommit.test.ts b/packages/@aws-cdk/aws-codecommit/test/codecommit.test.ts index 3dca30532e1ad..623f66eb4675a 100644 --- a/packages/@aws-cdk/aws-codecommit/test/codecommit.test.ts +++ b/packages/@aws-cdk/aws-codecommit/test/codecommit.test.ts @@ -1,5 +1,5 @@ -import '@aws-cdk/assert-internal/jest'; import { join, resolve } from 'path'; +import { Template } from '@aws-cdk/assertions'; import { Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import { Asset } from '@aws-cdk/aws-s3-assets'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; @@ -19,7 +19,7 @@ describe('codecommit', () => { new Repository(stack, 'MyRepository', props).notify(snsArn); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyRepository4C4BD5FC: { Type: 'AWS::CodeCommit::Repository', @@ -258,7 +258,7 @@ describe('codecommit', () => { repository.grantPullPush(role); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { diff --git a/packages/@aws-cdk/aws-codecommit/test/notification-rule.test.ts b/packages/@aws-cdk/aws-codecommit/test/notification-rule.test.ts index 721ce01d4c490..ae65a5f4f2ef8 100644 --- a/packages/@aws-cdk/aws-codecommit/test/notification-rule.test.ts +++ b/packages/@aws-cdk/aws-codecommit/test/notification-rule.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as sns from '@aws-cdk/aws-sns'; import * as cdk from '@aws-cdk/core'; import * as codecommit from '../lib'; @@ -16,7 +16,7 @@ describe('notification rule', () => { repository.notifyOnPullRequestMerged('NotifyOnPullRequestMerged', target); - expect(stack).toHaveResource('AWS::CodeStarNotifications::NotificationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeStarNotifications::NotificationRule', { Name: 'MyCodecommitRepositoryNotifyOnPullRequestCreatedBB14EA32', DetailType: 'FULL', EventTypeIds: [ @@ -38,7 +38,7 @@ describe('notification rule', () => { ], }); - expect(stack).toHaveResource('AWS::CodeStarNotifications::NotificationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeStarNotifications::NotificationRule', { Name: 'MyCodecommitRepositoryNotifyOnPullRequestMerged34A7EDF1', DetailType: 'FULL', EventTypeIds: [ diff --git a/packages/@aws-cdk/aws-codedeploy/package.json b/packages/@aws-cdk/aws-codedeploy/package.json index 52f0fe7223228..ff289e51bbe4c 100644 --- a/packages/@aws-cdk/aws-codedeploy/package.json +++ b/packages/@aws-cdk/aws-codedeploy/package.json @@ -82,13 +82,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-autoscaling": "0.0.0", diff --git a/packages/@aws-cdk/aws-codedeploy/test/ecs/application.test.ts b/packages/@aws-cdk/aws-codedeploy/test/ecs/application.test.ts index 6a6966167cd2f..ec130559aaff8 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/ecs/application.test.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/ecs/application.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as codedeploy from '../../lib'; @@ -7,7 +7,7 @@ describe('CodeDeploy ECS Application', () => { const stack = new cdk.Stack(); new codedeploy.EcsApplication(stack, 'MyApp'); - expect(stack).toHaveResource('AWS::CodeDeploy::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::Application', { ComputePlatform: 'ECS', }); }); @@ -18,7 +18,7 @@ describe('CodeDeploy ECS Application', () => { applicationName: 'my-name', }); - expect(stack).toHaveResource('AWS::CodeDeploy::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::Application', { ApplicationName: 'my-name', ComputePlatform: 'ECS', }); diff --git a/packages/@aws-cdk/aws-codedeploy/test/lambda/application.test.ts b/packages/@aws-cdk/aws-codedeploy/test/lambda/application.test.ts index eaa140d0c045c..6ccbd816935ba 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/lambda/application.test.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/lambda/application.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as codedeploy from '../../lib'; @@ -6,7 +6,7 @@ describe('CodeDeploy Lambda Application', () => { test('can be created', () => { const stack = new cdk.Stack(); new codedeploy.LambdaApplication(stack, 'MyApp'); - expect(stack).toHaveResource('AWS::CodeDeploy::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::Application', { ComputePlatform: 'Lambda', }); }); @@ -16,7 +16,7 @@ describe('CodeDeploy Lambda Application', () => { new codedeploy.LambdaApplication(stack, 'MyApp', { applicationName: 'my-name', }); - expect(stack).toHaveResource('AWS::CodeDeploy::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::Application', { ApplicationName: 'my-name', ComputePlatform: 'Lambda', }); diff --git a/packages/@aws-cdk/aws-codedeploy/test/lambda/custom-deployment-config.test.ts b/packages/@aws-cdk/aws-codedeploy/test/lambda/custom-deployment-config.test.ts index 9f69328613dac..7755402502857 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/lambda/custom-deployment-config.test.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/lambda/custom-deployment-config.test.ts @@ -1,5 +1,4 @@ -import { ResourcePart } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; import * as codedeploy from '../../lib'; @@ -45,7 +44,7 @@ test('custom resource created', () => { }); // THEN - expect(stack).toHaveResourceLike('Custom::AWS', { + Template.fromStack(stack).hasResourceProperties('Custom::AWS', { ServiceToken: { 'Fn::GetAtt': [ 'AWS679f53fac002430cb0da5b7982bd22872D164C4C', @@ -57,7 +56,7 @@ test('custom resource created', () => { Delete: '{"service":"CodeDeploy","action":"deleteDeploymentConfig","parameters":{"deploymentConfigName":"CustomConfig.LambdaCanary5Percent1Minutes"}}', }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -91,7 +90,7 @@ test('custom resource created with specific name', () => { }); // THEN - expect(stack).toHaveResourceLike('Custom::AWS', { + Template.fromStack(stack).hasResourceProperties('Custom::AWS', { Create: '{"service":"CodeDeploy","action":"createDeploymentConfig","parameters":{"deploymentConfigName":"MyDeploymentConfig","computePlatform":"Lambda","trafficRoutingConfig":{"type":"TimeBasedCanary","timeBasedCanary":{"canaryInterval":"1","canaryPercentage":"5"}}},"physicalResourceId":{"id":"MyDeploymentConfig"}}', Update: '{"service":"CodeDeploy","action":"createDeploymentConfig","parameters":{"deploymentConfigName":"MyDeploymentConfig","computePlatform":"Lambda","trafficRoutingConfig":{"type":"TimeBasedCanary","timeBasedCanary":{"canaryInterval":"1","canaryPercentage":"5"}}},"physicalResourceId":{"id":"MyDeploymentConfig"}}', Delete: '{"service":"CodeDeploy","action":"deleteDeploymentConfig","parameters":{"deploymentConfigName":"MyDeploymentConfig"}}', @@ -112,7 +111,7 @@ test('can create linear custom config', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { DeploymentConfigName: 'CustomConfig.LambdaLinear5PercentEvery1Minutes', }); }); @@ -131,7 +130,7 @@ test('can create canary custom config', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { DeploymentConfigName: 'CustomConfig.LambdaCanary5Percent1Minutes', }); }); @@ -150,7 +149,7 @@ test('dependency on the config exists to ensure ordering', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResource('AWS::CodeDeploy::DeploymentGroup', { Properties: { DeploymentConfigName: 'CustomConfig.LambdaCanary5Percent1Minutes', }, @@ -158,5 +157,5 @@ test('dependency on the config exists to ensure ordering', () => { 'CustomConfigDeploymentConfigCustomResourcePolicy0426B684', 'CustomConfigDeploymentConfigE9E1F384', ], - }, ResourcePart.CompleteDefinition); + }); }); diff --git a/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts b/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts index 1f4b18e852cff..365a03c4d5d30 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts @@ -1,5 +1,4 @@ -import { ResourcePart } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; @@ -33,7 +32,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { ApplicationName: { Ref: 'MyApp3CE31C26', }, @@ -56,7 +55,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }, }); - expect(stack).toHaveResource('AWS::Lambda::Alias', { + Template.fromStack(stack).hasResource('AWS::Lambda::Alias', { Type: 'AWS::Lambda::Alias', Properties: { FunctionName: { @@ -80,15 +79,23 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }, }, }, - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [{ Action: 'sts:AssumeRole', Effect: 'Allow', Principal: { - Service: { 'Fn::Join': ['', ['codedeploy.', { Ref: 'AWS::Region' }, '.', { Ref: 'AWS::URLSuffix' }]] }, + Service: { + 'Fn::FindInMap': [ + 'ServiceprincipalMap', + { + Ref: 'AWS::Region', + }, + 'codedeploy', + ], + }, }, }], Version: '2012-10-17', @@ -120,7 +127,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { deploymentGroupName: 'test', }); - expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { DeploymentGroupName: 'test', }); }); @@ -140,7 +147,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { role: serviceRole, }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [{ Action: 'sts:AssumeRole', @@ -176,7 +183,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { deploymentConfig: codedeploy.LambdaDeploymentConfig.LINEAR_10PERCENT_EVERY_1MINUTE, }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { ApplicationName: { Ref: 'MyApp3CE31C26', }, @@ -216,7 +223,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { })], }); - expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { AlarmConfiguration: { Alarms: [{ Name: { @@ -268,7 +275,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, }); - expect(stack).toHaveResourceLike('AWS::Lambda::Alias', { + Template.fromStack(stack).hasResource('AWS::Lambda::Alias', { UpdatePolicy: { CodeDeployLambdaAliasUpdate: { ApplicationName: { @@ -282,9 +289,9 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }, }, }, - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyName: 'MyDGServiceRoleDefaultPolicy65E8E1EA', Roles: [{ Ref: 'MyDGServiceRole5E94FD88', @@ -316,7 +323,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }); group.addPreHook(mockFunction(stack, 'PreHook')); - expect(stack).toHaveResourceLike('AWS::Lambda::Alias', { + Template.fromStack(stack).hasResource('AWS::Lambda::Alias', { UpdatePolicy: { CodeDeployLambdaAliasUpdate: { ApplicationName: { @@ -330,9 +337,9 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }, }, }, - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyName: 'MyDGServiceRoleDefaultPolicy65E8E1EA', Roles: [{ Ref: 'MyDGServiceRole5E94FD88', @@ -364,7 +371,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, }); - expect(stack).toHaveResourceLike('AWS::Lambda::Alias', { + Template.fromStack(stack).hasResource('AWS::Lambda::Alias', { UpdatePolicy: { CodeDeployLambdaAliasUpdate: { ApplicationName: { @@ -378,9 +385,9 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }, }, }, - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyName: 'MyDGServiceRoleDefaultPolicy65E8E1EA', Roles: [{ Ref: 'MyDGServiceRole5E94FD88', @@ -412,7 +419,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }); group.addPostHook(mockFunction(stack, 'PostHook')); - expect(stack).toHaveResourceLike('AWS::Lambda::Alias', { + Template.fromStack(stack).hasResource('AWS::Lambda::Alias', { UpdatePolicy: { CodeDeployLambdaAliasUpdate: { ApplicationName: { @@ -426,9 +433,9 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }, }, }, - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyName: 'MyDGServiceRoleDefaultPolicy65E8E1EA', Roles: [{ Ref: 'MyDGServiceRole5E94FD88', @@ -467,7 +474,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { })], }); - expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { AlarmConfiguration: { Alarms: [{ Name: { @@ -494,7 +501,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }, }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { ApplicationName: { Ref: 'MyApp3CE31C26', }, @@ -526,7 +533,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { AutoRollbackConfiguration: { Enabled: true, Events: [ @@ -557,7 +564,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { })], }); - expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { AutoRollbackConfiguration: { Enabled: true, Events: [ diff --git a/packages/@aws-cdk/aws-codedeploy/test/server/deployment-config.test.ts b/packages/@aws-cdk/aws-codedeploy/test/server/deployment-config.test.ts index 2838bacfdc7fc..52652e8024b28 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/server/deployment-config.test.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/server/deployment-config.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as codedeploy from '../../lib'; @@ -12,7 +12,7 @@ describe('CodeDeploy DeploymentConfig', () => { minimumHealthyHosts: codedeploy.MinimumHealthyHosts.count(1), }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentConfig', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentConfig', { 'MinimumHealthyHosts': { 'Type': 'HOST_COUNT', 'Value': 1, @@ -27,7 +27,7 @@ describe('CodeDeploy DeploymentConfig', () => { minimumHealthyHosts: codedeploy.MinimumHealthyHosts.percentage(75), }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentConfig', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentConfig', { 'MinimumHealthyHosts': { 'Type': 'FLEET_PERCENT', 'Value': 75, diff --git a/packages/@aws-cdk/aws-codedeploy/test/server/deployment-group.test.ts b/packages/@aws-cdk/aws-codedeploy/test/server/deployment-group.test.ts index 79d20323d5a21..43acaadc3e7fc 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/server/deployment-group.test.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/server/deployment-group.test.ts @@ -1,5 +1,4 @@ -import { SynthUtils } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; @@ -18,7 +17,7 @@ describe('CodeDeploy Server Deployment Group', () => { application, }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'ApplicationName': { 'Ref': 'MyApp3CE31C26', }, @@ -36,9 +35,8 @@ describe('CodeDeploy Server Deployment Group', () => { value: serverDeploymentGroup.application.applicationName, }); - expect(stack2).toHaveOutput({ - outputName: 'Output', - outputValue: 'defaultmydgapplication78dba0bb0c7580b32033', + Template.fromStack(stack2).hasOutput('Output', { + Value: 'defaultmydgapplication78dba0bb0c7580b32033', }); }); @@ -68,7 +66,7 @@ describe('CodeDeploy Server Deployment Group', () => { installAgent: true, }); - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { 'UserData': { 'Fn::Base64': { 'Fn::Join': [ @@ -104,7 +102,7 @@ describe('CodeDeploy Server Deployment Group', () => { installAgent: true, }); - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { 'UserData': { 'Fn::Base64': { 'Fn::Join': [ @@ -135,7 +133,7 @@ describe('CodeDeploy Server Deployment Group', () => { autoScalingGroups: [asg], }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'AutoScalingGroups': [ { 'Ref': 'ASG46ED3070', @@ -156,7 +154,7 @@ describe('CodeDeploy Server Deployment Group', () => { const deploymentGroup = new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup'); deploymentGroup.addAutoScalingGroup(asg); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'AutoScalingGroups': [ { 'Ref': 'ASG46ED3070', @@ -178,7 +176,7 @@ describe('CodeDeploy Server Deployment Group', () => { loadBalancer: codedeploy.LoadBalancer.application(targetGroup), }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'LoadBalancerInfo': { 'TargetGroupInfoList': [ { @@ -210,7 +208,7 @@ describe('CodeDeploy Server Deployment Group', () => { loadBalancer: codedeploy.LoadBalancer.network(targetGroup), }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'LoadBalancerInfo': { 'TargetGroupInfoList': [ { @@ -241,7 +239,7 @@ describe('CodeDeploy Server Deployment Group', () => { ), }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'Ec2TagSet': { 'Ec2TagSetList': [ { @@ -276,7 +274,7 @@ describe('CodeDeploy Server Deployment Group', () => { ), }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'OnPremisesTagSet': { 'OnPremisesTagSetList': [ { @@ -343,7 +341,7 @@ describe('CodeDeploy Server Deployment Group', () => { const deploymentGroup = new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup'); deploymentGroup.addAlarm(alarm); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'AlarmConfiguration': { 'Alarms': [ { @@ -362,7 +360,7 @@ describe('CodeDeploy Server Deployment Group', () => { new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup'); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'AutoRollbackConfiguration': { 'Enabled': true, 'Events': [ @@ -391,7 +389,7 @@ describe('CodeDeploy Server Deployment Group', () => { }); deploymentGroup.addAlarm(alarm); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'AutoRollbackConfiguration': { 'Enabled': true, 'Events': [ @@ -402,7 +400,8 @@ describe('CodeDeploy Server Deployment Group', () => { }); test('setting to roll back on alarms without providing any results in an exception', () => { - const stack = new cdk.Stack(); + const app = new cdk.App(); + const stack = new cdk.Stack(app); new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { autoRollback: { @@ -410,7 +409,7 @@ describe('CodeDeploy Server Deployment Group', () => { }, }); - expect(() => SynthUtils.toCloudFormation(stack)).toThrow(/deploymentInAlarm/); + expect(() => app.synth()).toThrow(/deploymentInAlarm/); }); test('can be used with an imported ALB Target Group as the load balancer', () => { @@ -424,7 +423,7 @@ describe('CodeDeploy Server Deployment Group', () => { ), }); - expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'LoadBalancerInfo': { 'TargetGroupInfoList': [ { diff --git a/packages/@aws-cdk/aws-codeguruprofiler/README.md b/packages/@aws-cdk/aws-codeguruprofiler/README.md index 09a2ea2d6b731..6c8c54954a40e 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/README.md +++ b/packages/@aws-cdk/aws-codeguruprofiler/README.md @@ -17,7 +17,7 @@ Amazon CodeGuru Profiler collects runtime performance data from your live applic Import to your project: -```ts +```ts nofixture import * as codeguruprofiler from '@aws-cdk/aws-codeguruprofiler'; ``` @@ -27,11 +27,11 @@ Here's how to setup a profiling group and give your compute role permissions to ```ts // The execution role of your application that publishes to the ProfilingGroup via CodeGuru Profiler Profiling Agent. (the following is merely an example) -const publishAppRole = new Role(stack, 'PublishAppRole', { - assumedBy: new AccountRootPrincipal(), +const publishAppRole = new iam.Role(this, 'PublishAppRole', { + assumedBy: new iam.AccountRootPrincipal(), }); -const profilingGroup = new ProfilingGroup(stack, 'MyProfilingGroup'); +const profilingGroup = new codeguruprofiler.ProfilingGroup(this, 'MyProfilingGroup'); profilingGroup.grantPublish(publishAppRole); ``` @@ -41,7 +41,7 @@ Code Guru Profiler supports multiple compute environments. They can be configured when creating a Profiling Group by using the `computePlatform` property: ```ts -const profilingGroup = new ProfilingGroup(stack, 'MyProfilingGroup', { - computePlatform: ComputePlatform.AWS_LAMBDA, +const profilingGroup = new codeguruprofiler.ProfilingGroup(this, 'MyProfilingGroup', { + computePlatform: codeguruprofiler.ComputePlatform.AWS_LAMBDA, }); ``` diff --git a/packages/@aws-cdk/aws-codeguruprofiler/package.json b/packages/@aws-cdk/aws-codeguruprofiler/package.json index c83bfc85306d0..65ab5a7d206dc 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/package.json +++ b/packages/@aws-cdk/aws-codeguruprofiler/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -74,12 +81,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-iam": "0.0.0", diff --git a/packages/@aws-cdk/aws-codeguruprofiler/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-codeguruprofiler/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..c809dffd674cb --- /dev/null +++ b/packages/@aws-cdk/aws-codeguruprofiler/rosetta/default.ts-fixture @@ -0,0 +1,12 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as codeguruprofiler from '@aws-cdk/aws-codeguruprofiler'; +import * as iam from '@aws-cdk/aws-iam'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} diff --git a/packages/@aws-cdk/aws-codeguruprofiler/test/profiling-group.test.ts b/packages/@aws-cdk/aws-codeguruprofiler/test/profiling-group.test.ts index 00212902bbec6..94f54c65945c4 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/test/profiling-group.test.ts +++ b/packages/@aws-cdk/aws-codeguruprofiler/test/profiling-group.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import { AccountRootPrincipal, Role } from '@aws-cdk/aws-iam'; import { Stack } from '@aws-cdk/core'; import { ProfilingGroup, ComputePlatform } from '../lib'; @@ -6,7 +6,6 @@ import { ProfilingGroup, ComputePlatform } from '../lib'; /* eslint-disable quote-props */ describe('profiling group', () => { - test('attach read permission to Profiling group via fromProfilingGroupArn', () => { const stack = new Stack(); // dummy role to test out read permissions on ProfilingGroup @@ -17,7 +16,7 @@ describe('profiling group', () => { const profilingGroup = ProfilingGroup.fromProfilingGroupArn(stack, 'MyProfilingGroup', 'arn:aws:codeguru-profiler:us-east-1:1234567890:profilingGroup/MyAwesomeProfilingGroup'); profilingGroup.grantRead(readAppRole); - expect(stack).toMatch({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'ReadAppRole52FE6317': { 'Type': 'AWS::IAM::Role', @@ -89,7 +88,7 @@ describe('profiling group', () => { const profilingGroup = ProfilingGroup.fromProfilingGroupName(stack, 'MyProfilingGroup', 'MyAwesomeProfilingGroup'); profilingGroup.grantPublish(publishAppRole); - expect(stack).toMatch({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'PublishAppRole9FEBD682': { 'Type': 'AWS::IAM::Role', @@ -176,7 +175,7 @@ describe('profiling group', () => { profilingGroupName: 'MyAwesomeProfilingGroup', }); - expect(stack).toMatch({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyProfilingGroup829F0507': { 'Type': 'AWS::CodeGuruProfiler::ProfilingGroup', @@ -195,9 +194,9 @@ describe('profiling group', () => { computePlatform: ComputePlatform.AWS_LAMBDA, }); - expect(stack).to(haveResourceLike('AWS::CodeGuruProfiler::ProfilingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeGuruProfiler::ProfilingGroup', { 'ComputePlatform': 'AWSLambda', - })); + }); }); test('default profiling group without name', () => { @@ -205,7 +204,7 @@ describe('profiling group', () => { new ProfilingGroup(stack, 'MyProfilingGroup', { }); - expect(stack).toMatch({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyProfilingGroup829F0507': { 'Type': 'AWS::CodeGuruProfiler::ProfilingGroup', @@ -222,7 +221,7 @@ describe('profiling group', () => { new ProfilingGroup(stack, 'MyProfilingGroupWithAReallyLongProfilingGroupNameThatExceedsTheLimitOfProfilingGroupNameSize_InOrderToDoSoTheNameMustBeGreaterThanTwoHundredAndFiftyFiveCharacters_InSuchCasesWePickUpTheFirstOneTwentyCharactersFromTheBeginningAndTheEndAndConcatenateThemToGetTheIdentifier', { }); - expect(stack).toMatch({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyProfilingGroupWithAReallyLongProfilingGroupNameThatExceedsTheLimitOfProfilingGroupNameSizeInOrderToDoSoTheNameMustBeGreaterThanTwoHundredAndFiftyFiveCharactersInSuchCasesWePickUpTheFirstOneTwentyCharactersFromTheBeginningAndTheEndAndConca4B39908C': { 'Type': 'AWS::CodeGuruProfiler::ProfilingGroup', @@ -245,7 +244,7 @@ describe('profiling group', () => { profilingGroup.grantPublish(publishAppRole); - expect(stack).toMatch({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyProfilingGroup829F0507': { 'Type': 'AWS::CodeGuruProfiler::ProfilingGroup', @@ -329,7 +328,7 @@ describe('profiling group', () => { profilingGroup.grantRead(readAppRole); - expect(stack).toMatch({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyProfilingGroup829F0507': { 'Type': 'AWS::CodeGuruProfiler::ProfilingGroup', @@ -401,5 +400,4 @@ describe('profiling group', () => { }, }); }); - }); diff --git a/packages/@aws-cdk/aws-codegurureviewer/package.json b/packages/@aws-cdk/aws-codegurureviewer/package.json index ddc4dab7bf420..e0e2d1748e1ae 100644 --- a/packages/@aws-cdk/aws-codegurureviewer/package.json +++ b/packages/@aws-cdk/aws-codegurureviewer/package.json @@ -7,6 +7,13 @@ "jsii": { "outdir": "dist", "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + }, "targets": { "dotnet": { "namespace": "Amazon.CDK.AWS.CodeGuruReviewer", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-codepipeline-actions/README.md b/packages/@aws-cdk/aws-codepipeline-actions/README.md index d3569b4ea5872..7169f72f2cddd 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/README.md +++ b/packages/@aws-cdk/aws-codepipeline-actions/README.md @@ -607,7 +607,7 @@ directly from a CodeCommit repository, with a manual approval step in between to See [the AWS documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/continuous-delivery-codepipeline.html) for more details about using CloudFormation in CodePipeline. -#### Actions defined by this package +#### Actions for updating individual CloudFormation Stacks This package contains the following CloudFormation actions: @@ -620,6 +620,57 @@ This package contains the following CloudFormation actions: changes from the people (or system) applying the changes. * **CloudFormationExecuteChangeSetAction** - Execute a change set prepared previously. +#### Actions for deploying CloudFormation StackSets to multiple accounts + +You can use CloudFormation StackSets to deploy the same CloudFormation template to multiple +accounts in a managed way. If you use AWS Organizations, StackSets can be deployed to +all accounts in a particular Organizational Unit (OU), and even automatically to new +accounts as soon as they are added to a particular OU. For more information, see +the [Working with StackSets](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/what-is-cfnstacksets.html) +section of the CloudFormation developer guide. + +The actions available for updating StackSets are: + +* **CloudFormationDeployStackSetAction** - Create or update a CloudFormation StackSet directly from the pipeline, optionally + immediately create and update Stack Instances as well. +* **CloudFormationDeployStackInstancesAction** - Update outdated Stack Instaces using the current version of the StackSet. + +Here's an example of using both of these actions: + +```ts +declare const pipeline: codepipeline.Pipeline; +declare const sourceOutput: codepipeline.Artifact; + +pipeline.addStage({ + stageName: 'DeployStackSets', + actions: [ + // First, update the StackSet itself with the newest template + new codepipeline_actions.CloudFormationDeployStackSetAction({ + actionName: 'UpdateStackSet', + runOrder: 1, + stackSetName: 'MyStackSet', + template: codepipeline_actions.StackSetTemplate.fromArtifactPath(sourceOutput.atPath('template.yaml')), + + // Change this to 'StackSetDeploymentModel.organizations()' if you want to deploy to OUs + deploymentModel: codepipeline_actions.StackSetDeploymentModel.selfManaged(), + // This deploys to a set of accounts + stackInstances: codepipeline_actions.StackInstances.inAccounts(['111111111111'], ['us-east-1', 'eu-west-1']), + }), + + // Afterwards, update/create additional instances in other accounts + new codepipeline_actions.CloudFormationDeployStackInstancesAction({ + actionName: 'AddMoreInstances', + runOrder: 2, + stackSetName: 'MyStackSet', + stackInstances: codepipeline_actions.StackInstances.inAccounts( + ['222222222222', '333333333333'], + ['us-east-1', 'eu-west-1'] + ), + }), + ], +}); +``` + #### Lambda deployed through CodePipeline If you want to deploy your Lambda through CodePipeline, @@ -764,6 +815,39 @@ const deployStage = pipeline.addStage({ [image definition file]: https://docs.aws.amazon.com/codepipeline/latest/userguide/pipelines-create.html#pipelines-create-image-definitions +#### Deploying ECS applications to existing services + +CodePipeline can deploy to an existing ECS service which uses the +[ECS service ARN format that contains the Cluster name](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html#ecs-resource-ids). +This also works if the service is in a different account and/or region than the pipeline: + +```ts +import * as ecs from '@aws-cdk/aws-ecs'; + +const service = ecs.BaseService.fromServiceArnWithCluster(this, 'EcsService', + 'arn:aws:ecs:us-east-1:123456789012:service/myClusterName/myServiceName' +); +const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); +const buildOutput = new codepipeline.Artifact(); +// add source and build stages to the pipeline as usual... +const deployStage = pipeline.addStage({ + stageName: 'Deploy', + actions: [ + new codepipeline_actions.EcsDeployAction({ + actionName: 'DeployAction', + service: service, + input: buildOutput, + }), + ], +}); +``` + +When deploying across accounts, especially in a CDK Pipelines self-mutating pipeline, +it is recommended to provide the `role` property to the `EcsDeployAction`. +The Role will need to have permissions assigned to it for ECS deployment. +See [the CodePipeline documentation](https://docs.aws.amazon.com/codepipeline/latest/userguide/how-to-custom-role.html#how-to-update-role-new-services) +for the permissions needed. + #### Deploying ECS applications stored in a separate source code repository The idiomatic CDK way of deploying an ECS application is to have your Dockerfiles and your CDK code in the same source code repository, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/index.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/index.ts new file mode 100644 index 0000000000000..413b0eccf60f5 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/index.ts @@ -0,0 +1,4 @@ +export * from './pipeline-actions'; +export * from './stackset-action'; +export * from './stackinstances-action'; +export * from './stackset-types'; diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/pipeline-actions.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/pipeline-actions.ts index 63618e086ed91..23f8185cd9c3b 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/pipeline-actions.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/pipeline-actions.ts @@ -3,6 +3,7 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import { Action } from '../action'; +import { parseCapabilities, SingletonPolicy } from './private/singleton-policy'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main // eslint-disable-next-line no-duplicate-imports, import/order @@ -512,148 +513,3 @@ export class CloudFormationDeleteStackAction extends CloudFormationDeployAction }; } } - -/** - * Manages a bunch of singleton-y statements on the policy of an IAM Role. - * Dedicated methods can be used to add specific permissions to the role policy - * using as few statements as possible (adding resources to existing compatible - * statements instead of adding new statements whenever possible). - * - * Statements created outside of this class are not considered when adding new - * permissions. - */ -class SingletonPolicy extends Construct implements iam.IGrantable { - /** - * Obtain a SingletonPolicy for a given role. - * @param role the Role this policy is bound to. - * @returns the SingletonPolicy for this role. - */ - public static forRole(role: iam.IRole): SingletonPolicy { - const found = role.node.tryFindChild(SingletonPolicy.UUID); - return (found as SingletonPolicy) || new SingletonPolicy(role); - } - - private static readonly UUID = '8389e75f-0810-4838-bf64-d6f85a95cf83'; - - public readonly grantPrincipal: iam.IPrincipal; - - private statements: { [key: string]: iam.PolicyStatement } = {}; - - private constructor(private readonly role: iam.IRole) { - super(role as unknown as cdk.Construct, SingletonPolicy.UUID); - this.grantPrincipal = role; - } - - public grantExecuteChangeSet(props: { stackName: string, changeSetName: string, region?: string }): void { - this.statementFor({ - actions: [ - 'cloudformation:DescribeStacks', - 'cloudformation:DescribeChangeSet', - 'cloudformation:ExecuteChangeSet', - ], - conditions: { StringEqualsIfExists: { 'cloudformation:ChangeSetName': props.changeSetName } }, - }).addResources(this.stackArnFromProps(props)); - } - - public grantCreateReplaceChangeSet(props: { stackName: string, changeSetName: string, region?: string }): void { - this.statementFor({ - actions: [ - 'cloudformation:CreateChangeSet', - 'cloudformation:DeleteChangeSet', - 'cloudformation:DescribeChangeSet', - 'cloudformation:DescribeStacks', - ], - conditions: { StringEqualsIfExists: { 'cloudformation:ChangeSetName': props.changeSetName } }, - }).addResources(this.stackArnFromProps(props)); - } - - public grantCreateUpdateStack(props: { stackName: string, replaceOnFailure?: boolean, region?: string }): void { - const actions = [ - 'cloudformation:DescribeStack*', - 'cloudformation:CreateStack', - 'cloudformation:UpdateStack', - 'cloudformation:GetTemplate*', - 'cloudformation:ValidateTemplate', - 'cloudformation:GetStackPolicy', - 'cloudformation:SetStackPolicy', - ]; - if (props.replaceOnFailure) { - actions.push('cloudformation:DeleteStack'); - } - this.statementFor({ actions }).addResources(this.stackArnFromProps(props)); - } - - public grantDeleteStack(props: { stackName: string, region?: string }): void { - this.statementFor({ - actions: [ - 'cloudformation:DescribeStack*', - 'cloudformation:DeleteStack', - ], - }).addResources(this.stackArnFromProps(props)); - } - - public grantPassRole(role: iam.IRole): void { - this.statementFor({ actions: ['iam:PassRole'] }).addResources(role.roleArn); - } - - private statementFor(template: StatementTemplate): iam.PolicyStatement { - const key = keyFor(template); - if (!(key in this.statements)) { - this.statements[key] = new iam.PolicyStatement({ actions: template.actions }); - if (template.conditions) { - this.statements[key].addConditions(template.conditions); - } - this.role.addToPolicy(this.statements[key]); - } - return this.statements[key]; - - function keyFor(props: StatementTemplate): string { - const actions = `${props.actions.sort().join('\x1F')}`; - const conditions = formatConditions(props.conditions); - return `${actions}\x1D${conditions}`; - - function formatConditions(cond?: StatementCondition): string { - if (cond == null) { return ''; } - let result = ''; - for (const op of Object.keys(cond).sort()) { - result += `${op}\x1E`; - const condition = cond[op]; - for (const attribute of Object.keys(condition).sort()) { - const value = condition[attribute]; - result += `${value}\x1F`; - } - } - return result; - } - } - } - - private stackArnFromProps(props: { stackName: string, region?: string }): string { - return cdk.Stack.of(this).formatArn({ - region: props.region, - service: 'cloudformation', - resource: 'stack', - resourceName: `${props.stackName}/*`, - }); - } -} - -interface StatementTemplate { - actions: string[]; - conditions?: StatementCondition; -} - -type StatementCondition = { [op: string]: { [attribute: string]: string } }; - -function parseCapabilities(capabilities: cdk.CfnCapabilities[] | undefined): string | undefined { - if (capabilities === undefined) { - return undefined; - } else if (capabilities.length === 1) { - const capability = capabilities.toString(); - return (capability === '') ? undefined : capability; - } else if (capabilities.length > 1) { - return capabilities.join(','); - } - - return undefined; -} diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/private/singleton-policy.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/private/singleton-policy.ts new file mode 100644 index 0000000000000..7a9a3efdde784 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/private/singleton-policy.ts @@ -0,0 +1,172 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +/** + * Manages a bunch of singleton-y statements on the policy of an IAM Role. + * Dedicated methods can be used to add specific permissions to the role policy + * using as few statements as possible (adding resources to existing compatible + * statements instead of adding new statements whenever possible). + * + * Statements created outside of this class are not considered when adding new + * permissions. + */ +export class SingletonPolicy extends Construct implements iam.IGrantable { + /** + * Obtain a SingletonPolicy for a given role. + * @param role the Role this policy is bound to. + * @returns the SingletonPolicy for this role. + */ + public static forRole(role: iam.IRole): SingletonPolicy { + const found = role.node.tryFindChild(SingletonPolicy.UUID); + return (found as SingletonPolicy) || new SingletonPolicy(role); + } + + private static readonly UUID = '8389e75f-0810-4838-bf64-d6f85a95cf83'; + + public readonly grantPrincipal: iam.IPrincipal; + + private statements: { [key: string]: iam.PolicyStatement } = {}; + + private constructor(private readonly role: iam.IRole) { + super(role as unknown as Construct, SingletonPolicy.UUID); + this.grantPrincipal = role; + } + + public grantExecuteChangeSet(props: { stackName: string, changeSetName: string, region?: string }): void { + this.statementFor({ + actions: [ + 'cloudformation:DescribeStacks', + 'cloudformation:DescribeChangeSet', + 'cloudformation:ExecuteChangeSet', + ], + conditions: { StringEqualsIfExists: { 'cloudformation:ChangeSetName': props.changeSetName } }, + }).addResources(this.stackArnFromProps(props)); + } + + public grantCreateReplaceChangeSet(props: { stackName: string, changeSetName: string, region?: string }): void { + this.statementFor({ + actions: [ + 'cloudformation:CreateChangeSet', + 'cloudformation:DeleteChangeSet', + 'cloudformation:DescribeChangeSet', + 'cloudformation:DescribeStacks', + ], + conditions: { StringEqualsIfExists: { 'cloudformation:ChangeSetName': props.changeSetName } }, + }).addResources(this.stackArnFromProps(props)); + } + + public grantCreateUpdateStack(props: { stackName: string, replaceOnFailure?: boolean, region?: string }): void { + const actions = [ + 'cloudformation:DescribeStack*', + 'cloudformation:CreateStack', + 'cloudformation:UpdateStack', + 'cloudformation:GetTemplate*', + 'cloudformation:ValidateTemplate', + 'cloudformation:GetStackPolicy', + 'cloudformation:SetStackPolicy', + ]; + if (props.replaceOnFailure) { + actions.push('cloudformation:DeleteStack'); + } + this.statementFor({ actions }).addResources(this.stackArnFromProps(props)); + } + + public grantCreateUpdateStackSet(props: { stackSetName: string, region?: string }): void { + const actions = [ + 'cloudformation:CreateStackSet', + 'cloudformation:UpdateStackSet', + 'cloudformation:DescribeStackSet', + 'cloudformation:DescribeStackSetOperation', + 'cloudformation:ListStackInstances', + 'cloudformation:CreateStackInstances', + ]; + this.statementFor({ actions }).addResources(this.stackSetArnFromProps(props)); + } + + public grantDeleteStack(props: { stackName: string, region?: string }): void { + this.statementFor({ + actions: [ + 'cloudformation:DescribeStack*', + 'cloudformation:DeleteStack', + ], + }).addResources(this.stackArnFromProps(props)); + } + + public grantPassRole(role: iam.IRole | string): void { + this.statementFor({ actions: ['iam:PassRole'] }).addResources(typeof role === 'string' ? role : role.roleArn); + } + + private statementFor(template: StatementTemplate): iam.PolicyStatement { + const key = keyFor(template); + if (!(key in this.statements)) { + this.statements[key] = new iam.PolicyStatement({ actions: template.actions }); + if (template.conditions) { + this.statements[key].addConditions(template.conditions); + } + this.role.addToPolicy(this.statements[key]); + } + return this.statements[key]; + + function keyFor(props: StatementTemplate): string { + const actions = `${props.actions.sort().join('\x1F')}`; + const conditions = formatConditions(props.conditions); + return `${actions}\x1D${conditions}`; + + function formatConditions(cond?: StatementCondition): string { + if (cond == null) { return ''; } + let result = ''; + for (const op of Object.keys(cond).sort()) { + result += `${op}\x1E`; + const condition = cond[op]; + for (const attribute of Object.keys(condition).sort()) { + const value = condition[attribute]; + result += `${value}\x1F`; + } + } + return result; + } + } + } + + private stackArnFromProps(props: { stackName: string, region?: string }): string { + return cdk.Stack.of(this).formatArn({ + region: props.region, + service: 'cloudformation', + resource: 'stack', + resourceName: `${props.stackName}/*`, + }); + } + + private stackSetArnFromProps(props: { stackSetName: string, region?: string }): string { + return cdk.Stack.of(this).formatArn({ + region: props.region, + service: 'cloudformation', + resource: 'stackset', + resourceName: `${props.stackSetName}:*`, + }); + } +} + +export interface StatementTemplate { + actions: string[]; + conditions?: StatementCondition; +} + +export type StatementCondition = { [op: string]: { [attribute: string]: string } }; + +export function parseCapabilities(capabilities: cdk.CfnCapabilities[] | undefined): string | undefined { + if (capabilities === undefined) { + return undefined; + } else if (capabilities.length === 1) { + const capability = capabilities.toString(); + return (capability === '') ? undefined : capability; + } else if (capabilities.length > 1) { + return capabilities.join(','); + } + + return undefined; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/stackinstances-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/stackinstances-action.ts new file mode 100644 index 0000000000000..e0ddf6c9c1cb6 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/stackinstances-action.ts @@ -0,0 +1,97 @@ +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import { Action } from '../action'; +import { validatePercentage } from '../common'; +import { SingletonPolicy } from './private/singleton-policy'; +import { CommonCloudFormationStackSetOptions, StackInstances, StackSetParameters } from './stackset-types'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +/** + * Properties for the CloudFormationDeployStackInstancesAction + */ +export interface CloudFormationDeployStackInstancesActionProps extends codepipeline.CommonAwsActionProps, CommonCloudFormationStackSetOptions { + /** + * The name of the StackSet we are adding instances to + */ + readonly stackSetName: string; + + /** + * Specify where to create or update Stack Instances + * + * You can specify either AWS Accounts Ids or AWS Organizations Organizational Units. + */ + readonly stackInstances: StackInstances; + + /** + * Parameter values that only apply to the current Stack Instances + * + * These parameters are shared between all instances added by this action. + * + * @default - no parameters will be overridden + */ + readonly parameterOverrides?: StackSetParameters; +} + +/** + * CodePipeline action to create/update Stack Instances of a StackSet + * + * After the initial creation of a stack set, you can add new stack instances by + * using CloudFormationStackInstances. Template parameter values can be + * overridden at the stack instance level during create or update stack set + * instance operations. + * + * Each stack set has one template and set of template parameters. When you + * update the template or template parameters, you update them for the entire + * set. Then all instance statuses are set to OUTDATED until the changes are + * deployed to that instance. + */ +export class CloudFormationDeployStackInstancesAction extends Action { + private readonly props: CloudFormationDeployStackInstancesActionProps; + + constructor(props: CloudFormationDeployStackInstancesActionProps) { + super({ + ...props, + region: props.stackSetRegion, + provider: 'CloudFormationStackInstances', + category: codepipeline.ActionCategory.DEPLOY, + artifactBounds: { + minInputs: 0, + maxInputs: 3, + minOutputs: 0, + maxOutputs: 0, + }, + inputs: [ + ...props.parameterOverrides?._artifactsReferenced ?? [], + ...props.stackInstances?._artifactsReferenced ?? [], + ], + }); + + this.props = props; + + validatePercentage('failureTolerancePercentage', props.failureTolerancePercentage); + validatePercentage('maxAccountConcurrencyPercentage', props.maxAccountConcurrencyPercentage); + } + + protected bound(scope: CoreConstruct, _stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): codepipeline.ActionConfig { + const singletonPolicy = SingletonPolicy.forRole(options.role); + singletonPolicy.grantCreateUpdateStackSet(this.props); + + const instancesResult = this.props.stackInstances?._bind(scope); + + if ((this.actionProperties.inputs || []).length > 0) { + options.bucket.grantRead(singletonPolicy); + } + + return { + configuration: { + StackSetName: this.props.stackSetName, + ParameterOverrides: this.props.parameterOverrides?._render(), + FailureTolerancePercentage: this.props.failureTolerancePercentage, + MaxConcurrentPercentage: this.props.maxAccountConcurrencyPercentage, + ...instancesResult?.stackSetConfiguration, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/stackset-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/stackset-action.ts new file mode 100644 index 0000000000000..574e69f86a96c --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/stackset-action.ts @@ -0,0 +1,174 @@ +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as cdk from '@aws-cdk/core'; +import { Action } from '../action'; +import { validatePercentage } from '../common'; +import { parseCapabilities, SingletonPolicy } from './private/singleton-policy'; +import { CommonCloudFormationStackSetOptions, StackInstances, StackSetDeploymentModel, StackSetParameters, StackSetTemplate } from './stackset-types'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +/** + * Properties for the CloudFormationDeployStackSetAction + */ +export interface CloudFormationDeployStackSetActionProps extends codepipeline.CommonAwsActionProps, CommonCloudFormationStackSetOptions { + /** + * The name to associate with the stack set. This name must be unique in the Region where it is created. + * + * The name may only contain alphanumeric and hyphen characters. It must begin with an alphabetic character and be 128 characters or fewer. + */ + readonly stackSetName: string; + + /** + * The location of the template that defines the resources in the stack set. + * This must point to a template with a maximum size of 460,800 bytes. + * + * Enter the path to the source artifact name and template file. + */ + readonly template: StackSetTemplate; + + /** + * A description of the stack set. You can use this to describe the stack set’s purpose or other relevant information. + * + * @default - no description + */ + readonly description?: string; + + /** + * Specify where to create or update Stack Instances + * + * You can specify either AWS Accounts Ids or AWS Organizations Organizational Units. + * + * @default - don't create or update any Stack Instances + */ + readonly stackInstances?: StackInstances; + + /** + * Determines how IAM roles are created and managed. + * + * The choices are: + * + * - Self Managed: you create IAM roles with the required permissions + * in the administration account and all target accounts. + * - Service Managed: only available if the account and target accounts + * are part of an AWS Organization. The necessary roles will be created + * for you. + * + * If you want to deploy to all accounts that are a member of AWS + * Organizations Organizational Units (OUs), you must select Service Managed + * permissions. + * + * Note: This parameter can only be changed when no stack instances exist in + * the stack set. + * + * @default StackSetDeploymentModel.selfManaged() + */ + readonly deploymentModel?: StackSetDeploymentModel; + + /** + * The template parameters for your stack set + * + * These parameters are shared between all instances of the stack set. + * + * @default - no parameters will be used + */ + readonly parameters?: StackSetParameters; + + /** + * Indicates that the template can create and update resources, depending on the types of resources in the template. + * + * You must use this property if you have IAM resources in your stack template or you create a stack directly from a template containing macros. + * + * @default - the StackSet will have no IAM capabilities + */ + readonly cfnCapabilities?: cdk.CfnCapabilities[]; +} + +/** + * CodePipeline action to deploy a stackset. + * + * CodePipeline offers the ability to perform AWS CloudFormation StackSets + * operations as part of your CI/CD process. You use a stack set to create + * stacks in AWS accounts across AWS Regions by using a single AWS + * CloudFormation template. All the resources included in each stack are defined + * by the stack set’s AWS CloudFormation template. When you create the stack + * set, you specify the template to use, as well as any parameters and + * capabilities that the template requires. + * + * For more information about concepts for AWS CloudFormation StackSets, see + * [StackSets + * concepts](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-concepts.html) + * in the AWS CloudFormation User Guide. + * + * If you use this action to make an update that includes adding stack + * instances, the new instances are deployed first and the update is completed + * last. The new instances first receive the old version, and then the update is + * applied to all instances. + * + * As a best practice, you should construct your pipeline so that the stack set + * is created and initially deploys to a subset or a single instance. After you + * test your deployment and view the generated stack set, then add the + * CloudFormationStackInstances action so that the remaining instances are + * created and updated. + */ +export class CloudFormationDeployStackSetAction extends Action { + private readonly props: CloudFormationDeployStackSetActionProps; + private readonly deploymentModel: StackSetDeploymentModel; + + constructor(props: CloudFormationDeployStackSetActionProps) { + super({ + ...props, + region: props.stackSetRegion, + provider: 'CloudFormationStackSet', + category: codepipeline.ActionCategory.DEPLOY, + artifactBounds: { + minInputs: 1, + maxInputs: 3, + minOutputs: 0, + maxOutputs: 0, + }, + inputs: [ + ...props.template._artifactsReferenced ?? [], + ...props.parameters?._artifactsReferenced ?? [], + ...props.stackInstances?._artifactsReferenced ?? [], + ], + }); + + this.props = props; + this.deploymentModel = props.deploymentModel ?? StackSetDeploymentModel.selfManaged(); + + validatePercentage('failureTolerancePercentage', props.failureTolerancePercentage); + validatePercentage('maxAccountConcurrencyPercentage', props.maxAccountConcurrencyPercentage); + } + + protected bound(scope: CoreConstruct, _stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): codepipeline.ActionConfig { + const singletonPolicy = SingletonPolicy.forRole(options.role); + singletonPolicy.grantCreateUpdateStackSet(this.props); + + const instancesResult = this.props.stackInstances?._bind(scope); + const permissionModelBind = this.deploymentModel?._bind(scope); + + for (const role of permissionModelBind?.passedRoles ?? []) { + singletonPolicy.grantPassRole(role); + } + + if ((this.actionProperties.inputs || []).length > 0) { + options.bucket.grantRead(singletonPolicy); + } + + return { + configuration: { + StackSetName: this.props.stackSetName, + Description: this.props.description, + TemplatePath: this.props.template._render(), + Parameters: this.props.parameters?._render(), + Capabilities: parseCapabilities(this.props.cfnCapabilities), + FailureTolerancePercentage: this.props.failureTolerancePercentage, + MaxConcurrentPercentage: this.props.maxAccountConcurrencyPercentage, + ...instancesResult?.stackSetConfiguration, + ...permissionModelBind?.stackSetConfiguration, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/stackset-types.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/stackset-types.ts new file mode 100644 index 0000000000000..319d96d732997 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/stackset-types.ts @@ -0,0 +1,511 @@ +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +/** + * Options in common between both StackSet actions + */ +export interface CommonCloudFormationStackSetOptions { + + /** + * The percentage of accounts per Region for which this stack operation can fail before AWS CloudFormation stops the operation in that Region. If + * the operation is stopped in a Region, AWS CloudFormation doesn't attempt the operation in subsequent Regions. When calculating the number + * of accounts based on the specified percentage, AWS CloudFormation rounds down to the next whole number. + * + * @default 0% + */ + readonly failureTolerancePercentage?: number; + + /** + * The maximum percentage of accounts in which to perform this operation at one time. When calculating the number of accounts based on the specified + * percentage, AWS CloudFormation rounds down to the next whole number. If rounding down would result in zero, AWS CloudFormation sets the number as + * one instead. Although you use this setting to specify the maximum, for large deployments the actual number of accounts acted upon concurrently + * may be lower due to service throttling. + * + * @default 1% + */ + readonly maxAccountConcurrencyPercentage?: number; + + /** + * The AWS Region the StackSet is in. + * + * Note that a cross-region Pipeline requires replication buckets to function correctly. + * You can provide their names with the `PipelineProps.crossRegionReplicationBuckets` property. + * If you don't, the CodePipeline Construct will create new Stacks in your CDK app containing those buckets, + * that you will need to `cdk deploy` before deploying the main, Pipeline-containing Stack. + * + * @default - same region as the Pipeline + */ + readonly stackSetRegion?: string; +} + +/** + * The source of a StackSet template + */ +export abstract class StackSetTemplate { + /** + * Use a file in an artifact as Stack Template. + */ + public static fromArtifactPath(artifactPath: codepipeline.ArtifactPath): StackSetTemplate { + return new class extends StackSetTemplate { + public readonly _artifactsReferenced?: codepipeline.Artifact[] | undefined = [artifactPath.artifact]; + + public _render() { + return artifactPath.location; + } + }(); + } + + /** + * Which artifacts are referenced by this template + * + * Does not need to be called by app builders. + * + * @internal + */ + public abstract readonly _artifactsReferenced?: codepipeline.Artifact[] | undefined; + + /** + * Render the template to the pipeline + * + * Does not need to be called by app builders. + * + * @internal + */ + public abstract _render(): any; +} + +/** + * Where Stack Instances will be created from the StackSet + */ +export abstract class StackInstances { + /** + * Create stack instances in a set of accounts and regions passed as literal lists + * + * Stack Instances will be created in every combination of region and account. + * + * > NOTE: `StackInstances.inAccounts()` and `StackInstances.inOrganizationalUnits()` + * > have exactly the same behavior, and you can use them interchangeably if you want. + * > The only difference between them is that your code clearly indicates what entity + * > it's working with. + */ + public static inAccounts(accounts: string[], regions: string[]): StackInstances { + return StackInstances.fromList(accounts, regions); + } + + /** + * Create stack instances in all accounts in a set of Organizational Units (OUs) and regions passed as literal lists + * + * If you want to deploy to Organization Units, you must choose have created the StackSet + * with `deploymentModel: DeploymentModel.organizations()`. + * + * Stack Instances will be created in every combination of region and account. + * + * > NOTE: `StackInstances.inAccounts()` and `StackInstances.inOrganizationalUnits()` + * > have exactly the same behavior, and you can use them interchangeably if you want. + * > The only difference between them is that your code clearly indicates what entity + * > it's working with. + */ + public static inOrganizationalUnits(ous: string[], regions: string[]): StackInstances { + return StackInstances.fromList(ous, regions); + } + + /** + * Create stack instances in a set of accounts or organizational units taken from the pipeline artifacts, and a set of regions + * + * The file must be a JSON file containing a list of strings. For example: + * + * ```json + * [ + * "111111111111", + * "222222222222", + * "333333333333" + * ] + * ``` + * + * Stack Instances will be created in every combination of region and account, or region and + * Organizational Units (OUs). + * + * If this is set of Organizational Units, you must have selected `StackSetDeploymentModel.organizations()` + * as deployment model. + */ + public static fromArtifactPath(artifactPath: codepipeline.ArtifactPath, regions: string[]): StackInstances { + if (regions.length === 0) { + throw new Error("'regions' may not be an empty list"); + } + + return new class extends StackInstances { + public readonly _artifactsReferenced?: codepipeline.Artifact[] | undefined = [artifactPath.artifact]; + public _bind(_scope: Construct): StackInstancesBindResult { + return { + stackSetConfiguration: { + DeploymentTargets: artifactPath.location, + Regions: regions.join(','), + }, + }; + } + }(); + } + + /** + * Create stack instances in a literal set of accounts or organizational units, and a set of regions + * + * Stack Instances will be created in every combination of region and account, or region and + * Organizational Units (OUs). + * + * If this is set of Organizational Units, you must have selected `StackSetDeploymentModel.organizations()` + * as deployment model. + */ + private static fromList(targets: string[], regions: string[]): StackInstances { + if (targets.length === 0) { + throw new Error("'targets' may not be an empty list"); + } + + if (regions.length === 0) { + throw new Error("'regions' may not be an empty list"); + } + + return new class extends StackInstances { + public _bind(_scope: Construct): StackInstancesBindResult { + return { + stackSetConfiguration: { + DeploymentTargets: targets.join(','), + Regions: regions.join(','), + }, + }; + } + }(); + } + + + /** + * The artifacts referenced by the properties of this deployment target + * + * Does not need to be called by app builders. + * + * @internal + */ + readonly _artifactsReferenced?: codepipeline.Artifact[]; + + /** + * Called to attach the stack set instances to a stackset action + * + * Does not need to be called by app builders. + * + * @internal + */ + public abstract _bind(scope: Construct): StackInstancesBindResult; +} + +/** + * Returned by the StackInstances.bind() function + * + * Does not need to be used by app builders. + * + * @internal + */ +export interface StackInstancesBindResult { + /** + * Properties to mix into the Action configuration + */ + readonly stackSetConfiguration: any; +} + +/** + * Base parameters for the StackSet + */ +export abstract class StackSetParameters { + /** + * A list of template parameters for your stack set. + * + * You must specify all template parameters. Parameters you don't specify will revert + * to their `Default` values as specified in the template. + * + * Specify the names of parameters you want to retain their existing values, + * without specifying what those values are, in an array in the second + * argument to this function. Use of this feature is discouraged. CDK is for + * specifying desired-state infrastructure, and use of this feature makes the + * parameter values unmanaged. + * + * @example + * + * const parameters = codepipeline_actions.StackSetParameters.fromLiteral({ + * BucketName: 'my-bucket', + * Asset1: 'true', + * }); + */ + public static fromLiteral(parameters: Record, usePreviousValues?: string[]): StackSetParameters { + return new class extends StackSetParameters { + public readonly _artifactsReferenced: codepipeline.Artifact[] = []; + + _render(): string { + return [ + ...Object.entries(parameters).map(([key, value]) => + `ParameterKey=${key},ParameterValue=${value}`), + ...(usePreviousValues ?? []).map((key) => + `ParameterKey=${key},UsePreviousValue=true`), + ].join(' '); + } + }(); + } + + /** + * Read the parameters from a JSON file from one of the pipeline's artifacts + * + * The file needs to contain a list of `{ ParameterKey, ParameterValue, UsePreviousValue }` objects, like + * this: + * + * ``` + * [ + * { + * "ParameterKey": "BucketName", + * "ParameterValue": "my-bucket" + * }, + * { + * "ParameterKey": "Asset1", + * "ParameterValue": "true" + * }, + * { + * "ParameterKey": "Asset2", + * "UsePreviousValue": true + * } + * ] + * ``` + * + * You must specify all template parameters. Parameters you don't specify will revert + * to their `Default` values as specified in the template. + * + * For of parameters you want to retain their existing values + * without specifying what those values are, set `UsePreviousValue: true`. + * Use of this feature is discouraged. CDK is for + * specifying desired-state infrastructure, and use of this feature makes the + * parameter values unmanaged. + */ + public static fromArtifactPath(artifactPath: codepipeline.ArtifactPath): StackSetParameters { + return new class extends StackSetParameters { + public _artifactsReferenced: codepipeline.Artifact[] = [artifactPath.artifact]; + + public _render(): string { + return artifactPath.location; + } + }(); + } + + /** + * Artifacts referenced by this parameter set + * + * @internal + */ + public abstract readonly _artifactsReferenced: codepipeline.Artifact[]; + + /** + * Converts Parameters to a string. + * + * @internal + */ + public abstract _render(): string; +} + +/** + * Determines how IAM roles are created and managed. + */ +export abstract class StackSetDeploymentModel { + /** + * Deploy to AWS Organizations accounts. + * + * AWS CloudFormation StackSets automatically creates the IAM roles required + * to deploy to accounts managed by AWS Organizations. This requires an + * account to be a member of an Organization. + * + * Using this deployment model, you can specify either AWS Account Ids or + * Organization Unit Ids in the `stackInstances` parameter. + */ + public static organizations(props: OrganizationsDeploymentProps = {}): StackSetDeploymentModel { + return new class extends StackSetDeploymentModel { + _bind() { + return { + stackSetConfiguration: { + PermissionModel: 'SERVICE_MANAGED', + OrganizationsAutoDeployment: props.autoDeployment, + }, + }; + } + }(); + } + + /** + * Deploy to AWS Accounts not managed by AWS Organizations + * + * You are responsible for creating Execution Roles in every account you will + * be deploying to in advance to create the actual stack instances. Unless you + * specify overrides, StackSets expects the execution roles you create to have + * the default name `AWSCloudFormationStackSetExecutionRole`. See the [Grant + * self-managed + * permissions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-prereqs-self-managed.html) + * section of the CloudFormation documentation. + * + * The CDK will automatically create the central Administration Role in the + * Pipeline account which will be used to assume the Execution Role in each of + * the target accounts. + * + * If you wish to use a pre-created Administration Role, use `Role.fromRoleName()` + * or `Role.fromRoleArn()` to import it, and pass it to this function: + * + * ```ts + * const existingAdminRole = iam.Role.fromRoleName(this, 'AdminRole', 'AWSCloudFormationStackSetAdministrationRole'); + * + * const deploymentModel = codepipeline_actions.StackSetDeploymentModel.selfManaged({ + * // Use an existing Role. Leave this out to create a new Role. + * administrationRole: existingAdminRole, + * }); + * ``` + * + * Using this deployment model, you can only specify AWS Account Ids in the + * `stackInstances` parameter. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-prereqs-self-managed.html + */ + public static selfManaged(props: SelfManagedDeploymentProps = {}): StackSetDeploymentModel { + return new class extends StackSetDeploymentModel { + _bind(scope: Construct) { + let administrationRole = props.administrationRole; + if (!administrationRole) { + administrationRole = new iam.Role(scope, 'StackSetAdministrationRole', { + assumedBy: new iam.ServicePrincipal('cloudformation.amazonaws.com', { + conditions: { + // Confused deputy protection + StringLike: { + 'aws:SourceArn': `arn:${cdk.Aws.PARTITION}:cloudformation:*:${cdk.Aws.ACCOUNT_ID}:stackset/*`, + }, + }, + }), + }); + administrationRole.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['sts:AssumeRole'], + resources: [`arn:${cdk.Aws.PARTITION}:iam::*:role/${props.executionRoleName ?? 'AWSCloudFormationStackSetExecutionRole'}`], + })); + } + + return { + stackSetConfiguration: { + PermissionModel: 'SELF_MANAGED', + AdministrationRoleArn: administrationRole.roleArn, + ExecutionRoleName: props.executionRoleName, + }, + passedRoles: [administrationRole], + } as StackSetDeploymentModelBindResult; + } + }(); + } + + /** + * Bind to the Stack Set action and return the Action configuration + * + * Does not need to be called by app builders. + * + * @internal + */ + public abstract _bind(scope: Construct): StackSetDeploymentModelBindResult; +} + +/** + * Returned by the StackSetDeploymentModel.bind() function + * + * Does not need to be used by app builders. + * + * @internal + */ +export interface StackSetDeploymentModelBindResult { + /** + * Properties to mix into the Action configuration + */ + readonly stackSetConfiguration: any; + + /** + * Roles that need to be passed by the pipeline action + * + * @default - No roles + */ + readonly passedRoles?: iam.IRole[]; +} + +/** + * Properties for configuring service-managed (Organizations) permissions + */ +export interface OrganizationsDeploymentProps { + /** + * Automatically deploy to new accounts added to Organizational Units + * + * Whether AWS CloudFormation StackSets automatically deploys to AWS + * Organizations accounts that are added to a target organization or + * organizational unit (OU). + * + * @default Disabled + */ + readonly autoDeployment?: StackSetOrganizationsAutoDeployment; +} + +/** + * Describes whether AWS CloudFormation StackSets automatically deploys to AWS Organizations accounts that are added to a target organization or + * organizational unit (OU). + */ +export enum StackSetOrganizationsAutoDeployment { + /** + * StackSets automatically deploys additional stack instances to AWS Organizations accounts that are added to a target organization or + * organizational unit (OU) in the specified Regions. If an account is removed from a target organization or OU, AWS CloudFormation StackSets + * deletes stack instances from the account in the specified Regions. + */ + ENABLED = 'Enabled', + + /** + * StackSets does not automatically deploy additional stack instances to AWS Organizations accounts that are added to a target organization or + * organizational unit (OU) in the specified Regions. + */ + DISABLED = 'Disabled', + + /** + * Stack resources are retained when an account is removed from a target organization or OU. + */ + ENABLED_WITH_STACK_RETENTION = 'EnabledWithStackRetention' +} + + +/** + * Properties for configuring self-managed permissions + */ +export interface SelfManagedDeploymentProps { + /** + * The IAM role in the administrator account used to assume execution roles in the target accounts + * + * You must create this role before using the StackSet action. + * + * The role needs to be assumable by CloudFormation, and it needs to be able + * to `sts:AssumeRole` each of the execution roles (whose names are specified + * in the `executionRoleName` parameter) in each of the target accounts. + * + * If you do not specify the role, we assume you have created a role named + * `AWSCloudFormationStackSetAdministrationRole`. + * + * @default - Assume an existing role named `AWSCloudFormationStackSetAdministrationRole` in the same account as the pipeline. + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-prereqs-self-managed.html + */ + readonly administrationRole?: iam.IRole; + + /** + * The name of the IAM role in the target accounts used to perform stack set operations. + * + * You must create these roles in each of the target accounts before using the + * StackSet action. + * + * The roles need to be assumable by by the `administrationRole`, and need to + * have the permissions necessary to successfully create and modify the + * resources that the subsequent CloudFormation deployments need. + * Administrator permissions would be commonly granted to these, but if you can + * scope the permissions down frome there you would be safer. + * + * @default AWSCloudFormationStackSetExecutionRole + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-prereqs-self-managed.html + */ + readonly executionRoleName?: string; +} diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/common.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/common.ts index 7e3aade895ce8..c534661e469da 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/common.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/common.ts @@ -1,4 +1,5 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import { Token } from '@aws-cdk/core'; /** * The ArtifactBounds that make sense for source Actions - @@ -25,3 +26,13 @@ export function deployArtifactBounds(): codepipeline.ActionArtifactBounds { maxOutputs: 0, }; } + +export function validatePercentage(name: string, value?: number) { + if (value === undefined || Token.isUnresolved(value)) { + return; + } + + if (value < 0 || value > 100 || !Number.isInteger(value)) { + throw new Error(`'${name}': must be a whole number between 0 and 100, got: ${value}`); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/index.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/index.ts index e861161ab39c1..a0d665eddd886 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/index.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/index.ts @@ -1,7 +1,7 @@ export * from './alexa-ask/deploy-action'; export * from './bitbucket/source-action'; export * from './codestar-connections/source-action'; -export * from './cloudformation/pipeline-actions'; +export * from './cloudformation'; export * from './codebuild/build-action'; export * from './codecommit/source-action'; export * from './codedeploy/ecs-deploy-action'; diff --git a/packages/@aws-cdk/aws-codepipeline-actions/package.json b/packages/@aws-cdk/aws-codepipeline-actions/package.json index 52c322e836ea4..1bf26f89562e6 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package.json @@ -75,16 +75,17 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-cloudtrail": "0.0.0", "@aws-cdk/aws-codestarnotifications": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "@types/lodash": "^4.14.178", - "jest": "^27.4.5", + "jest": "^27.5.1", "lodash": "^4.17.21" }, "dependencies": { 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 4e3fd6045a251..7d22f2b690015 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,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { arrayWith, objectLike } from '@aws-cdk/assert-internal'; +import { Template, Match } from '@aws-cdk/assertions'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import { describeDeprecated } from '@aws-cdk/cdk-build-tools'; @@ -17,7 +16,7 @@ describeDeprecated('BitBucket source Action', () => { codeBuildCloneOutput: false, }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -58,7 +57,7 @@ describeDeprecated('BitBucket source Action', () => { codeBuildCloneOutput: true, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -88,11 +87,14 @@ describeDeprecated('BitBucket source Action', () => { createBitBucketAndCodeBuildPipeline(stack, { codeBuildCloneOutput: true, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith( - objectLike({ - 'Action': 's3:PutObjectAcl', + 'Statement': Match.arrayWith([ + Match.objectLike({ + 'Action': [ + 's3:PutObjectAcl', + 's3:PutObjectVersionAcl', + ], 'Effect': 'Allow', 'Resource': { 'Fn::Join': [ @@ -109,7 +111,7 @@ describeDeprecated('BitBucket source Action', () => { ], }, }), - ), + ]), }, }); @@ -121,7 +123,7 @@ describeDeprecated('BitBucket source Action', () => { triggerOnPush: false, }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', 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 955e54107789a..c34fde11237ab 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,10 +1,11 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template, Match } from '@aws-cdk/assertions'; 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 * as cpactions from '../../lib'; +import { TestFixture } from './test-fixture'; /* eslint-disable quote-props */ @@ -78,7 +79,7 @@ describe('CloudFormation Pipeline Actions', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'ArtifactStore': { 'Location': { 'Ref': 'MagicPipelineArtifactsBucket212FE7BF', @@ -212,7 +213,7 @@ describe('CloudFormation Pipeline Actions', () => { const roleId = 'PipelineDeployCreateUpdateRole515CB7D4'; // THEN: Action in Pipeline has named IAM capabilities - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -235,7 +236,7 @@ describe('CloudFormation Pipeline Actions', () => { }); // THEN: Role is created with full permissions - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -281,7 +282,7 @@ describe('CloudFormation Pipeline Actions', () => { })); // THEN: Action has output artifacts - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -313,7 +314,7 @@ describe('CloudFormation Pipeline Actions', () => { })); // THEN: Action has output artifacts - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -349,7 +350,7 @@ describe('CloudFormation Pipeline Actions', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -397,7 +398,7 @@ describe('CloudFormation Pipeline Actions', () => { stackName: 'magicStack', })); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', /* don't care about the rest */ @@ -444,7 +445,7 @@ describe('CloudFormation Pipeline Actions', () => { const roleId = 'PipelineDeployCreateUpdateRole515CB7D4'; // THEN: Action in Pipeline has named IAM capabilities - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -488,7 +489,7 @@ describe('CloudFormation Pipeline Actions', () => { const roleId = 'PipelineDeployCreateUpdateRole515CB7D4'; // THEN: Action in Pipeline has named IAM and AUTOEXPAND capabilities - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -531,7 +532,7 @@ describe('CloudFormation Pipeline Actions', () => { const roleId = 'PipelineDeployCreateUpdateRole515CB7D4'; // THEN: Action in Pipeline has no capabilities - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -572,7 +573,7 @@ describe('CloudFormation Pipeline Actions', () => { })); // THEN: Action in Pipeline has named IAM and AUTOEXPAND capabilities - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -636,7 +637,7 @@ describe('CloudFormation Pipeline Actions', () => { ], }); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -663,9 +664,20 @@ describe('CloudFormation Pipeline Actions', () => { }); // the pipeline's BucketPolicy should trust both CFN roles - expect(pipelineStack).toHaveResourceLike('AWS::S3::BucketPolicy', { - 'PolicyDocument': { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::S3::BucketPolicy', { + 'PolicyDocument': Match.objectLike({ 'Statement': [ + { + 'Action': 's3:*', + 'Condition': { + 'Bool': { 'aws:SecureTransport': 'false' }, + }, + 'Effect': 'Deny', + 'Principal': { + 'AWS': '*', + }, + 'Resource': Match.anyValue(), + }, { 'Action': [ 's3:GetObject*', @@ -679,6 +691,7 @@ describe('CloudFormation Pipeline Actions', () => { ':iam::123456789012:role/pipelinestack-support-123fndeploymentrole4668d9b5a30ce3dc4508']], }, }, + 'Resource': Match.anyValue(), }, { 'Action': [ @@ -693,16 +706,17 @@ describe('CloudFormation Pipeline Actions', () => { ':iam::123456789012:role/pipelinestack-support-123loycfnactionrole56af64af3590f311bc50']], }, }, + 'Resource': Match.anyValue(), }, ], - }, + }), }); const otherStack = app.node.findChild('cross-account-support-stack-123456789012') as cdk.Stack; - expect(otherStack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(otherStack).hasResourceProperties('AWS::IAM::Role', { 'RoleName': 'pipelinestack-support-123loycfnactionrole56af64af3590f311bc50', }); - expect(otherStack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(otherStack).hasResourceProperties('AWS::IAM::Role', { 'RoleName': 'pipelinestack-support-123fndeploymentrole4668d9b5a30ce3dc4508', }); @@ -710,32 +724,3 @@ describe('CloudFormation Pipeline Actions', () => { }); }); }); - -/** - * A test stack with a half-prepared pipeline ready to add CloudFormation actions to - */ -class TestFixture extends cdk.Stack { - public readonly pipeline: codepipeline.Pipeline; - public readonly sourceStage: codepipeline.IStage; - public readonly deployStage: codepipeline.IStage; - public readonly repo: codecommit.Repository; - public readonly sourceOutput: codepipeline.Artifact; - - constructor() { - super(); - - this.pipeline = new codepipeline.Pipeline(this, 'Pipeline'); - this.sourceStage = this.pipeline.addStage({ stageName: 'Source' }); - this.deployStage = this.pipeline.addStage({ stageName: 'Deploy' }); - this.repo = new codecommit.Repository(this, 'MyVeryImportantRepo', { - repositoryName: 'my-very-important-repo', - }); - this.sourceOutput = new codepipeline.Artifact('SourceArtifact'); - const source = new cpactions.CodeCommitSourceAction({ - actionName: 'Source', - output: this.sourceOutput, - repository: this.repo, - }); - this.sourceStage.addAction(source); - } -} diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/cloudformation-stackset-pipeline-actions.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/cloudformation-stackset-pipeline-actions.test.ts new file mode 100644 index 0000000000000..80638dc219add --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/cloudformation-stackset-pipeline-actions.test.ts @@ -0,0 +1,419 @@ +import { Match, Template } from '@aws-cdk/assertions'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import * as cpactions from '../../lib'; +import { TestFixture } from './test-fixture'; +/* eslint-disable quote-props */ + +let stack: TestFixture; +let importedAdminRole: iam.IRole; +beforeEach(() => { + stack = new TestFixture({ + env: { + account: '111111111111', + region: 'us-east-1', + }, + }); + importedAdminRole = iam.Role.fromRoleArn(stack, 'ChangeSetRole', 'arn:aws:iam::1234:role/ImportedAdminRole'); +}); + +describe('StackSetAction', () => { + function defaultOpts() { + return { + actionName: 'StackSetUpdate', + description: 'desc', + stackSetName: 'MyStack', + cfnCapabilities: [cdk.CfnCapabilities.NAMED_IAM], + failureTolerancePercentage: 50, + maxAccountConcurrencyPercentage: 25, + template: cpactions.StackSetTemplate.fromArtifactPath(stack.sourceOutput.atPath('template.yaml')), + parameters: cpactions.StackSetParameters.fromArtifactPath(stack.sourceOutput.atPath('parameters.json')), + }; + }; + + describe('self-managed mode', () => { + test('creates admin role if not specified', () => { + stack.deployStage.addAction(new cpactions.CloudFormationDeployStackSetAction({ + ...defaultOpts(), + stackInstances: cpactions.StackInstances.fromArtifactPath( + stack.sourceOutput.atPath('accounts.json'), + ['us-east-1', 'us-west-1', 'ca-central-1'], + ), + deploymentModel: cpactions.StackSetDeploymentModel.selfManaged({ + executionRoleName: 'Exec', + }), + })); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IAM::Policy', { + 'PolicyDocument': { + 'Statement': Match.arrayWith([ + { + 'Action': [ + 'cloudformation:CreateStackInstances', + 'cloudformation:CreateStackSet', + 'cloudformation:DescribeStackSet', + 'cloudformation:DescribeStackSetOperation', + 'cloudformation:ListStackInstances', + 'cloudformation:UpdateStackSet', + ], + 'Effect': 'Allow', + 'Resource': { + 'Fn::Join': ['', [ + 'arn:', + { 'Ref': 'AWS::Partition' }, + ':cloudformation:us-east-1:111111111111:stackset/MyStack:*', + ]], + }, + }, + { + 'Action': 'iam:PassRole', + 'Effect': 'Allow', + 'Resource': { 'Fn::GetAtt': ['PipelineDeployStackSetUpdateStackSetAdministrationRole183434B0', 'Arn'] }, + }, + ]), + }, + 'Roles': [ + { 'Ref': 'PipelineDeployStackSetUpdateCodePipelineActionRole3EDBB32C' }, + ], + }); + + template.hasResourceProperties('AWS::CodePipeline::Pipeline', { + 'Stages': [ + { 'Name': 'Source' /* don't care about the rest */ }, + { + 'Name': 'Deploy', + 'Actions': [ + { + 'Configuration': { + 'StackSetName': 'MyStack', + 'Description': 'desc', + 'TemplatePath': 'SourceArtifact::template.yaml', + 'Parameters': 'SourceArtifact::parameters.json', + 'PermissionModel': 'SELF_MANAGED', + 'AdministrationRoleArn': { 'Fn::GetAtt': ['PipelineDeployStackSetUpdateStackSetAdministrationRole183434B0', 'Arn'] }, + 'ExecutionRoleName': 'Exec', + 'Capabilities': 'CAPABILITY_NAMED_IAM', + 'DeploymentTargets': 'SourceArtifact::accounts.json', + 'FailureTolerancePercentage': 50, + 'MaxConcurrentPercentage': 25, + 'Regions': 'us-east-1,us-west-1,ca-central-1', + }, + 'Name': 'StackSetUpdate', + }, + ], + }, + ], + }); + + template.hasResourceProperties('AWS::IAM::Policy', { + 'PolicyDocument': { + 'Statement': [ + { + 'Effect': 'Allow', + 'Action': 'sts:AssumeRole', + 'Resource': { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::*:role/Exec']] }, + }, + ], + }, + }); + }); + + test('passes admin role if specified', () => { + stack.deployStage.addAction(new cpactions.CloudFormationDeployStackSetAction({ + ...defaultOpts(), + stackInstances: cpactions.StackInstances.fromArtifactPath( + stack.sourceOutput.atPath('accounts.json'), + ['us-east-1', 'us-west-1', 'ca-central-1'], + ), + deploymentModel: cpactions.StackSetDeploymentModel.selfManaged({ + executionRoleName: 'Exec', + administrationRole: importedAdminRole, + }), + })); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IAM::Policy', { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 'cloudformation:CreateStackInstances', + 'cloudformation:CreateStackSet', + 'cloudformation:DescribeStackSet', + 'cloudformation:DescribeStackSetOperation', + 'cloudformation:ListStackInstances', + 'cloudformation:UpdateStackSet', + ], + 'Effect': 'Allow', + 'Resource': { + 'Fn::Join': [ + '', + [ + 'arn:', + { 'Ref': 'AWS::Partition' }, + ':cloudformation:us-east-1:111111111111:stackset/MyStack:*', + ], + ], + }, + }, + { + 'Action': 'iam:PassRole', + 'Effect': 'Allow', + 'Resource': 'arn:aws:iam::1234:role/ImportedAdminRole', + }, + { + 'Action': [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + ], + 'Effect': 'Allow', + 'Resource': [ + { 'Fn::GetAtt': ['PipelineArtifactsBucket22248F97', 'Arn'] }, + { + 'Fn::Join': ['', [ + { 'Fn::GetAtt': ['PipelineArtifactsBucket22248F97', 'Arn'] }, + '/*', + ]], + }, + ], + }, + { + 'Action': [ + 'kms:Decrypt', + 'kms:DescribeKey', + ], + 'Effect': 'Allow', + 'Resource': { 'Fn::GetAtt': ['PipelineArtifactsBucketEncryptionKey01D58D69', 'Arn'] }, + }, + ], + }, + 'Roles': [{ 'Ref': 'PipelineDeployStackSetUpdateCodePipelineActionRole3EDBB32C' }], + }); + }); + }); + + test('creates correct resources in organizations mode', () => { + stack.deployStage.addAction(new cpactions.CloudFormationDeployStackSetAction({ + ...defaultOpts(), + deploymentModel: cpactions.StackSetDeploymentModel.organizations(), + stackInstances: cpactions.StackInstances.fromArtifactPath( + stack.sourceOutput.atPath('accounts.json'), + ['us-east-1', 'us-west-1', 'ca-central-1'], + ), + })); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IAM::Policy', { + 'PolicyDocument': { + 'Statement': Match.arrayWith([ + { + 'Action': [ + 'cloudformation:CreateStackInstances', + 'cloudformation:CreateStackSet', + 'cloudformation:DescribeStackSet', + 'cloudformation:DescribeStackSetOperation', + 'cloudformation:ListStackInstances', + 'cloudformation:UpdateStackSet', + ], + 'Effect': 'Allow', + 'Resource': { + 'Fn::Join': [ + '', + [ + 'arn:', + { 'Ref': 'AWS::Partition' }, + ':cloudformation:us-east-1:111111111111:stackset/MyStack:*', + ], + ], + }, + }, + ]), + }, + 'Roles': [ + { 'Ref': 'PipelineDeployStackSetUpdateCodePipelineActionRole3EDBB32C' }, + ], + }); + }); + + test('creates correct pipeline resource with target list', () => { + stack.deployStage.addAction(new cpactions.CloudFormationDeployStackSetAction({ + ...defaultOpts(), + stackInstances: cpactions.StackInstances.inAccounts( + ['11111111111', '22222222222'], + ['us-east-1', 'us-west-1', 'ca-central-1'], + ), + deploymentModel: cpactions.StackSetDeploymentModel.selfManaged({ + administrationRole: importedAdminRole, + executionRoleName: 'Exec', + }), + })); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::CodePipeline::Pipeline', { + 'Stages': [ + { 'Name': 'Source' /* don't care about the rest */ }, + { + 'Name': 'Deploy', + 'Actions': [ + { + 'Configuration': { + 'StackSetName': 'MyStack', + 'Description': 'desc', + 'TemplatePath': 'SourceArtifact::template.yaml', + 'Parameters': 'SourceArtifact::parameters.json', + 'Capabilities': 'CAPABILITY_NAMED_IAM', + 'DeploymentTargets': '11111111111,22222222222', + 'FailureTolerancePercentage': 50, + 'MaxConcurrentPercentage': 25, + 'Regions': 'us-east-1,us-west-1,ca-central-1', + }, + 'InputArtifacts': [{ 'Name': 'SourceArtifact' }], + 'Name': 'StackSetUpdate', + }, + ], + }, + ], + }); + }); + + test('creates correct pipeline resource with parameter list', () => { + stack.deployStage.addAction(new cpactions.CloudFormationDeployStackSetAction({ + ...defaultOpts(), + parameters: cpactions.StackSetParameters.fromLiteral({ + key0: 'val0', + key1: 'val1', + }, ['key2', 'key3']), + stackInstances: cpactions.StackInstances.fromArtifactPath( + stack.sourceOutput.atPath('accounts.json'), + ['us-east-1', 'us-west-1', 'ca-central-1'], + ), + deploymentModel: cpactions.StackSetDeploymentModel.selfManaged({ + administrationRole: importedAdminRole, + executionRoleName: 'Exec', + }), + })); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::CodePipeline::Pipeline', { + 'Stages': [ + { 'Name': 'Source' /* don't care about the rest */ }, + { + 'Name': 'Deploy', + 'Actions': [ + { + 'Configuration': { + 'StackSetName': 'MyStack', + 'Description': 'desc', + 'TemplatePath': 'SourceArtifact::template.yaml', + 'Parameters': 'ParameterKey=key0,ParameterValue=val0 ParameterKey=key1,ParameterValue=val1 ParameterKey=key2,UsePreviousValue=true ParameterKey=key3,UsePreviousValue=true', + 'Capabilities': 'CAPABILITY_NAMED_IAM', + 'DeploymentTargets': 'SourceArtifact::accounts.json', + 'FailureTolerancePercentage': 50, + 'MaxConcurrentPercentage': 25, + 'Regions': 'us-east-1,us-west-1,ca-central-1', + }, + 'InputArtifacts': [{ 'Name': 'SourceArtifact' }], + 'Name': 'StackSetUpdate', + }, + ], + }, + ], + }); + }); + + test('correctly passes region', () => { + stack.deployStage.addAction(new cpactions.CloudFormationDeployStackSetAction({ + ...defaultOpts(), + stackSetRegion: 'us-banana-2', + })); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::CodePipeline::Pipeline', { + 'Stages': [ + { 'Name': 'Source' /* don't care about the rest */ }, + { + 'Name': 'Deploy', + 'Actions': [ + { + 'Region': 'us-banana-2', + }, + ], + }, + ], + }); + }); +}); + +describe('StackInstancesAction', () => { + function defaultOpts() { + return { + actionName: 'StackInstances', + stackSetName: 'MyStack', + failureTolerancePercentage: 50, + maxAccountConcurrencyPercentage: 25, + }; + }; + + test('simple', () => { + stack.deployStage.addAction(new cpactions.CloudFormationDeployStackInstancesAction({ + ...defaultOpts(), + stackInstances: cpactions.StackInstances.inAccounts( + ['1234', '5678'], + ['us-east-1', 'us-west-1'], + ), + })); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::CodePipeline::Pipeline', { + 'Stages': [ + { 'Name': 'Source' /* don't care about the rest */ }, + { + 'Name': 'Deploy', + 'Actions': [ + { + 'ActionTypeId': { + 'Category': 'Deploy', + 'Owner': 'AWS', + 'Provider': 'CloudFormationStackInstances', + 'Version': '1', + }, + 'Configuration': { + 'StackSetName': 'MyStack', + 'FailureTolerancePercentage': 50, + 'MaxConcurrentPercentage': 25, + 'DeploymentTargets': '1234,5678', + 'Regions': 'us-east-1,us-west-1', + }, + 'Name': 'StackInstances', + }, + ], + }, + ], + }); + }); + + test('correctly passes region', () => { + stack.deployStage.addAction(new cpactions.CloudFormationDeployStackInstancesAction({ + ...defaultOpts(), + stackSetRegion: 'us-banana-2', + stackInstances: cpactions.StackInstances.inAccounts(['1'], ['us-east-1']), + })); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::CodePipeline::Pipeline', { + 'Stages': [ + { 'Name': 'Source' /* don't care about the rest */ }, + { + 'Name': 'Deploy', + 'Actions': [ + { + 'Region': 'us-banana-2', + }, + ], + }, + ], + }); + }); +}); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/integ.stacksets.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/integ.stacksets.expected.json new file mode 100644 index 0000000000000..6df306f7f30a0 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/integ.stacksets.expected.json @@ -0,0 +1,658 @@ +{ + "Resources": { + "ArtifactBucket7410C9EF": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "PipelineRoleD68726F7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleDefaultPolicyC7A05455": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineSourceCodePipelineActionRoleC6F9E7F5", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineCfnStackSetCodePipelineActionRole9EA256DB", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineCfnInstancesCodePipelineActionRole289FD062", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineRoleDefaultPolicyC7A05455", + "Roles": [ + { + "Ref": "PipelineRoleD68726F7" + } + ] + } + }, + "PipelineC660917D": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "PipelineRoleD68726F7", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "AWS", + "Provider": "S3", + "Version": "1" + }, + "Configuration": { + "S3Bucket": { + "Ref": "AssetParameters5bcf205623ea5b34a1944fea4c9982e835555e710235ae6f60172097737302e2S3Bucket3C8B9651" + }, + "S3ObjectKey": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5bcf205623ea5b34a1944fea4c9982e835555e710235ae6f60172097737302e2S3VersionKeyD144071F" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5bcf205623ea5b34a1944fea4c9982e835555e710235ae6f60172097737302e2S3VersionKeyD144071F" + } + ] + } + ] + } + ] + ] + } + }, + "Name": "Source", + "OutputArtifacts": [ + { + "Name": "SourceArtifact" + } + ], + "RoleArn": { + "Fn::GetAtt": [ + "PipelineSourceCodePipelineActionRoleC6F9E7F5", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormationStackSet", + "Version": "1" + }, + "Configuration": { + "StackSetName": "TestStackSet", + "TemplatePath": "SourceArtifact::template.yaml", + "DeploymentTargets": "1111,2222", + "Regions": "us-east-1,eu-west-1", + "PermissionModel": "SELF_MANAGED", + "AdministrationRoleArn": { + "Fn::GetAtt": [ + "PipelineCfnStackSetStackSetAdministrationRoleAE2E9C50", + "Arn" + ] + } + }, + "InputArtifacts": [ + { + "Name": "SourceArtifact" + } + ], + "Name": "StackSet", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineCfnStackSetCodePipelineActionRole9EA256DB", + "Arn" + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormationStackInstances", + "Version": "1" + }, + "Configuration": { + "StackSetName": "TestStackSet", + "DeploymentTargets": "1111,2222", + "Regions": "us-east-1,eu-west-1" + }, + "Name": "Instances", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineCfnInstancesCodePipelineActionRole289FD062", + "Arn" + ] + }, + "RunOrder": 2 + } + ], + "Name": "Cfn" + } + ], + "ArtifactStore": { + "Location": { + "Ref": "ArtifactBucket7410C9EF" + }, + "Type": "S3" + } + }, + "DependsOn": [ + "PipelineRoleDefaultPolicyC7A05455", + "PipelineRoleD68726F7" + ] + }, + "PipelineSourceCodePipelineActionRoleC6F9E7F5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineSourceCodePipelineActionRoleDefaultPolicy2D565925": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParameters5bcf205623ea5b34a1944fea4c9982e835555e710235ae6f60172097737302e2S3Bucket3C8B9651" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParameters5bcf205623ea5b34a1944fea4c9982e835555e710235ae6f60172097737302e2S3Bucket3C8B9651" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5bcf205623ea5b34a1944fea4c9982e835555e710235ae6f60172097737302e2S3VersionKeyD144071F" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5bcf205623ea5b34a1944fea4c9982e835555e710235ae6f60172097737302e2S3VersionKeyD144071F" + } + ] + } + ] + } + ] + ] + } + ] + }, + { + "Action": [ + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineSourceCodePipelineActionRoleDefaultPolicy2D565925", + "Roles": [ + { + "Ref": "PipelineSourceCodePipelineActionRoleC6F9E7F5" + } + ] + } + }, + "PipelineCfnStackSetCodePipelineActionRole9EA256DB": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineCfnStackSetCodePipelineActionRoleDefaultPolicyE5B66E2C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "cloudformation:CreateStackInstances", + "cloudformation:CreateStackSet", + "cloudformation:DescribeStackSet", + "cloudformation:DescribeStackSetOperation", + "cloudformation:ListStackInstances", + "cloudformation:UpdateStackSet" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:test-region:12345678:stackset/TestStackSet:*" + ] + ] + } + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineCfnStackSetStackSetAdministrationRoleAE2E9C50", + "Arn" + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineCfnStackSetCodePipelineActionRoleDefaultPolicyE5B66E2C", + "Roles": [ + { + "Ref": "PipelineCfnStackSetCodePipelineActionRole9EA256DB" + } + ] + } + }, + "PipelineCfnStackSetStackSetAdministrationRoleAE2E9C50": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { + "StringLike": { + "aws:SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:*:", + { + "Ref": "AWS::AccountId" + }, + ":stackset/*" + ] + ] + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "cloudformation.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineCfnStackSetStackSetAdministrationRoleDefaultPolicy55145C4E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::*:role/AWSCloudFormationStackSetExecutionRole" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineCfnStackSetStackSetAdministrationRoleDefaultPolicy55145C4E", + "Roles": [ + { + "Ref": "PipelineCfnStackSetStackSetAdministrationRoleAE2E9C50" + } + ] + } + }, + "PipelineCfnInstancesCodePipelineActionRole289FD062": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineCfnInstancesCodePipelineActionRoleDefaultPolicy38A9673E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "cloudformation:CreateStackInstances", + "cloudformation:CreateStackSet", + "cloudformation:DescribeStackSet", + "cloudformation:DescribeStackSetOperation", + "cloudformation:ListStackInstances", + "cloudformation:UpdateStackSet" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:test-region:12345678:stackset/TestStackSet:*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineCfnInstancesCodePipelineActionRoleDefaultPolicy38A9673E", + "Roles": [ + { + "Ref": "PipelineCfnInstancesCodePipelineActionRole289FD062" + } + ] + } + } + }, + "Parameters": { + "AssetParameters5bcf205623ea5b34a1944fea4c9982e835555e710235ae6f60172097737302e2S3Bucket3C8B9651": { + "Type": "String", + "Description": "S3 bucket for asset \"5bcf205623ea5b34a1944fea4c9982e835555e710235ae6f60172097737302e2\"" + }, + "AssetParameters5bcf205623ea5b34a1944fea4c9982e835555e710235ae6f60172097737302e2S3VersionKeyD144071F": { + "Type": "String", + "Description": "S3 key for asset version \"5bcf205623ea5b34a1944fea4c9982e835555e710235ae6f60172097737302e2\"" + }, + "AssetParameters5bcf205623ea5b34a1944fea4c9982e835555e710235ae6f60172097737302e2ArtifactHashA83BA1E9": { + "Type": "String", + "Description": "Artifact hash for asset \"5bcf205623ea5b34a1944fea4c9982e835555e710235ae6f60172097737302e2\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/integ.stacksets.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/integ.stacksets.ts new file mode 100644 index 0000000000000..bc41e86474584 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/integ.stacksets.ts @@ -0,0 +1,83 @@ +/** + * This integration test needs 2 accounts properly configured beforehand to properly test, + * and so is tested by hand. + * + * To test: + * + * ``` + * env AWS_REGION=eu-west-1 STACKSET_ACCOUNTS=11111111,22222222 cdk deploy -a test/cloudformation/integ.stacksets.js + * ``` + * + * Then make the pipeline in your account run. + * + * To update the snapshot: + * + * ``` + * yarn integ --dry-run cloudformation/integ.stacksets.js + * ``` + */ +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as s3 from '@aws-cdk/aws-s3'; +import { Asset } from '@aws-cdk/aws-s3-assets'; +import { App, RemovalPolicy, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as cpactions from '../../lib'; + +export class StackSetPipelineStack extends Stack { + constructor(scope: Construct, id: string, props: StackProps) { + super(scope, id, props); + + const pipeline = new codepipeline.Pipeline(this, 'Pipeline', { + artifactBucket: new s3.Bucket(this, 'ArtifactBucket', { + removalPolicy: RemovalPolicy.DESTROY, + }), + }); + + const asset = new Asset(this, 'Asset', { + path: `${__dirname}/test-artifact`, + }); + + const sourceOutput = new codepipeline.Artifact('SourceArtifact'); + + pipeline.addStage({ + stageName: 'Source', + actions: [ + new cpactions.S3SourceAction({ + actionName: 'Source', + output: sourceOutput, + bucket: asset.bucket, + bucketKey: asset.s3ObjectKey, + }), + ], + }); + + const accounts = process.env.STACKSET_ACCOUNTS?.split(',') ?? ['1111', '2222']; + + pipeline.addStage({ + stageName: 'Cfn', + actions: [ + new cpactions.CloudFormationDeployStackSetAction({ + actionName: 'StackSet', + stackSetName: 'TestStackSet', + template: cpactions.StackSetTemplate.fromArtifactPath(sourceOutput.atPath('template.yaml')), + stackInstances: cpactions.StackInstances.inAccounts(accounts, ['us-east-1', 'eu-west-1']), + runOrder: 1, + }), + new cpactions.CloudFormationDeployStackInstancesAction({ + actionName: 'Instances', + stackSetName: 'TestStackSet', + stackInstances: cpactions.StackInstances.inAccounts(accounts, ['us-east-1', 'eu-west-1']), + runOrder: 2, + }), + ], + }); + } +} + +const app = new App(); +new StackSetPipelineStack(app, 'StackSetPipelineStack', { + env: { + region: process.env.CDK_DEFAULT_REGION, + account: process.env.CDK_DEFAULT_ACCOUNT, + }, +}); 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 3d9594a9ddfbd..9b1dc140f6068 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,4 +1,3 @@ -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'; diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test-artifact/template.yaml b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test-artifact/template.yaml new file mode 100644 index 0000000000000..c2c61591f6778 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test-artifact/template.yaml @@ -0,0 +1,6 @@ +Resources: + Filler: + Type: AWS::CloudFormation::WaitConditionHandle +Outputs: + Great: + Value: It works! \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test-fixture.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test-fixture.ts new file mode 100644 index 0000000000000..f981d829c2a2d --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test-fixture.ts @@ -0,0 +1,33 @@ +import * as codecommit from '@aws-cdk/aws-codecommit'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as cdk from '@aws-cdk/core'; +import * as cpactions from '../../lib'; + +/** + * A test stack with a half-prepared pipeline ready to add CloudFormation actions to + */ +export class TestFixture extends cdk.Stack { + public readonly pipeline: codepipeline.Pipeline; + public readonly sourceStage: codepipeline.IStage; + public readonly deployStage: codepipeline.IStage; + public readonly repo: codecommit.Repository; + public readonly sourceOutput: codepipeline.Artifact; + + constructor(props: cdk.StackProps = {}) { + super(undefined, undefined, props); + + this.pipeline = new codepipeline.Pipeline(this, 'Pipeline'); + this.sourceStage = this.pipeline.addStage({ stageName: 'Source' }); + this.deployStage = this.pipeline.addStage({ stageName: 'Deploy' }); + this.repo = new codecommit.Repository(this, 'MyVeryImportantRepo', { + repositoryName: 'my-very-important-repo', + }); + this.sourceOutput = new codepipeline.Artifact('SourceArtifact'); + const source = new cpactions.CodeCommitSourceAction({ + actionName: 'Source', + output: this.sourceOutput, + repository: this.repo, + }); + this.sourceStage.addAction(source); + } +} 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 c8ddc211a813d..fdd470bf19640 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,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; @@ -101,7 +101,7 @@ describe('CodeBuild Action', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -176,7 +176,7 @@ describe('CodeBuild Action', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -235,7 +235,7 @@ describe('CodeBuild Action', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -291,7 +291,7 @@ describe('CodeBuild Action', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', 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 648c113ce2155..2ca60b9d5c49e 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,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT, arrayWith, objectLike } from '@aws-cdk/assert-internal'; +import { Template, Match } from '@aws-cdk/assertions'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; @@ -18,7 +17,7 @@ describe('CodeCommit Source Action', () => { minimalPipeline(stack, undefined); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -33,7 +32,7 @@ describe('CodeCommit Source Action', () => { ], }); - expect(stack).toCountResources('AWS::Events::Rule', 1); + Template.fromStack(stack).resourceCountIs('AWS::Events::Rule', 1); }); @@ -67,7 +66,7 @@ describe('CodeCommit Source Action', () => { }); // THEN - creates a Rule in the source stack targeting the pipeline stack's event bus using a generated role - expect(sourceStack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(sourceStack).hasResourceProperties('AWS::Events::Rule', { EventPattern: { source: ['aws.codecommit'], resources: [ @@ -75,7 +74,7 @@ describe('CodeCommit Source Action', () => { ], }, Targets: [{ - RoleARN: ABSENT, + RoleARN: Match.absent(), Arn: { 'Fn::Join': ['', [ 'arn:', @@ -87,7 +86,7 @@ describe('CodeCommit Source Action', () => { }); // THEN - creates a Rule in the pipeline stack using the role to start the pipeline - expect(targetStack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(targetStack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.codecommit', @@ -128,7 +127,7 @@ describe('CodeCommit Source Action', () => { minimalPipeline(stack, cpactions.CodeCommitTrigger.EVENTS); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -143,7 +142,7 @@ describe('CodeCommit Source Action', () => { ], }); - expect(stack).toCountResources('AWS::Events::Rule', 1); + Template.fromStack(stack).resourceCountIs('AWS::Events::Rule', 1); }); @@ -153,7 +152,7 @@ describe('CodeCommit Source Action', () => { minimalPipeline(stack, cpactions.CodeCommitTrigger.POLL); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -168,7 +167,7 @@ describe('CodeCommit Source Action', () => { ], }); - expect(stack).not.toHaveResourceLike('AWS::Events::Rule'); + Template.fromStack(stack).resourceCountIs('AWS::Events::Rule', 0); }); @@ -178,7 +177,7 @@ describe('CodeCommit Source Action', () => { minimalPipeline(stack, cpactions.CodeCommitTrigger.NONE); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -193,7 +192,7 @@ describe('CodeCommit Source Action', () => { ], }); - expect(stack).not.toHaveResourceLike('AWS::Events::Rule'); + Template.fromStack(stack).resourceCountIs('AWS::Events::Rule', 0); }); @@ -291,7 +290,7 @@ describe('CodeCommit Source Action', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -345,7 +344,7 @@ describe('CodeCommit Source Action', () => { ], }); - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { EventPattern: { detail: { referenceName: ['my-branch'], @@ -389,7 +388,7 @@ describe('CodeCommit Source Action', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -410,17 +409,17 @@ describe('CodeCommit Source Action', () => { ], }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith( - objectLike({ + 'Statement': Match.arrayWith([ + Match.objectLike({ 'Action': [ 'logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents', ], }), - objectLike({ + Match.objectLike({ 'Action': 'codecommit:GitPull', 'Effect': 'Allow', 'Resource': { @@ -430,14 +429,14 @@ describe('CodeCommit Source Action', () => { ], }, }), - ), + ]), }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith( - objectLike({ + 'Statement': Match.arrayWith([ + Match.objectLike({ 'Action': [ 'codecommit:GetBranch', 'codecommit:GetCommit', @@ -454,7 +453,7 @@ describe('CodeCommit Source Action', () => { ], }, }), - ), + ]), }, }); @@ -504,7 +503,7 @@ describe('CodeCommit Source Action', () => { actions: [buildAction], }); - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: stack.resolve(pipeline.pipelineArn), @@ -552,10 +551,13 @@ describe('CodeCommit Source Action', () => { ], }); - expect(repoStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(repoStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith({ - 'Action': 's3:PutObjectAcl', + Statement: Match.arrayWith([{ + 'Action': [ + 's3:PutObjectAcl', + 's3:PutObjectVersionAcl', + ], 'Effect': 'Allow', 'Resource': { 'Fn::Join': ['', [ @@ -564,7 +566,7 @@ describe('CodeCommit Source Action', () => { ':s3:::pipeline-bucket/*', ]], }, - }), + }]), }, }); 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 18a0c714f4d5b..c59465746c25b 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,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as codedeploy from '@aws-cdk/aws-codedeploy'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as cdk from '@aws-cdk/core'; @@ -111,7 +111,7 @@ describe('CodeDeploy ECS Deploy Action', () => { appSpecTemplateInput: new codepipeline.Artifact('AppSpecArtifact'), }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: [ {}, { @@ -162,7 +162,7 @@ describe('CodeDeploy ECS Deploy Action', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: [ {}, { 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 312251bd8457a..eb85f0fc2d1e3 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,8 +1,7 @@ -import '@aws-cdk/assert-internal/jest'; -import { arrayWith, objectLike, SynthUtils } from '@aws-cdk/assert-internal'; +import { Template, Match } from '@aws-cdk/assertions'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; -import { Stack } from '@aws-cdk/core'; +import { App, Stack } from '@aws-cdk/core'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ @@ -16,7 +15,7 @@ describe('CodeStar Connections source Action', () => { codeBuildCloneOutput: false, }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -57,7 +56,7 @@ describe('CodeStar Connections source Action', () => { codeBuildCloneOutput: true, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -90,11 +89,14 @@ describe('CodeStar Connections source Action', () => { codeBuildCloneOutput: true, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith( - objectLike({ - 'Action': 's3:PutObjectAcl', + 'Statement': Match.arrayWith([ + Match.objectLike({ + 'Action': [ + 's3:PutObjectAcl', + 's3:PutObjectVersionAcl', + ], 'Effect': 'Allow', 'Resource': { 'Fn::Join': ['', [ @@ -103,7 +105,7 @@ describe('CodeStar Connections source Action', () => { ]], }, }), - ), + ]), }, }); @@ -117,7 +119,7 @@ describe('CodeStar Connections source Action', () => { triggerOnPush: false, }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -154,7 +156,7 @@ describe('CodeStar Connections source Action', () => { const stack = new Stack(); createBitBucketAndCodeBuildPipeline(stack); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -180,7 +182,7 @@ describe('CodeStar Connections source Action', () => { variablesNamespace: 'kornicameister', }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -207,7 +209,8 @@ describe('CodeStar Connections source Action', () => { }); test('fail if variable from unused action is referenced', () => { - const stack = new Stack(); + const app = new App(); + const stack = new Stack(app); const pipeline = createBitBucketAndCodeBuildPipeline(stack); const unusedSourceOutput = new codepipeline.Artifact(); @@ -229,12 +232,13 @@ describe('CodeStar Connections source Action', () => { pipeline.stage('Build').addAction(unusedBuildAction); expect(() => { - SynthUtils.synthesize(stack); + App.of(stack)!.synth(); }).toThrow(/Cannot reference variables of action 'UnusedBitBucket', as that action was never added to a pipeline/); }); test('fail if variable from unused action with custom namespace is referenced', () => { - const stack = new Stack(); + const app = new App(); + const stack = new Stack(app); const pipeline = createBitBucketAndCodeBuildPipeline(stack, { variablesNamespace: 'kornicameister', }); @@ -258,7 +262,7 @@ describe('CodeStar Connections source Action', () => { pipeline.stage('Build').addAction(unusedBuildAction); expect(() => { - SynthUtils.synthesize(stack); + App.of(stack)!.synth(); }).toThrow(/Cannot reference variables of action 'UnusedBitBucket', as that action was never added to a pipeline/); }); }); 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 99160cdd4193a..99752272df90f 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,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT } from '@aws-cdk/assert-internal'; +import { Template, Match } from '@aws-cdk/assertions'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as ecr from '@aws-cdk/aws-ecr'; @@ -41,27 +40,27 @@ describe('ecr source action', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - 'Stages': [ - { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + 'Stages': Match.arrayWith([ + Match.objectLike({ 'Name': 'Source', - }, - { + }), + Match.objectLike({ 'Name': 'Build', - 'Actions': [ - { + 'Actions': Match.arrayWith([ + Match.objectLike({ 'Name': 'Build', 'Configuration': { 'EnvironmentVariables': '[{"name":"ImageDigest","type":"PLAINTEXT","value":"#{Source_Source_NS.ImageDigest}"}]', }, - }, - ], - }, - ], + }), + ]), + }), + ]), }); - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'detail': { 'requestParameters': { @@ -105,33 +104,33 @@ describe('ecr source action', () => { ], }); - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.ecr', ], 'detail': { 'requestParameters': { - 'imageTag': ABSENT, + 'imageTag': Match.absent(), }, }, }, }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - 'Stages': [ - { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + 'Stages': Match.arrayWith([ + Match.objectLike({ 'Name': 'Source', - 'Actions': [ - { + 'Actions': Match.arrayWith([ + Match.objectLike({ 'Name': 'Source', 'Configuration': { - 'ImageTag': ABSENT, + 'ImageTag': Match.absent(), }, - }, - ], - }, - ], + }), + ]), + }), + ]), }); }); }); 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 63927d5832ec8..0841d47946a23 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,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; @@ -172,7 +172,7 @@ describe('ecs deploy action', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: [ {}, { @@ -196,6 +196,84 @@ describe('ecs deploy action', () => { }); + + test('can be created by existing service with cluster ARN format', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'PipelineStack', { + env: { + region: 'pipeline-region', account: 'pipeline-account', + }, + }); + const clusterName = 'cluster-name'; + const serviceName = 'service-name'; + const region = 'service-region'; + const account = 'service-account'; + const serviceArn = `arn:aws:ecs:${region}:${account}:service/${clusterName}/${serviceName}`; + const service = ecs.BaseService.fromServiceArnWithCluster(stack, 'FargateService', serviceArn); + + const artifact = new codepipeline.Artifact('Artifact'); + const bucket = new s3.Bucket(stack, 'PipelineBucket', { + versioned: true, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + const source = new cpactions.S3SourceAction({ + actionName: 'Source', + output: artifact, + bucket, + bucketKey: 'key', + }); + const action = new cpactions.EcsDeployAction({ + actionName: 'ECS', + service: service, + input: artifact, + }); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [source], + }, + { + stageName: 'Deploy', + actions: [action], + }, + ], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: [ + {}, + { + Actions: [ + { + Name: 'ECS', + ActionTypeId: { + Category: 'Deploy', + Provider: 'ECS', + }, + Configuration: { + ClusterName: clusterName, + ServiceName: serviceName, + }, + Region: region, + RoleArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + `:iam::${account}:role/pipelinestack-support-serloyecsactionrole49867f847238c85af7c0`, + ], + ], + }, + }, + ], + }, + ], + }); + }); }); }); 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 6a1342888042e..b7e970ba59a02 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,8 +1,7 @@ -import '@aws-cdk/assert-internal/jest'; -import { SynthUtils } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; -import { SecretValue, Stack } from '@aws-cdk/core'; +import { App, SecretValue, Stack } from '@aws-cdk/core'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ @@ -42,7 +41,7 @@ describe('Github source action', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -96,7 +95,7 @@ describe('Github source action', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -116,7 +115,8 @@ describe('Github source action', () => { }); 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 app = new App(); + const stack = new Stack(app); const unusedSourceAction = new cpactions.GitHubSourceAction({ actionName: 'Source2', @@ -155,14 +155,15 @@ describe('Github source action', () => { }); expect(() => { - SynthUtils.synthesize(stack); + App.of(stack)!.synth(); }).toThrow(/Cannot reference variables of action 'Source2', as that action was never added to a pipeline/); }); 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 app = new App(); + const stack = new Stack(app); const unusedSourceAction = new cpactions.GitHubSourceAction({ actionName: 'Source2', @@ -202,7 +203,7 @@ describe('Github source action', () => { }); expect(() => { - SynthUtils.synthesize(stack); + App.of(stack)!.synth(); }).toThrow(/Cannot reference variables of action 'Source2', as that action was never added to a pipeline/); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.cfn-template-from-repo.lit.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.cfn-template-from-repo.lit.expected.json index 6d5734f005c70..87d6594c254c4 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.cfn-template-from-repo.lit.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.cfn-template-from-repo.lit.expected.json @@ -41,6 +41,20 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "PipelineArtifactsBucketEncryptionKeyAlias5C510EEE": { + "Type": "AWS::KMS::Alias", + "Properties": { + "AliasName": "alias/codepipeline-awscdkcodepipelinecloudformationpipeline7dbde619", + "TargetKeyId": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "PipelineArtifactsBucket22248F97": { "Type": "AWS::S3::Bucket", "Properties": { @@ -69,19 +83,52 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, - "PipelineArtifactsBucketEncryptionKeyAlias5C510EEE": { - "Type": "AWS::KMS::Alias", + "PipelineArtifactsBucketPolicyD4F9712A": { + "Type": "AWS::S3::BucketPolicy", "Properties": { - "AliasName": "alias/codepipeline-awscdkcodepipelinecloudformationpipeline7dbde619", - "TargetKeyId": { - "Fn::GetAtt": [ - "PipelineArtifactsBucketEncryptionKey01D58D69", - "Arn" - ] + "Bucket": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" } - }, - "UpdateReplacePolicy": "Delete", - "DeletionPolicy": "Delete" + } }, "PipelineRoleD68726F7": { "Type": "AWS::IAM::Role", @@ -112,6 +159,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -393,6 +444,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.expected.json index 29afc8317c758..823b5af6c908b 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.expected.json @@ -35,6 +35,20 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "PipelineArtifactsBucketEncryptionKeyAlias5C510EEE": { + "Type": "AWS::KMS::Alias", + "Properties": { + "AliasName": "alias/codepipeline-pipelinestackpipeline9db740af", + "TargetKeyId": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "PipelineArtifactsBucket22248F97": { "Type": "AWS::S3::Bucket", "Properties": { @@ -63,19 +77,52 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, - "PipelineArtifactsBucketEncryptionKeyAlias5C510EEE": { - "Type": "AWS::KMS::Alias", + "PipelineArtifactsBucketPolicyD4F9712A": { + "Type": "AWS::S3::BucketPolicy", "Properties": { - "AliasName": "alias/codepipeline-pipelinestackpipeline9db740af", - "TargetKeyId": { - "Fn::GetAtt": [ - "PipelineArtifactsBucketEncryptionKey01D58D69", - "Arn" - ] + "Bucket": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" } - }, - "UpdateReplacePolicy": "Delete", - "DeletionPolicy": "Delete" + } }, "PipelineRoleD68726F7": { "Type": "AWS::IAM::Role", @@ -106,6 +153,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -461,6 +512,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -574,6 +629,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -1303,6 +1362,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -1525,6 +1588,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.expected.json index b925c611a0591..0990f457aae8f 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.expected.json @@ -77,6 +77,53 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, + "PipelineArtifactsBucketPolicyD4F9712A": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, "PipelineRoleD68726F7": { "Type": "AWS::IAM::Role", "Properties": { @@ -106,6 +153,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -341,6 +392,10 @@ "Action": [ "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -622,6 +677,40 @@ }, "PolicyDocument": { "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "CloudTrailS310CD22F2", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CloudTrailS310CD22F2", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, { "Action": "s3:GetBucketAcl", "Effect": "Allow", @@ -788,4 +877,4 @@ ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-alexa-deploy.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-alexa-deploy.expected.json index db1378dc62f7c..c255275baa82b 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-alexa-deploy.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-alexa-deploy.expected.json @@ -87,6 +87,53 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, + "PipelineArtifactsBucketPolicyD4F9712A": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, "PipelineRoleD68726F7": { "Type": "AWS::IAM::Role", "Properties": { @@ -116,6 +163,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -340,6 +391,10 @@ "Action": [ "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-cross-region.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-cross-region.expected.json index 85ddb7d7dc4a9..167b278084683 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-cross-region.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-cross-region.expected.json @@ -39,6 +39,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -265,6 +269,10 @@ "Action": [ "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-with-action-role.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-with-action-role.expected.json index 81c9c5fc2a998..b3d024a33f2a8 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-with-action-role.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-with-action-role.expected.json @@ -164,6 +164,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -384,6 +388,10 @@ "Action": [ "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn.expected.json index 47d57c1301cb4..53189d1369dda 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn.expected.json @@ -77,6 +77,53 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, + "PipelineArtifactsBucketPolicyD4F9712A": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, "PipelineRoleD68726F7": { "Type": "AWS::IAM::Role", "Properties": { @@ -106,6 +153,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -393,6 +444,10 @@ "Action": [ "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -516,6 +571,10 @@ "Action": [ "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-batch.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-batch.expected.json index 3af1036c27a2b..854b01ae12ac6 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-batch.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-batch.expected.json @@ -110,6 +110,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json index cd9a20670da04..c1624ab7e2f7b 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json @@ -110,6 +110,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -196,6 +200,10 @@ "Action": [ "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -557,6 +565,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit-build.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit-build.expected.json index 410001cabd59b..cb437e83eb651 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit-build.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit-build.expected.json @@ -92,6 +92,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -295,6 +299,20 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "PipelineArtifactsBucketEncryptionKeyAlias5C510EEE": { + "Type": "AWS::KMS::Alias", + "Properties": { + "AliasName": "alias/codepipeline-awscdkcodepipelinecodecommitcodebuildpipeline9540e1f5", + "TargetKeyId": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "PipelineArtifactsBucket22248F97": { "Type": "AWS::S3::Bucket", "Properties": { @@ -323,19 +341,52 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, - "PipelineArtifactsBucketEncryptionKeyAlias5C510EEE": { - "Type": "AWS::KMS::Alias", + "PipelineArtifactsBucketPolicyD4F9712A": { + "Type": "AWS::S3::BucketPolicy", "Properties": { - "AliasName": "alias/codepipeline-awscdkcodepipelinecodecommitcodebuildpipeline9540e1f5", - "TargetKeyId": { - "Fn::GetAtt": [ - "PipelineArtifactsBucketEncryptionKey01D58D69", - "Arn" - ] + "Bucket": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" } - }, - "UpdateReplacePolicy": "Delete", - "DeletionPolicy": "Delete" + } }, "PipelineRoleD68726F7": { "Type": "AWS::IAM::Role", @@ -366,6 +417,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -624,6 +679,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit.expected.json index 5bd2974d1ceb8..23bdec497e551 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit.expected.json @@ -106,6 +106,20 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "PipelineArtifactsBucketEncryptionKeyAlias5C510EEE": { + "Type": "AWS::KMS::Alias", + "Properties": { + "AliasName": "alias/codepipeline-awscdkcodepipelinecodecommitpipelinef780ca18", + "TargetKeyId": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "PipelineArtifactsBucket22248F97": { "Type": "AWS::S3::Bucket", "Properties": { @@ -134,19 +148,52 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, - "PipelineArtifactsBucketEncryptionKeyAlias5C510EEE": { - "Type": "AWS::KMS::Alias", + "PipelineArtifactsBucketPolicyD4F9712A": { + "Type": "AWS::S3::BucketPolicy", "Properties": { - "AliasName": "alias/codepipeline-awscdkcodepipelinecodecommitpipelinef780ca18", - "TargetKeyId": { - "Fn::GetAtt": [ - "PipelineArtifactsBucketEncryptionKey01D58D69", - "Arn" - ] + "Bucket": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" } - }, - "UpdateReplacePolicy": "Delete", - "DeletionPolicy": "Delete" + } }, "PipelineRoleD68726F7": { "Type": "AWS::IAM::Role", @@ -177,6 +224,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -383,6 +434,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-deploy-ecs.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-deploy-ecs.expected.json index ad229d36e2207..1b20a71fd7def 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-deploy-ecs.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-deploy-ecs.expected.json @@ -39,6 +39,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -258,6 +262,10 @@ "Action": [ "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-deploy.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-deploy.expected.json index d2d0bea52821f..eb87655dd0e60 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-deploy.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-deploy.expected.json @@ -124,6 +124,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -341,6 +345,10 @@ "Action": [ "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecr-source.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecr-source.expected.json index 412d1219dfa36..f89ab7b1d7fcd 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecr-source.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecr-source.expected.json @@ -34,6 +34,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -217,6 +221,10 @@ "Action": [ "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json index 8942b694d30d9..924e58c4ea89c 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json @@ -432,6 +432,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -581,6 +585,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -848,6 +856,10 @@ "Action": [ "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-separate-source.lit.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-separate-source.lit.expected.json index 66f5f9c1dc2be..6fb7325e5f0d2 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-separate-source.lit.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-separate-source.lit.expected.json @@ -402,6 +402,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -644,6 +648,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -967,6 +975,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -1064,6 +1076,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-events.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-events.expected.json index 19be710545e7e..0a7d958491359 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-events.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-events.expected.json @@ -35,6 +35,20 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "MyPipelineArtifactsBucketEncryptionKeyAlias9D4F8C59": { + "Type": "AWS::KMS::Alias", + "Properties": { + "AliasName": "alias/codepipeline-awscdkpipelineeventtargetmypipeline4ae5d407", + "TargetKeyId": { + "Fn::GetAtt": [ + "MyPipelineArtifactsBucketEncryptionKey8BF0A7F3", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "MyPipelineArtifactsBucket727923DD": { "Type": "AWS::S3::Bucket", "Properties": { @@ -63,19 +77,52 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, - "MyPipelineArtifactsBucketEncryptionKeyAlias9D4F8C59": { - "Type": "AWS::KMS::Alias", + "MyPipelineArtifactsBucketPolicyDFDA675B": { + "Type": "AWS::S3::BucketPolicy", "Properties": { - "AliasName": "alias/codepipeline-awscdkpipelineeventtargetmypipeline4ae5d407", - "TargetKeyId": { - "Fn::GetAtt": [ - "MyPipelineArtifactsBucketEncryptionKey8BF0A7F3", - "Arn" - ] + "Bucket": { + "Ref": "MyPipelineArtifactsBucket727923DD" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "MyPipelineArtifactsBucket727923DD", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyPipelineArtifactsBucket727923DD", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" } - }, - "UpdateReplacePolicy": "Delete", - "DeletionPolicy": "Delete" + } }, "MyPipelineRoleC0D47CA4": { "Type": "AWS::IAM::Role", @@ -106,6 +153,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -327,6 +378,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -714,6 +769,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-jenkins.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-jenkins.expected.json index 5c89d0d1119b9..eda778cf7ec55 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-jenkins.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-jenkins.expected.json @@ -39,6 +39,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -276,6 +280,10 @@ "Action": [ "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-manual-approval.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-manual-approval.expected.json index f0c66384b1709..1193cbb2f30d0 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-manual-approval.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-manual-approval.expected.json @@ -34,6 +34,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -243,6 +247,10 @@ "Action": [ "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-s3-deploy.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-s3-deploy.expected.json index ecd97ebdd239d..9cc08aec93f44 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-s3-deploy.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-s3-deploy.expected.json @@ -44,6 +44,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -261,6 +265,10 @@ "Action": [ "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -339,6 +347,10 @@ "Action": [ "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -366,7 +378,10 @@ ] }, { - "Action": "s3:PutObjectAcl", + "Action": [ + "s3:PutObjectAcl", + "s3:PutObjectVersionAcl" + ], "Effect": "Allow", "Resource": { "Fn::Join": [ diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-stepfunctions.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-stepfunctions.expected.json index 03e04ca5348b1..d35d3f59e6bb2 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-stepfunctions.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-stepfunctions.expected.json @@ -120,6 +120,53 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, + "MyPipelineArtifactsBucketPolicyDFDA675B": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "MyPipelineArtifactsBucket727923DD" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "MyPipelineArtifactsBucket727923DD", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyPipelineArtifactsBucket727923DD", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, "MyPipelineRoleC0D47CA4": { "Type": "AWS::IAM::Role", "Properties": { @@ -149,6 +196,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -386,6 +437,10 @@ "Action": [ "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/lambda-invoke-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/lambda-invoke-action.test.ts index 1acf0b4764cd6..c8d3c5e1dba7d 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/lambda-invoke-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/lambda-invoke-action.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template, Match } from '@aws-cdk/assertions'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; @@ -21,7 +21,7 @@ describe('', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', Match.objectLike({ 'Stages': [ {}, { @@ -34,9 +34,7 @@ describe('', () => { ], }, ], - }); - - + })); }); test('properly resolves any Tokens passed in userParameters', () => { @@ -45,8 +43,7 @@ describe('', () => { key: Lazy.string({ produce: () => Aws.REGION }), }, }); - - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', Match.objectLike({ 'Stages': [ {}, { @@ -70,9 +67,7 @@ describe('', () => { ], }, ], - }); - - + })); }); test('properly resolves any stringified Tokens passed in userParameters', () => { @@ -82,7 +77,7 @@ describe('', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', Match.objectLike({ 'Stages': [ {}, { @@ -95,9 +90,7 @@ describe('', () => { ], }, ], - }); - - + })); }); test('properly assings userParametersString to UserParameters', () => { @@ -105,7 +98,7 @@ describe('', () => { userParamsString: '**/*.template.json', }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', Match.objectLike({ 'Stages': [ {}, { @@ -118,7 +111,7 @@ describe('', () => { ], }, ], - }); + })); }); test('throws if both userParameters and userParametersString are supplied', () => { @@ -135,7 +128,7 @@ describe('', () => { lambdaInput: new codepipeline.Artifact(), }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', Match.objectLike({ 'PolicyDocument': { 'Statement': [ { @@ -164,9 +157,7 @@ describe('', () => { }, ], }, - }); - - + })); }); testFutureBehavior("assigns the Action's Role with write permissions to the Bucket if it has only outputs", s3GrantWriteCtx, App, (app) => { @@ -175,8 +166,9 @@ describe('', () => { // no input to the Lambda Action - we want write permissions only in this case }, app); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { - 'PolicyDocument': { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyName: 'PipelineInvokeLambdaCodePipelineActionRoleDefaultPolicy103F34DA', + 'PolicyDocument': Match.objectLike({ 'Statement': [ { 'Action': 'lambda:ListFunctions', @@ -191,6 +183,10 @@ describe('', () => { 'Action': [ 's3:DeleteObject*', 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', 's3:Abort*', ], 'Effect': 'Allow', @@ -200,14 +196,13 @@ describe('', () => { 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', + 'kms:Decrypt', ], 'Effect': 'Allow', }, ], - }, + }), }); - - }); testFutureBehavior("assigns the Action's Role with read-write permissions to the Bucket if it has both inputs and outputs", s3GrantWriteCtx, App, (app) => { @@ -216,8 +211,9 @@ describe('', () => { lambdaOutput: new codepipeline.Artifact(), }, app); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { - 'PolicyDocument': { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyName: 'PipelineInvokeLambdaCodePipelineActionRoleDefaultPolicy103F34DA', + 'PolicyDocument': Match.objectLike({ 'Statement': [ { 'Action': 'lambda:ListFunctions', @@ -247,6 +243,10 @@ describe('', () => { 'Action': [ 's3:DeleteObject*', 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', 's3:Abort*', ], 'Effect': 'Allow', @@ -256,14 +256,13 @@ describe('', () => { 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', + 'kms:Decrypt', ], 'Effect': 'Allow', }, ], - }, + }), }); - - }); test('exposes variables for other actions to consume', () => { @@ -304,7 +303,7 @@ describe('', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', Match.objectLike({ 'Stages': [ { 'Name': 'Source', @@ -325,9 +324,7 @@ describe('', () => { ], }, ], - }); - - + })); }); }); }); 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 bf894a5db1c88..6ebfe84b84c8a 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,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as iam from '@aws-cdk/aws-iam'; import * as sns from '@aws-cdk/aws-sns'; @@ -48,7 +48,7 @@ describe('manual approval', () => { manualApprovalAction.grantManualApproval(role); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -154,7 +154,7 @@ describe('manual approval', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', 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 1c680e0c7e95d..16a1bc66e4cce 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { SynthUtils } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; @@ -45,7 +44,7 @@ describe('pipeline', () => { ], }); - expect(SynthUtils.toCloudFormation(stack)).not.toEqual({}); + expect(Template.fromStack(stack).toJSON()).not.toEqual({}); expect([]).toEqual(ConstructNode.validate(pipeline.node)); }); @@ -79,7 +78,7 @@ describe('pipeline', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Name': { 'Ref': 'AWS::StackName', }, @@ -118,9 +117,9 @@ describe('pipeline', () => { ], }); - expect(stack).not.toHaveResourceLike('AWS::CodePipeline::Webhook'); + Template.fromStack(stack).resourceCountIs('AWS::CodePipeline::Webhook', 0); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -177,9 +176,9 @@ describe('pipeline', () => { ], }); - expect(stack).not.toHaveResourceLike('AWS::CodePipeline::Webhook'); + Template.fromStack(stack).resourceCountIs('AWS::CodePipeline::Webhook', 0); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -235,9 +234,9 @@ describe('pipeline', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Webhook'); + Template.fromStack(stack).resourceCountIs('AWS::CodePipeline::Webhook', 1); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'ArtifactStore': { 'Location': { 'Ref': 'PArtifactsBucket5E711C12', @@ -338,7 +337,7 @@ describe('pipeline', () => { }, }); - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'Description': 'desc', 'EventPattern': { 'detail': { @@ -402,7 +401,7 @@ describe('pipeline', () => { projectName: 'MyProject', }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Name': 'MyProject', 'Source': { 'Type': 'CODEPIPELINE', @@ -481,7 +480,7 @@ describe('pipeline', () => { actions: [lambdaAction], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'ArtifactStore': { 'Location': { 'Ref': 'PipelineArtifactsBucket22248F97', @@ -532,7 +531,7 @@ describe('pipeline', () => { expect((lambdaAction.actionProperties.outputs || []).length).toEqual(3); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -616,7 +615,7 @@ describe('pipeline', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'ArtifactStores': [ { 'Region': 'us-west-1', @@ -733,9 +732,9 @@ describe('pipeline', () => { ], }); - expect(stack).toCountResources('AWS::S3::Bucket', 1); + Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 1); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'ArtifactStores': [ { 'Region': pipelineRegion, @@ -786,7 +785,7 @@ describe('pipeline', () => { ], }); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'ArtifactStores': [ { 'Region': replicationRegion, @@ -833,7 +832,7 @@ describe('pipeline', () => { ], }); - expect(replicationStack).toHaveResourceLike('AWS::S3::Bucket', { + Template.fromStack(replicationStack).hasResourceProperties('AWS::S3::Bucket', { 'BucketName': 'replicationstackeplicationbucket2464cd5c33b386483b66', }); @@ -895,7 +894,7 @@ describe('pipeline', () => { ], }); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -926,7 +925,7 @@ describe('pipeline', () => { ], }); - expect(buildStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(buildStack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -1038,7 +1037,7 @@ describe('pipeline', () => { ], }); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -1122,7 +1121,7 @@ describe('pipeline', () => { ], }); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-deploy-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-deploy-action.test.ts index 580a4dc688e19..99383e33ae541 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-deploy-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-deploy-action.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as s3 from '@aws-cdk/aws-s3'; import { App, Duration, SecretValue, Stack } from '@aws-cdk/core'; @@ -14,7 +14,7 @@ describe('', () => { const stack = new Stack(); minimalPipeline(stack); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -53,7 +53,7 @@ describe('', () => { const stack = new Stack(app); minimalPipeline(stack); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -64,6 +64,10 @@ describe('', () => { 's3:List*', 's3:DeleteObject*', 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', 's3:Abort*', ], }, @@ -85,7 +89,7 @@ describe('', () => { accessControl: s3.BucketAccessControl.PUBLIC_READ_WRITE, }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ {}, { @@ -113,7 +117,7 @@ describe('', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ {}, { @@ -137,7 +141,7 @@ describe('', () => { objectKey: '/a/b/c', }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ {}, { @@ -169,7 +173,7 @@ describe('', () => { bucket: deployBucket, }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: [ {}, { 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 65553aee81be5..0fe6b311ad8a3 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,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template, Match } from '@aws-cdk/assertions'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as s3 from '@aws-cdk/aws-s3'; @@ -14,7 +14,7 @@ describe('S3 source Action', () => { minimalPipeline(stack, undefined); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', Match.objectLike({ 'Stages': [ { 'Actions': [ @@ -26,9 +26,9 @@ describe('S3 source Action', () => { }, {}, ], - }); + })); - expect(stack).not.toHaveResourceLike('AWS::Events::Rule'); + Template.fromStack(stack).resourceCountIs('AWS::Events::Rule', 0); }); @@ -38,22 +38,22 @@ describe('S3 source Action', () => { minimalPipeline(stack, { trigger: cpactions.S3Trigger.EVENTS }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', Match.objectLike({ 'Stages': [ { - 'Actions': [ - { + 'Actions': Match.arrayWith([ + Match.objectLike({ 'Configuration': { 'PollForSourceChanges': false, }, - }, - ], + }), + ]), }, {}, ], - }); + })); - expect(stack).toCountResources('AWS::Events::Rule', 1); + Template.fromStack(stack).resourceCountIs('AWS::Events::Rule', 1); }); @@ -63,7 +63,7 @@ describe('S3 source Action', () => { minimalPipeline(stack, { trigger: cpactions.S3Trigger.POLL }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', Match.objectLike({ 'Stages': [ { 'Actions': [ @@ -76,9 +76,9 @@ describe('S3 source Action', () => { }, {}, ], - }); + })); - expect(stack).not.toHaveResourceLike('AWS::Events::Rule'); + Template.fromStack(stack).resourceCountIs('AWS::Events::Rule', 0); }); @@ -88,7 +88,7 @@ describe('S3 source Action', () => { minimalPipeline(stack, { trigger: cpactions.S3Trigger.NONE }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', Match.objectLike({ 'Stages': [ { 'Actions': [ @@ -101,11 +101,9 @@ describe('S3 source Action', () => { }, {}, ], - }); - - expect(stack).not.toHaveResourceLike('AWS::Events::Rule'); - + })); + Template.fromStack(stack).resourceCountIs('AWS::Events::Rule', 0); }); test('does not allow passing an empty string for the bucketKey property', () => { @@ -171,8 +169,6 @@ describe('S3 source Action', () => { expect(() => { sourceStage.addAction(duplicateBucketAndPath); }).toThrow(/S3 source action with path 'my\/other\/path' is already present in the pipeline for this source bucket/); - - }); test('allows using a Token bucketKey with trigger = Events, multiple times', () => { @@ -192,9 +188,9 @@ describe('S3 source Action', () => { output: new codepipeline.Artifact(), })); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - 'Stages': [ - { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', Match.objectLike({ + 'Stages': Match.arrayWith([ + Match.objectLike({ 'Actions': [ { 'Configuration': { @@ -207,11 +203,9 @@ describe('S3 source Action', () => { }, }, ], - }, - ], - }); - - + }), + ]), + })); }); test('exposes variables for other actions to consume', () => { @@ -246,7 +240,7 @@ describe('S3 source Action', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', Match.objectLike({ 'Stages': [ { 'Name': 'Source', @@ -263,9 +257,7 @@ describe('S3 source Action', () => { ], }, ], - }); - - + })); }); }); }); 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 cf490f21fc206..0be3e3a610c49 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,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template, Match } from '@aws-cdk/assertions'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import { Stack } from '@aws-cdk/core'; @@ -19,13 +19,13 @@ describe('ServiceCatalog Deploy Action', () => { productId: 'prod-xxxxxxxxx', })); // THEN - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - 'Stages': [ - { 'Name': 'Source' /* don't care about the rest */ }, - { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + 'Stages': Match.arrayWith([ + Match.objectLike({ 'Name': 'Source' /* don't care about the rest */ }), + Match.objectLike({ 'Name': 'Deploy', - 'Actions': [ - { + 'Actions': Match.arrayWith([ + Match.objectLike({ 'ActionTypeId': { 'Category': 'Deploy', 'Owner': 'AWS', @@ -45,10 +45,10 @@ describe('ServiceCatalog Deploy Action', () => { }, ], 'Name': 'ServiceCatalogTest', - }, - ], - }, - ], + }), + ]), + }), + ]), }); @@ -64,13 +64,13 @@ describe('ServiceCatalog Deploy Action', () => { productId: 'prod-xxxxxxxxx', })); // THEN - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - 'Stages': [ - { 'Name': 'Source' /* don't care about the rest */ }, - { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + 'Stages': Match.arrayWith([ + Match.objectLike({ 'Name': 'Source' /* don't care about the rest */ }), + Match.objectLike({ 'Name': 'Deploy', - 'Actions': [ - { + 'Actions': Match.arrayWith([ + Match.objectLike({ 'ActionTypeId': { 'Category': 'Deploy', 'Owner': 'AWS', @@ -89,10 +89,10 @@ describe('ServiceCatalog Deploy Action', () => { }, ], 'Name': 'ServiceCatalogTest', - }, - ], - }, - ], + }), + ]), + }), + ]), }); 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 0ad5d8b2213a1..0825f8fdfcf35 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,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template, Match } from '@aws-cdk/assertions'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as s3 from '@aws-cdk/aws-s3'; import * as stepfunction from '@aws-cdk/aws-stepfunctions'; @@ -14,7 +14,7 @@ describe('StepFunctions Invoke Action', () => { minimalPipeline(stack); // then - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', Match.objectLike({ Stages: [ // Must have a source stage { @@ -57,7 +57,7 @@ describe('StepFunctions Invoke Action', () => { ], }, ], - }); + })); }); @@ -67,9 +67,10 @@ describe('StepFunctions Invoke Action', () => { minimalPipeline(stack); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyName: 'MyPipelineInvokeCodePipelineActionRoleDefaultPolicy07A602B1', PolicyDocument: { - Statement: [ + Statement: Match.arrayWith([ { Action: ['states:StartExecution', 'states:DescribeStateMachine'], Resource: { @@ -77,11 +78,11 @@ describe('StepFunctions Invoke Action', () => { }, Effect: 'Allow', }, - ], + ]), }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Role'); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 4); }); @@ -91,7 +92,7 @@ describe('StepFunctions Invoke Action', () => { minimalPipeline(stack); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ {}, @@ -137,7 +138,7 @@ describe('StepFunctions Invoke Action', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Role'); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 4); }); diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 7e02b83d03939..6dad03744c8e7 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -399,6 +399,7 @@ export class Pipeline extends PipelineBase { bucketName: PhysicalName.GENERATE_IF_NEEDED, encryptionKey, encryption: encryptionKey ? s3.BucketEncryption.KMS : s3.BucketEncryption.KMS_MANAGED, + enforceSSL: true, blockPublicAccess: new s3.BlockPublicAccess(s3.BlockPublicAccess.BLOCK_ALL), removalPolicy: RemovalPolicy.RETAIN, }); diff --git a/packages/@aws-cdk/aws-codepipeline/lib/private/cross-region-support-stack.ts b/packages/@aws-cdk/aws-codepipeline/lib/private/cross-region-support-stack.ts index 9ab45f8942436..5decade872f1e 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/private/cross-region-support-stack.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/private/cross-region-support-stack.ts @@ -77,6 +77,7 @@ export class CrossRegionSupportConstruct extends Construct { bucketName: cdk.PhysicalName.GENERATE_IF_NEEDED, encryption: encryptionAlias ? s3.BucketEncryption.KMS : s3.BucketEncryption.KMS_MANAGED, encryptionKey: encryptionAlias, + enforceSSL: true, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, }); } diff --git a/packages/@aws-cdk/aws-codepipeline/package.json b/packages/@aws-cdk/aws-codepipeline/package.json index a7c4926caeebf..8d3f6978b445a 100644 --- a/packages/@aws-cdk/aws-codepipeline/package.json +++ b/packages/@aws-cdk/aws-codepipeline/package.json @@ -84,14 +84,14 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-codestarnotifications": "0.0.0", diff --git a/packages/@aws-cdk/aws-codepipeline/test/action.test.ts b/packages/@aws-cdk/aws-codepipeline/test/action.test.ts index 69a5839385fcd..a8b6529b3e80a 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/action.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as codepipeline from '../lib'; @@ -10,54 +10,45 @@ import { FakeSourceAction } from './fake-source-action'; describe('action', () => { describe('artifact bounds validation', () => { - test('artifacts count exceed maximum', () => { const result = boundsValidationResult(1, 0, 0); expect(result.length).toEqual(1); expect(result[0]).toMatch(/cannot have more than 0/); - }); test('artifacts count below minimum', () => { const result = boundsValidationResult(1, 2, 2); expect(result.length).toEqual(1); expect(result[0]).toMatch(/must have at least 2/); - }); test('artifacts count within bounds', () => { const result = boundsValidationResult(1, 0, 2); expect(result.length).toEqual(0); - }); }); describe('action type validation', () => { - test('must be source and is source', () => { const result = validations.validateSourceAction(true, codepipeline.ActionCategory.SOURCE, 'test action', 'test stage'); expect(result.length).toEqual(0); - }); test('must be source and is not source', () => { const result = validations.validateSourceAction(true, codepipeline.ActionCategory.DEPLOY, 'test action', 'test stage'); expect(result.length).toEqual(1); expect(result[0]).toMatch(/may only contain Source actions/); - }); test('cannot be source and is source', () => { const result = validations.validateSourceAction(false, codepipeline.ActionCategory.SOURCE, 'test action', 'test stage'); expect(result.length).toEqual(1); expect(result[0]).toMatch(/may only occur in first stage/); - }); test('cannot be source and is not source', () => { const result = validations.validateSourceAction(false, codepipeline.ActionCategory.DEPLOY, 'test action', 'test stage'); expect(result.length).toEqual(0); - }); }); @@ -74,8 +65,6 @@ describe('action', () => { expect(() => { stage.addAction(action); }).toThrow(/Action name must match regular expression:/); - - }); }); @@ -121,11 +110,9 @@ describe('action', () => { }); expect(() => { - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { }); }).toThrow(/Build\/Fake cannot have more than 3 input artifacts/); - - }); test('validates that output Artifacts are within bounds', () => { @@ -166,11 +153,9 @@ describe('action', () => { }); expect(() => { - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { }); }).toThrow(/Source\/Fake cannot have more than 4 output artifacts/); - - }); }); @@ -199,7 +184,7 @@ describe('action', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -234,8 +219,6 @@ describe('action', () => { }, ], }); - - }); test('the same Action can be safely added to 2 different Stages', () => { @@ -270,8 +253,6 @@ describe('action', () => { expect(() => { pipeline.addStage(stage3); }).not.toThrow(/FakeAction/); - - }); describe('input Artifacts', () => { @@ -285,8 +266,6 @@ describe('action', () => { extraInputs: [artifact], }); }).not.toThrow(); - - }); test('can have duplicate names', () => { @@ -300,8 +279,6 @@ describe('action', () => { extraInputs: [artifact2], }); }).not.toThrow(); - - }); }); @@ -317,8 +294,6 @@ describe('action', () => { ], }); }).not.toThrow(); - - }); }); @@ -355,8 +330,6 @@ describe('action', () => { expect(() => { buildStage.addAction(buildAction); }).toThrow(/Role is not supported for actions with an owner different than 'AWS' - got 'ThirdParty' \(Action: 'build' in Stage: 'Build'\)/); - - }); test('actions can be retrieved from stages they have been added to', () => { @@ -399,8 +372,6 @@ describe('action', () => { expect(buildStage.actions.length).toEqual(2); expect(buildStage.actions[0].actionProperties.actionName).toEqual('build1'); expect(buildStage.actions[1].actionProperties.actionName).toEqual('build2'); - - }); }); diff --git a/packages/@aws-cdk/aws-codepipeline/test/artifacts.test.ts b/packages/@aws-cdk/aws-codepipeline/test/artifacts.test.ts index 4570ea850df3a..9e2e62a6aedda 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/artifacts.test.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/artifacts.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as codepipeline from '../lib'; import { FakeBuildAction } from './fake-build-action'; @@ -10,8 +10,6 @@ describe('artifacts', () => { describe('Artifacts in CodePipeline', () => { test('cannot be created with an empty name', () => { expect(() => new codepipeline.Artifact('')).toThrow(/Artifact name must match regular expression/); - - }); test('without a name, when used as an input without being used as an output first - should fail validation', () => { @@ -45,8 +43,6 @@ describe('artifacts', () => { expect(errors.length).toEqual(1); const error = errors[0]; expect(error).toMatch(/Action 'Build' is using an unnamed input Artifact, which is not being produced in this pipeline/); - - }); test('with a name, when used as an input without being used as an output first - should fail validation', () => { @@ -80,8 +76,6 @@ describe('artifacts', () => { expect(errors.length).toEqual(1); const error = errors[0]; expect(error).toMatch(/Action 'Build' is using input Artifact 'named', which is not being produced in this pipeline/); - - }); test('without a name, when used as an output multiple times - should fail validation', () => { @@ -116,8 +110,6 @@ describe('artifacts', () => { expect(errors.length).toEqual(1); const error = errors[0]; expect(error).toMatch(/Both Actions 'Source' and 'Build' are producting Artifact 'Artifact_Source_Source'. Every artifact can only be produced once./); - - }); test("an Action's output can be used as input for an Action in the same Stage with a higher runOrder", () => { @@ -162,9 +154,7 @@ describe('artifacts', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline'); - - + Template.fromStack(stack).resourceCountIs('AWS::CodePipeline::Pipeline', 1); }); test('violation of runOrder constraints is detected and reported', () => { @@ -215,8 +205,6 @@ describe('artifacts', () => { expect(errors.length).toEqual(1); const error = errors[0]; expect(error).toMatch(/Stage 2 Action 2 \('Build'\/'build2'\) is consuming input Artifact 'buildOutput1' before it is being produced at Stage 2 Action 3 \('Build'\/'build1'\)/); - - }); test('without a name, sanitize the auto stage-action derived name', () => { @@ -246,7 +234,7 @@ describe('artifacts', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source.@', @@ -272,8 +260,6 @@ describe('artifacts', () => { }, ], }); - - }); }); }); diff --git a/packages/@aws-cdk/aws-codepipeline/test/cross-env.test.ts b/packages/@aws-cdk/aws-codepipeline/test/cross-env.test.ts index f26de209ecaf9..9e2608dd36f36 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/cross-env.test.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/cross-env.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; import { Stack, App, Stage as CdkStage } from '@aws-cdk/core'; @@ -58,8 +58,8 @@ describe.each([ test('creates a bucket but no keys', () => { // THEN - expect(stack).not.toHaveResource('AWS::KMS::Key'); - expect(stack).toHaveResource('AWS::S3::Bucket'); + Template.fromStack(stack).resourceCountIs('AWS::KMS::Key', 0); + Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 1); }); describe('prevents adding a cross-account action', () => { @@ -128,8 +128,8 @@ describe.each([ const supportStack = asm.getStackByName(`${stack.stackName}-support-eu-west-1`); // THEN - expect(supportStack).not.toHaveResource('AWS::KMS::Key'); - expect(supportStack).toHaveResourceLike('AWS::S3::Bucket', { + Template.fromJSON(supportStack.template).resourceCountIs('AWS::KMS::Key', 0); + Template.fromJSON(supportStack.template).hasResourceProperties('AWS::S3::Bucket', { PublicAccessBlockConfiguration: { BlockPublicAcls: true, BlockPublicPolicy: true, @@ -150,8 +150,8 @@ describe.each([ })); // THEN - expect(stack2).not.toHaveResource('AWS::KMS::Key'); - expect(stack2).toHaveResource('AWS::S3::Bucket'); + Template.fromStack(stack2).resourceCountIs('AWS::KMS::Key', 0); + Template.fromStack(stack2).resourceCountIs('AWS::S3::Bucket', 1); }); }); }); @@ -200,11 +200,11 @@ describe('cross-environment CodePipeline', function () { const asm = app.synth(); const supportStack = asm.getStackByName(`${pipelineStack.stackName}-support-456`); - expect(supportStack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromJSON(supportStack.template).hasResourceProperties('AWS::IAM::Role', { RoleName: 'pipelinestack-support-456dbuildactionrole91c6f1a469fd11d52dfe', }); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: [ { Name: 'Source' }, { diff --git a/packages/@aws-cdk/aws-codepipeline/test/general-validation.test.ts b/packages/@aws-cdk/aws-codepipeline/test/general-validation.test.ts index d8186532d7cea..f3024ec163e9b 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/general-validation.test.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/general-validation.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import * as cdk from '@aws-cdk/core'; import { IStage } from '../lib/action'; import { Artifact } from '../lib/artifact'; @@ -30,8 +29,6 @@ describe('general validation', () => { expect(validationBlock).toThrow(); } }); - - }); describe('Stage validation', () => { @@ -39,8 +36,6 @@ describe('general validation', () => { const stage = stageForTesting(); expect((stage as any).validate().length).toEqual(1); - - }); }); @@ -50,8 +45,6 @@ describe('general validation', () => { const pipeline = new Pipeline(stack, 'Pipeline'); expect(cdk.ConstructNode.validate(pipeline.node).length).toEqual(1); - - }); test('should fail if Pipeline has a Source Action in a non-first Stage', () => { @@ -69,8 +62,6 @@ describe('general validation', () => { }); expect(cdk.ConstructNode.validate(pipeline.node).length).toEqual(1); - - }); }); }); diff --git a/packages/@aws-cdk/aws-codepipeline/test/notification-rule.test.ts b/packages/@aws-cdk/aws-codepipeline/test/notification-rule.test.ts index 46d32794dc3e1..ac178204dd263 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/notification-rule.test.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/notification-rule.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as codepipeline from '../lib'; import { FakeBuildAction } from './fake-build-action'; @@ -35,7 +35,7 @@ describe('pipeline with codestar notification integration', () => { pipeline.notifyOnExecutionStateChange('NotifyOnExecutionStateChange', target); - expect(stack).toHaveResourceLike('AWS::CodeStarNotifications::NotificationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeStarNotifications::NotificationRule', { Name: 'PipelineNotifyOnExecutionStateChange9FE60973', DetailType: 'FULL', EventTypeIds: [ @@ -77,7 +77,7 @@ describe('pipeline with codestar notification integration', () => { pipeline.notifyOnAnyStageStateChange('NotifyOnAnyStageStateChange', target); - expect(stack).toHaveResourceLike('AWS::CodeStarNotifications::NotificationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeStarNotifications::NotificationRule', { Name: 'PipelineNotifyOnAnyStageStateChange05355CCD', DetailType: 'FULL', EventTypeIds: [ @@ -118,7 +118,7 @@ describe('pipeline with codestar notification integration', () => { pipeline.notifyOnAnyActionStateChange('NotifyOnAnyActionStateChange', target); - expect(stack).toHaveResourceLike('AWS::CodeStarNotifications::NotificationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeStarNotifications::NotificationRule', { Name: 'PipelineNotifyOnAnyActionStateChange64D5B2AA', DetailType: 'FULL', EventTypeIds: [ @@ -158,7 +158,7 @@ describe('pipeline with codestar notification integration', () => { pipeline.notifyOnAnyManualApprovalStateChange('NotifyOnAnyManualApprovalStateChange', target); - expect(stack).toHaveResourceLike('AWS::CodeStarNotifications::NotificationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeStarNotifications::NotificationRule', { Name: 'PipelineNotifyOnAnyManualApprovalStateChangeE60778F7', DetailType: 'FULL', EventTypeIds: [ diff --git a/packages/@aws-cdk/aws-codepipeline/test/pipeline.test.ts b/packages/@aws-cdk/aws-codepipeline/test/pipeline.test.ts index 5af1eea557f3f..dd4f79d040201 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/pipeline.test.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/pipeline.test.ts @@ -1,5 +1,4 @@ -import { expect as ourExpect, ResourcePart, arrayWith, objectLike, haveResourceLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; @@ -22,20 +21,36 @@ describe('', () => { const role = new iam.Role(stack, 'Role', { assumedBy: new iam.ServicePrincipal('codepipeline.amazonaws.com'), }); - new codepipeline.Pipeline(stack, 'Pipeline', { + const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { role, }); - ourExpect(stack, true).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + // Adding 2 stages with actions so pipeline validation will pass + const sourceArtifact = new codepipeline.Artifact(); + pipeline.addStage({ + stageName: 'Source', + actions: [new FakeSourceAction({ + actionName: 'FakeSource', + output: sourceArtifact, + })], + }); + + pipeline.addStage({ + stageName: 'Build', + actions: [new FakeBuildAction({ + actionName: 'FakeBuild', + input: sourceArtifact, + })], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'RoleArn': { 'Fn::GetAtt': [ 'Role1ABCC5F0', 'Arn', ], }, - })); - - + }); }); test('can be imported by ARN', () => { @@ -46,8 +61,6 @@ describe('', () => { expect(pipeline.pipelineArn).toEqual('arn:aws:codepipeline:us-east-1:123456789012:MyPipeline'); expect(pipeline.pipelineName).toEqual('MyPipeline'); - - }); describe('that is cross-region', () => { @@ -67,8 +80,6 @@ describe('', () => { expect(() => { sourceStage.addAction(sourceAction); }).toThrow(/Source action 'FakeSource' must be in the same region as the pipeline/); - - }); test('allows passing an Alias in place of the KMS Key in the replication Bucket', () => { @@ -113,7 +124,7 @@ describe('', () => { ], }); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'ArtifactStores': [ { 'Region': replicationRegion, @@ -142,7 +153,7 @@ describe('', () => { ], }); - expect(replicationStack).toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(replicationStack).hasResourceProperties('AWS::KMS::Key', { 'KeyPolicy': { 'Statement': [ { @@ -175,8 +186,6 @@ describe('', () => { ], }, }); - - }); test('generates ArtifactStores with the alias ARN as the KeyID', () => { @@ -208,7 +217,7 @@ describe('', () => { ], }); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'ArtifactStores': [ { 'Region': replicationRegion, @@ -237,12 +246,10 @@ describe('', () => { ], }); - expect(pipeline.crossRegionSupport[replicationRegion].stack).toHaveResourceLike('AWS::KMS::Alias', { + Template.fromStack(pipeline.crossRegionSupport[replicationRegion].stack).hasResource('AWS::KMS::Alias', { 'DeletionPolicy': 'Delete', 'UpdateReplacePolicy': 'Delete', - }, ResourcePart.CompleteDefinition); - - + }); }); test('allows passing an imported Bucket and Key for the replication Bucket', () => { @@ -280,7 +287,7 @@ describe('', () => { ], }); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'ArtifactStores': [ { 'Region': replicationRegion, @@ -298,8 +305,6 @@ describe('', () => { }, ], }); - - }); test('generates the support stack containing the replication Bucket without the need to bootstrap in that environment', () => { @@ -380,18 +385,18 @@ describe('', () => { const supportStackBArtifact = assembly.getStackByName('PipelineStackB-support-eu-south-1'); const supportStackATemplate = supportStackAArtifact.template; - expect(supportStackATemplate).toHaveResourceLike('AWS::S3::Bucket', { + Template.fromJSON(supportStackATemplate).hasResourceProperties('AWS::S3::Bucket', { BucketName: 'pipelinestacka-support-eueplicationbucket8934e91f26961aa6cbfa', }); - expect(supportStackATemplate).toHaveResourceLike('AWS::KMS::Alias', { + Template.fromJSON(supportStackATemplate).hasResourceProperties('AWS::KMS::Alias', { AliasName: 'alias/pport-eutencryptionalias02f1cda3732942f6c529', }); const supportStackBTemplate = supportStackBArtifact.template; - expect(supportStackBTemplate).toHaveResourceLike('AWS::S3::Bucket', { + Template.fromJSON(supportStackBTemplate).hasResourceProperties('AWS::S3::Bucket', { BucketName: 'pipelinestackb-support-eueplicationbucketdf7c0e10245faa377228', }); - expect(supportStackBTemplate).toHaveResourceLike('AWS::KMS::Alias', { + Template.fromJSON(supportStackBTemplate).hasResourceProperties('AWS::KMS::Alias', { AliasName: 'alias/pport-eutencryptionaliasdef3fd3fec63bc54980e', }); }); @@ -419,8 +424,6 @@ describe('', () => { account: cdk.Aws.ACCOUNT_ID, })); }).toThrow(/The 'account' property must be a concrete value \(action: 'FakeBuild'\)/); - - }); test('does not allow an env-agnostic Pipeline Stack if an Action account has been provided', () => { @@ -444,8 +447,6 @@ describe('', () => { account: '123456789012', })); }).toThrow(/Pipeline stack which uses cross-environment actions must have an explicitly set account/); - - }); test('does not allow enabling key rotation if cross account keys have been disabled', () => { @@ -478,7 +479,7 @@ describe('', () => { ], }); - expect(stack).toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { 'EnableKeyRotation': true, }); }); @@ -513,19 +514,18 @@ describe('test with shared setup', () => { pipeline.stage('Build').addAction(new FakeBuildAction({ actionName: 'debug.com', input: sourceArtifact })); // THEN - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Build', Actions: [ - objectLike({ Name: 'Gcc' }), - objectLike({ Name: 'debug.com' }), + Match.objectLike({ Name: 'Gcc' }), + Match.objectLike({ Name: 'debug.com' }), ], - }), + }]), }); }); }); - interface ReusePipelineStackProps extends cdk.StackProps { reuseCrossRegionSupportStacks?: boolean; } diff --git a/packages/@aws-cdk/aws-codepipeline/test/stages.test.ts b/packages/@aws-cdk/aws-codepipeline/test/stages.test.ts index d7931a23a531e..ca5882a4c42a6 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/stages.test.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/stages.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as codepipeline from '../lib'; import { Stage } from '../lib/private/stage'; @@ -33,14 +33,12 @@ describe('stages', () => { })); // -- - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'FirstStage' }, { 'Name': 'SecondStage' }, ], }); - - }); test('can be inserted after another Stage', () => { @@ -72,15 +70,13 @@ describe('stages', () => { })); // -- - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'FirstStage' }, { 'Name': 'SecondStage' }, { 'Name': 'ThirdStage' }, ], }); - - }); test("attempting to insert a Stage before a Stage that doesn't exist results in an error", () => { @@ -97,8 +93,6 @@ describe('stages', () => { }, }); }).toThrow(/before/i); - - }); test("attempting to insert a Stage after a Stage that doesn't exist results in an error", () => { @@ -115,8 +109,6 @@ describe('stages', () => { }, }); }).toThrow(/after/i); - - }); test('providing more than one placement value results in an error', () => { @@ -135,8 +127,6 @@ describe('stages', () => { // incredibly, an arrow function below causes nodeunit to crap out with: // "TypeError: Function has non-object prototype 'undefined' in instanceof check" }).toThrow(/(rightBefore.*justAfter)|(justAfter.*rightBefore)/); - - }); test('can be retrieved from a pipeline after it has been created', () => { @@ -160,8 +150,6 @@ describe('stages', () => { stageName: 'ThirdStage', }, pipeline)); expect(pipeline.stageCount).toEqual(2); - - }); }); }); diff --git a/packages/@aws-cdk/aws-codepipeline/test/variables.test.ts b/packages/@aws-cdk/aws-codepipeline/test/variables.test.ts index 537b108df805a..6d70847bec853 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/variables.test.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/variables.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { arrayWith, objectLike } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as codepipeline from '../lib'; import { FakeBuildAction } from './fake-build-action'; @@ -38,21 +37,19 @@ describe('variables', () => { })); // -- - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - 'Stages': [ + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + 'Stages': Match.arrayWith([ { 'Name': 'Source', 'Actions': [ - { + Match.objectLike({ 'Name': 'Source', 'Namespace': 'MyNamespace', - }, + }), ], }, - ], + ]), }); - - }); test('allows using the variable in the configuration of a different action', () => { @@ -80,7 +77,7 @@ describe('variables', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -98,8 +95,6 @@ describe('variables', () => { }, ], }); - - }); test('fails when trying add an action using variables with an empty string for the namespace to a pipeline', () => { @@ -116,8 +111,6 @@ describe('variables', () => { expect(() => { sourceStage.addAction(sourceAction); }).toThrow(/Namespace name must match regular expression:/); - - }); test('can use global variables', () => { @@ -144,12 +137,12 @@ describe('variables', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - 'Stages': arrayWith( + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + 'Stages': Match.arrayWith([ { 'Name': 'Build', 'Actions': [ - objectLike({ + Match.objectLike({ 'Name': 'Build', 'Configuration': { 'CustomConfigKey': '#{codepipeline.PipelineExecutionId}', @@ -157,10 +150,8 @@ describe('variables', () => { }), ], }, - ), + ]), }); - - }); }); }); diff --git a/packages/@aws-cdk/aws-codestar/README.md b/packages/@aws-cdk/aws-codestar/README.md index e9c6ec2e4bf75..6bdf425aab1d1 100644 --- a/packages/@aws-cdk/aws-codestar/README.md +++ b/packages/@aws-cdk/aws-codestar/README.md @@ -29,13 +29,13 @@ To create a new GitHub Repository and commit the assets from S3 bucket into the import * as codestar from '@aws-cdk/aws-codestar'; import * as s3 from '@aws-cdk/aws-s3' -new codestar.GitHubRepository(stack, 'GitHubRepo', { +new codestar.GitHubRepository(this, 'GitHubRepo', { owner: 'aws', repositoryName: 'aws-cdk', - accessToken: cdk.SecretValue.secretsManager('my-github-token', { + accessToken: SecretValue.secretsManager('my-github-token', { jsonField: 'token', }), - contentsBucket: s3.Bucket.fromBucketName(stack, 'Bucket', 'bucket-name'), + contentsBucket: s3.Bucket.fromBucketName(this, 'Bucket', 'bucket-name'), contentsKey: 'import.zip', }); ``` diff --git a/packages/@aws-cdk/aws-codestar/package.json b/packages/@aws-cdk/aws-codestar/package.json index 85cd5a408b921..478f09bef3b9d 100644 --- a/packages/@aws-cdk/aws-codestar/package.json +++ b/packages/@aws-cdk/aws-codestar/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -79,7 +86,7 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-s3": "0.0.0", diff --git a/packages/@aws-cdk/aws-codestar/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-codestar/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..439308f8c0176 --- /dev/null +++ b/packages/@aws-cdk/aws-codestar/rosetta/default.ts-fixture @@ -0,0 +1,10 @@ +// Fixture with packages imported, but nothing else +import { SecretValue, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codestarconnections/package.json b/packages/@aws-cdk/aws-codestarconnections/package.json index d4189eb77ec89..2e8cfc5deac99 100644 --- a/packages/@aws-cdk/aws-codestarconnections/package.json +++ b/packages/@aws-cdk/aws-codestarconnections/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-codestarnotifications/README.md b/packages/@aws-cdk/aws-codestarnotifications/README.md index b13a33089b01a..ba1da9ced0026 100644 --- a/packages/@aws-cdk/aws-codestarnotifications/README.md +++ b/packages/@aws-cdk/aws-codestarnotifications/README.md @@ -39,17 +39,17 @@ import * as codebuild from '@aws-cdk/aws-codebuild'; import * as sns from '@aws-cdk/aws-sns'; import * as chatbot from '@aws-cdk/aws-chatbot'; -const project = new codebuild.PipelineProject(stack, 'MyProject'); +const project = new codebuild.PipelineProject(this, 'MyProject'); -const topic = new sns.Topic(stack, 'MyTopic1'); +const topic = new sns.Topic(this, 'MyTopic1'); -const slack = new chatbot.SlackChannelConfiguration(stack, 'MySlackChannel', { +const slack = new chatbot.SlackChannelConfiguration(this, 'MySlackChannel', { slackChannelConfigurationName: 'YOUR_CHANNEL_NAME', slackWorkspaceId: 'YOUR_SLACK_WORKSPACE_ID', slackChannelId: 'YOUR_SLACK_CHANNEL_ID', }); -const rule = new notifications.NotificationRule(stack, 'NotificationRule', { +const rule = new notifications.NotificationRule(this, 'NotificationRule', { source: project, events: [ 'codebuild-project-build-state-succeeded', diff --git a/packages/@aws-cdk/aws-codestarnotifications/package.json b/packages/@aws-cdk/aws-codestarnotifications/package.json index 2dd20cb2c9b05..5e181702720cf 100644 --- a/packages/@aws-cdk/aws-codestarnotifications/package.json +++ b/packages/@aws-cdk/aws-codestarnotifications/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -74,12 +81,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-codestarnotifications/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-codestarnotifications/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..50d86e8a055ce --- /dev/null +++ b/packages/@aws-cdk/aws-codestarnotifications/rosetta/default.ts-fixture @@ -0,0 +1,10 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file 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 332e808ca3176..6670135d5a416 100644 --- a/packages/@aws-cdk/aws-codestarnotifications/test/notification-rule.test.ts +++ b/packages/@aws-cdk/aws-codestarnotifications/test/notification-rule.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as notifications from '../lib'; import { @@ -24,7 +24,7 @@ describe('NotificationRule', () => { events: ['codebuild-project-build-state-succeeded'], }); - expect(stack).toHaveResourceLike('AWS::CodeStarNotifications::NotificationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeStarNotifications::NotificationRule', { Resource: project.projectArn, EventTypeIds: ['codebuild-project-build-state-succeeded'], }); @@ -41,7 +41,7 @@ describe('NotificationRule', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodeStarNotifications::NotificationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeStarNotifications::NotificationRule', { Resource: repository.repositoryArn, EventTypeIds: [ 'codecommit-repository-pull-request-created', @@ -65,7 +65,7 @@ describe('NotificationRule', () => { targets: [slack], }); - expect(stack).toHaveResourceLike('AWS::CodeStarNotifications::NotificationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeStarNotifications::NotificationRule', { Name: 'MyNotificationRule', DetailType: 'FULL', EventTypeIds: [ @@ -90,7 +90,7 @@ describe('NotificationRule', () => { events: ['codebuild-project-build-state-succeeded'], }); - expect(stack).toHaveResourceLike('AWS::CodeStarNotifications::NotificationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeStarNotifications::NotificationRule', { Name: 'MyNotificationRuleGeneratedFromId', Resource: project.projectArn, EventTypeIds: ['codebuild-project-build-state-succeeded'], @@ -105,7 +105,7 @@ describe('NotificationRule', () => { events: ['codebuild-project-build-state-succeeded'], }); - expect(stack).toHaveResourceLike('AWS::CodeStarNotifications::NotificationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeStarNotifications::NotificationRule', { Name: 'ificationRuleGeneratedFromIdIsToooooooooooooooooooooooooooooLong', Resource: project.projectArn, EventTypeIds: ['codebuild-project-build-state-succeeded'], @@ -121,7 +121,7 @@ describe('NotificationRule', () => { events: ['codebuild-project-build-state-succeeded'], }); - expect(stack).toHaveResourceLike('AWS::CodeStarNotifications::NotificationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeStarNotifications::NotificationRule', { Name: 'MyNotificationRule', Resource: project.projectArn, EventTypeIds: ['codebuild-project-build-state-succeeded'], @@ -138,7 +138,7 @@ describe('NotificationRule', () => { enabled: false, }); - expect(stack).toHaveResourceLike('AWS::CodeStarNotifications::NotificationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeStarNotifications::NotificationRule', { Name: 'MyNotificationRule', Resource: project.projectArn, EventTypeIds: ['codebuild-project-build-state-succeeded'], @@ -155,7 +155,7 @@ describe('NotificationRule', () => { enabled: true, }); - expect(stack).toHaveResourceLike('AWS::CodeStarNotifications::NotificationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeStarNotifications::NotificationRule', { Name: 'MyNotificationRule', Resource: project.projectArn, EventTypeIds: ['codebuild-project-build-state-succeeded'], @@ -177,7 +177,7 @@ describe('NotificationRule', () => { expect(rule.addTarget(topic)).toEqual(true); - expect(stack).toHaveResourceLike('AWS::CodeStarNotifications::NotificationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeStarNotifications::NotificationRule', { Resource: project.projectArn, EventTypeIds: ['codebuild-project-build-state-succeeded'], Targets: [ @@ -206,7 +206,7 @@ describe('NotificationRule', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodeStarNotifications::NotificationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeStarNotifications::NotificationRule', { Resource: pipeline.pipelineArn, EventTypeIds: [ 'codepipeline-pipeline-pipeline-execution-succeeded', diff --git a/packages/@aws-cdk/aws-cognito-identitypool/.eslintrc.js b/packages/@aws-cdk/aws-cognito-identitypool/.eslintrc.js new file mode 100644 index 0000000000000..2658ee8727166 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-cognito-identitypool/.gitignore b/packages/@aws-cdk/aws-cognito-identitypool/.gitignore new file mode 100644 index 0000000000000..d8a8561d50885 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +nyc.config.js +.LAST_PACKAGE +*.snk +!.eslintrc.js +!jest.config.js + +junit.xml \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito-identitypool/.npmignore b/packages/@aws-cdk/aws-cognito-identitypool/.npmignore new file mode 100644 index 0000000000000..aaabf1df59065 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/.npmignore @@ -0,0 +1,28 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml +test/ +!*.lit.ts \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito-identitypool/LICENSE b/packages/@aws-cdk/aws-cognito-identitypool/LICENSE new file mode 100644 index 0000000000000..82ad00bb02d0b --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-cognito-identitypool/NOTICE b/packages/@aws-cdk/aws-cognito-identitypool/NOTICE new file mode 100644 index 0000000000000..1e8442b37b61d --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito-identitypool/README.md b/packages/@aws-cdk/aws-cognito-identitypool/README.md new file mode 100644 index 0000000000000..e1be0d70ec29c --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/README.md @@ -0,0 +1,383 @@ +# Amazon Cognito Identity Pool Construct Library + +> **Identity Pools are in a separate module while the API is being stabilized. Once we stabilize the module, they will** +**be included into the stable [aws-cognito](../aws-cognito) library. Please provide feedback on this experience by** +**creating an [issue here](https://github.com/aws/aws-cdk/issues/new/choose)** + + + +--- + +![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. + +--- + + + +[Amazon Cognito Identity Pools](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-identity.html) enable you to grant your users access to other AWS services. + +Identity Pools are one of the two main components of [Amazon Cognito](https://docs.aws.amazon.com/cognito/latest/developerguide/what-is-amazon-cognito.html), which provides authentication, authorization, and +user management for your web and mobile apps. Your users can sign in directly with a user name and password, or through +a third party such as Facebook, Amazon, Google or Apple. + +The other main component in Amazon Cognito is [user pools](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html). User Pools are user directories that provide sign-up and +sign-in options for your app users. + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +```ts nofixture +import { IdentityPool, UserPoolAuthenticationProvider } from '@aws-cdk/aws-cognito-identitypool'; +``` + +## Table of Contents + +- [Identity Pools](#identity-pools) + - [Authenticated and Unauthenticated Identities](#authenticated-and-unauthenticated-identities) + - [Authentication Providers](#authentication-providers) + - [User Pool Authentication Provider](#user-pool-authentication-provider) + - [Server Side Token Check](#server-side-token-check) + - [Associating an External Provider Directly](#associating-an-external-provider-directly) + - [OpenIdConnect and Saml](#openid-connect-and-saml) + - [Custom Providers](#custom-providers) + - [Role Mapping](#role-mapping) + - [Provider Urls](#provider-urls) + - [Authentication Flow](#authentication-flow) + - [Cognito Sync](#cognito-sync) + - [Importing Identity Pools](#importing-identity-pools) + +## Identity Pools + +Identity pools provide temporary AWS credentials for users who are guests (unauthenticated) and for users who have been +authenticated and received a token. An identity pool is a store of user identity data specific to an account. + +Identity pools can be used in conjunction with Cognito User Pools or by accessing external federated identity providers +directly. Learn more at [Amazon Cognito Identity Pools](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-identity.html). + +### Authenticated and Unauthenticated Identities + +Identity pools define two types of identities: authenticated(`user`) and unauthenticated (`guest`). Every identity in +an identity pool is either authenticated or unauthenticated. Each identity pool has a default role for authenticated +identities, and a default role for unauthenticated identities. Absent other overriding rules (see below), these are the +roles that will be assumed by the corresponding users in the authentication process. + +A basic Identity Pool with minimal configuration has no required props, with default authenticated (user) and +unauthenticated (guest) roles applied to the identity pool: + +```ts +new IdentityPool(this, 'myIdentityPool'); +``` + +By default, both the authenticated and unauthenticated roles will have no permissions attached. Grant permissions +to roles using the public `authenticatedRole` and `unauthenticatedRole` properties: + +```ts +import * as dynamodb from '@aws-cdk/aws-dynamodb'; + +const identityPool = new IdentityPool(this, 'myIdentityPool'); +declare const table: dynamodb.Table; + +// Grant permissions to authenticated users +table.grantReadWriteData(identityPool.authenticatedRole); +// Grant permissions to unauthenticated guest users +table.grantReadData(identityPool.unauthenticatedRole); + +//Or add policy statements straight to the role +identityPool.authenticatedRole.addToPrincipalPolicy(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['dynamodb:*'], + resources: ['*'], +})); +``` + +The default roles can also be supplied in `IdentityPoolProps`: + +```ts +const stack = new Stack(); +const authenticatedRole = new iam.Role(this, 'authRole', { + assumedBy: new iam.ServicePrincipal('service.amazonaws.com'), +}); +const unauthenticatedRole = new iam.Role(this, 'unauthRole', { + assumedBy: new iam.ServicePrincipal('service.amazonaws.com'), +}); +const identityPool = new IdentityPool(this, 'TestIdentityPoolActions', { + authenticatedRole, + unauthenticatedRole, +}); +``` + +### Authentication Providers + +Authenticated identities belong to users who are authenticated by a public login provider (Amazon Cognito user pools, +Login with Amazon, Sign in with Apple, Facebook, Google, SAML, or any OpenID Connect Providers) or a developer provider +(your own backend authentication process). + +[Authentication providers](https://docs.aws.amazon.com/cognito/latest/developerguide/external-identity-providers.html) can be associated with an Identity Pool by first associating them with a Cognito User Pool or by +associating the provider directly with the identity pool. + +#### User Pool Authentication Provider + +In order to attach a user pool to an identity pool as an authentication provider, the identity pool needs properties +from both the user pool and the user pool client. For this reason identity pools use a `UserPoolAuthenticationProvider` +to gather the necessary properties from the user pool constructs. + +```ts +const userPool = new cognito.UserPool(this, 'Pool'); + +new IdentityPool(this, 'myidentitypool', { + identityPoolName: 'myidentitypool', + authenticationProviders: { + userPools: [new UserPoolAuthenticationProvider({ userPool })], + }, +}); +``` + +User pools can also be associated with an identity pool after instantiation. The Identity Pool's `addUserPoolAuthentication` method +returns the User Pool Client that has been created: + +```ts +declare const identityPool: IdentityPool; +const userPool = new cognito.UserPool(this, 'Pool'); +const userPoolClient = identityPool.addUserPoolAuthentication(new UserPoolAuthenticationProvider({ + userPool, +})); +``` + +#### Server Side Token Check + +With the `IdentityPool` CDK Construct, by default the pool is configured to check with the integrated user pools to +make sure that the user has not been globally signed out or deleted before the identity pool provides an OIDC token or +AWS credentials for the user. + +If the user is signed out or deleted, the identity pool will return a 400 Not Authorized error. This setting can be +disabled, however, in several ways. + +Setting `disableServerSideTokenCheck` to true will change the default behavior to no server side token check. Learn +more [here](https://docs.aws.amazon.com/cognitoidentity/latest/APIReference/API_CognitoIdentityProvider.html#CognitoIdentity-Type-CognitoIdentityProvider-ServerSideTokenCheck): + +```ts +declare const identityPool: IdentityPool; +const userPool = new cognito.UserPool(this, 'Pool'); +identityPool.addUserPoolAuthentication(new UserPoolAuthenticationProvider({ + userPool, + disableServerSideTokenCheck: true, +})); +``` + +#### Associating an External Provider Directly + +One or more [external identity providers](https://docs.aws.amazon.com/cognito/latest/developerguide/external-identity-providers.html) can be associated with an identity pool directly using +`authenticationProviders`: + +```ts +new IdentityPool(this, 'myidentitypool', { + identityPoolName: 'myidentitypool', + authenticationProviders: { + amazon: { + appId: 'amzn1.application.12312k3j234j13rjiwuenf', + }, + facebook: { + appId: '1234567890123', + }, + google: { + clientId: '12345678012.apps.googleusercontent.com', + }, + apple: { + servicesId: 'com.myappleapp.auth', + }, + twitter: { + consumerKey: 'my-twitter-id', + consumerSecret: 'my-twitter-secret', + }, + }, +}); +``` + +To associate more than one provider of the same type with the identity pool, use User +Pools, OpenIdConnect, or SAML. Only one provider per external service can be attached directly to the identity pool. + +#### OpenId Connect and Saml + +[OpenID Connect](https://docs.aws.amazon.com/cognito/latest/developerguide/open-id.html) is an open standard for +authentication that is supported by a number of login providers. Amazon Cognito supports linking of identities with +OpenID Connect providers that are configured through [AWS Identity and Access Management](http://aws.amazon.com/iam/). + +An identity provider that supports [Security Assertion Markup Language 2.0 (SAML 2.0)](https://docs.aws.amazon.com/cognito/latest/developerguide/saml-identity-provider.html) can be used to provide a simple +onboarding flow for users. The SAML-supporting identity provider specifies the IAM roles that can be assumed by users +so that different users can be granted different sets of permissions. Associating an OpenId Connect or Saml provider +with an identity pool: + +```ts +declare const openIdConnectProvider: iam.OpenIdConnectProvider; +declare const samlProvider: iam.SamlProvider; + +new IdentityPool(this, 'myidentitypool', { + identityPoolName: 'myidentitypool', + authenticationProviders: { + openIdConnectProviders: [openIdConnectProvider], + samlProviders: [samlProvider], + }, +}); +``` + +#### Custom Providers + +The identity pool's behavior can be customized further using custom [developer authenticated identities](https://docs.aws.amazon.com/cognito/latest/developerguide/developer-authenticated-identities.html). +With developer authenticated identities, users can be registered and authenticated via an existing authentication +process while still using Amazon Cognito to synchronize user data and access AWS resources. + +Like the supported external providers, though, only one custom provider can be directly associated with the identity +pool. + +```ts +declare const openIdConnectProvider: iam.OpenIdConnectProvider; +new IdentityPool(this, 'myidentitypool', { + identityPoolName: 'myidentitypool', + authenticationProviders: { + google: { + clientId: '12345678012.apps.googleusercontent.com', + }, + openIdConnectProviders: [openIdConnectProvider], + customProvider: 'my-custom-provider.example.com', + }, +}); +``` + +### Role Mapping + +In addition to setting default roles for authenticated and unauthenticated users, identity pools can also be used to +define rules to choose the role for each user based on claims in the user's ID token by using Role Mapping. When using +role mapping, it's important to be aware of some of the permissions the role will need. An in depth +review of roles and role mapping can be found [here](https://docs.aws.amazon.com/cognito/latest/developerguide/role-based-access-control.html). + +Using a [token-based approach](https://docs.aws.amazon.com/cognito/latest/developerguide/role-based-access-control.html#using-tokens-to-assign-roles-to-users) to role mapping will allow mapped roles to be passed through the `cognito:roles` or +`cognito:preferred_role` claims from the identity provider: + +```ts +import { IdentityPoolProviderUrl } from '@aws-cdk/aws-cognito-identitypool'; + +new IdentityPool(this, 'myidentitypool', { + identityPoolName: 'myidentitypool', + roleMappings: [{ + providerUrl: IdentityPoolProviderUrl.AMAZON, + useToken: true, + }], +}); +``` + +Using a rule-based approach to role mapping allows roles to be assigned based on custom claims passed from the identity provider: + +```ts +import { IdentityPoolProviderUrl, RoleMappingMatchType } from '@aws-cdk/aws-cognito-identitypool'; + +declare const adminRole: iam.Role; +declare const nonAdminRole: iam.Role; +new IdentityPool(this, 'myidentitypool', { + identityPoolName: 'myidentitypool', + // Assign specific roles to users based on whether or not the custom admin claim is passed from the identity provider + roleMappings: [{ + providerUrl: IdentityPoolProviderUrl.AMAZON, + rules: [ + { + claim: 'custom:admin', + claimValue: 'admin', + mappedRole: adminRole, + }, + { + claim: 'custom:admin', + claimValue: 'admin', + matchType: RoleMappingMatchType.NOTEQUAL, + mappedRole: nonAdminRole, + } + ], + }], +}); +``` + +Role mappings can also be added after instantiation with the Identity Pool's `addRoleMappings` method: + +```ts +import { IdentityPoolRoleMapping } from '@aws-cdk/aws-cognito-identitypool'; + +declare const identityPool: IdentityPool; +declare const myAddedRoleMapping1: IdentityPoolRoleMapping; +declare const myAddedRoleMapping2: IdentityPoolRoleMapping; +declare const myAddedRoleMapping3: IdentityPoolRoleMapping; + +identityPool.addRoleMappings(myAddedRoleMapping1, myAddedRoleMapping2, myAddedRoleMapping3); +``` + +#### Provider Urls + +Role mappings must be associated with the url of an Identity Provider which can be supplied +`IdentityPoolProviderUrl`. Supported Providers have static Urls that can be used: + +```ts +import { IdentityPoolProviderUrl } from '@aws-cdk/aws-cognito-identitypool'; + +new IdentityPool(this, 'myidentitypool', { + identityPoolName: 'myidentitypool', + roleMappings: [{ + providerUrl: IdentityPoolProviderUrl.FACEBOOK, + useToken: true, + }], +}); +``` + +For identity providers that don't have static Urls, a custom Url or User Pool Client Url can be supplied: + +```ts +import { IdentityPoolProviderUrl } from '@aws-cdk/aws-cognito-identitypool'; + +new IdentityPool(this, 'myidentitypool', { + identityPoolName: 'myidentitypool', + roleMappings: [ + { + providerUrl: IdentityPoolProviderUrl.userPool('cognito-idp.my-idp-region.amazonaws.com/my-idp-region_abcdefghi:app_client_id'), + useToken: true, + }, + { + providerUrl: IdentityPoolProviderUrl.custom('my-custom-provider.com'), + useToken: true, + }, + ], +}); +``` + +See [here](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-identitypoolroleattachment-rolemapping.html#cfn-cognito-identitypoolroleattachment-rolemapping-identityprovider) for more information. + +### Authentication Flow + +Identity Pool [Authentication Flow](https://docs.aws.amazon.com/cognito/latest/developerguide/authentication-flow.html) defaults to the enhanced, simplified flow. The Classic (basic) Authentication Flow +can also be implemented using `allowClassicFlow`: + +```ts +new IdentityPool(this, 'myidentitypool', { + identityPoolName: 'myidentitypool', + allowClassicFlow: true, +}); +``` + +### Cognito Sync + +It's now recommended to integrate [AWS AppSync](https://aws.amazon.com/appsync/) for synchronizing app data across devices, so +Cognito Sync features like `PushSync`, `CognitoEvents`, and `CognitoStreams` are not a part of `IdentityPool`. More +information can be found [here](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-sync.html). + +### Importing Identity Pools + +You can import existing identity pools into your stack using Identity Pool static methods with the Identity Pool Id or +Arn: + +```ts +IdentityPool.fromIdentityPoolId(this, 'my-imported-identity-pool', + 'us-east-1:dj2823ryiwuhef937'); +IdentityPool.fromIdentityPoolArn(this, 'my-imported-identity-pool', + 'arn:aws:cognito-identity:us-east-1:123456789012:identitypool/us-east-1:dj2823ryiwuhef937'); +``` + diff --git a/packages/@aws-cdk/aws-cognito-identitypool/jest.config.js b/packages/@aws-cdk/aws-cognito-identitypool/jest.config.js new file mode 100644 index 0000000000000..3a2fd93a1228a --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool-role-attachment.ts b/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool-role-attachment.ts new file mode 100644 index 0000000000000..b3811d99de5f3 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool-role-attachment.ts @@ -0,0 +1,203 @@ +import { + CfnIdentityPoolRoleAttachment, +} from '@aws-cdk/aws-cognito'; +import { + IRole, +} from '@aws-cdk/aws-iam'; +import { + Resource, + IResource, +} from '@aws-cdk/core'; +import { + Construct, +} from 'constructs'; +import { + IIdentityPool, + IdentityPoolProviderUrl, +} from './identitypool'; + +/** + * Represents an Identity Pool Role Attachment + */ +export interface IIdentityPoolRoleAttachment extends IResource { + /** + * Id of the Attachments Underlying Identity Pool + */ + readonly identityPoolId: string; +} + +/** + * Props for an Identity Pool Role Attachment + */ +export interface IdentityPoolRoleAttachmentProps { + + /** + * Id of the Attachments Underlying Identity Pool + */ + readonly identityPool: IIdentityPool; + + /** + * Default Authenticated (User) Role + * @default - No default authenticated role will be added + */ + readonly authenticatedRole?: IRole; + + /** + * Default Unauthenticated (Guest) Role + * @default - No default unauthenticated role will be added + */ + readonly unauthenticatedRole?: IRole; + + /** + * Rules for mapping roles to users + * @default - no Role Mappings + */ + readonly roleMappings?: IdentityPoolRoleMapping[]; +} + +/** + * Map roles to users in the identity pool based on claims from the Identity Provider + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-identitypoolroleattachment.html + */ +export interface IdentityPoolRoleMapping { + /** + * The url of the provider of for which the role is mapped + */ + readonly providerUrl: IdentityPoolProviderUrl; + + /** + * If true then mapped roles must be passed through the cognito:roles or cognito:preferred_role claims from identity provider. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/role-based-access-control.html#using-tokens-to-assign-roles-to-users + * + * @default false + */ + readonly useToken?: boolean; + + /** + * Allow for role assumption when results of role mapping are ambiguous + * @default false - Ambiguous role resolutions will lead to requester being denied + */ + readonly resolveAmbiguousRoles?: boolean; + + /** + * The claim and value that must be matched in order to assume the role. Required if useToken is false + * @default - No Rule Mapping Rule + */ + readonly rules?: RoleMappingRule[]; +} + +/** + * Types of matches allowed for Role Mapping + */ +export enum RoleMappingMatchType { + /** + * The Claim from the token must equal the given value in order for a match + */ + EQUALS = 'Equals', + + /** + * The Claim from the token must contain the given value in order for a match + */ + CONTAINS = 'Contains', + + /** + * The Claim from the token must start with the given value in order for a match + */ + STARTS_WITH = 'StartsWith', + + /** + * The Claim from the token must not equal the given value in order for a match + */ + NOTEQUAL = 'NotEqual', +} + +/** + * Represents an Identity Pool Role Attachment Role Mapping Rule + */ +export interface RoleMappingRule { + /** + * The key sent in the token by the federated identity provider. + */ + readonly claim: string; + + /** + * The Role to be assumed when Claim Value is matched. + */ + readonly mappedRole: IRole; + + /** + * The value of the claim that must be matched + */ + readonly claimValue: string; + + /** + * How to match with the Claim value + * @default RoleMappingMatchType.EQUALS + */ + readonly matchType?: RoleMappingMatchType +} + +/** + * Defines an Identity Pool Role Attachment + * + * @resource AWS::Cognito::IdentityPoolRoleAttachment + */ +export class IdentityPoolRoleAttachment extends Resource implements IIdentityPoolRoleAttachment { + /** + * Id of the underlying identity pool + */ + public readonly identityPoolId: string + + constructor(scope: Construct, id: string, props: IdentityPoolRoleAttachmentProps) { + super(scope, id); + this.identityPoolId = props.identityPool.identityPoolId; + const mappings = props.roleMappings || []; + let roles: any = undefined, roleMappings: any = undefined; + if (props.authenticatedRole || props.unauthenticatedRole) { + roles = {}; + if (props.authenticatedRole) roles.authenticated = props.authenticatedRole.roleArn; + if (props.unauthenticatedRole) roles.unauthenticated = props.unauthenticatedRole.roleArn; + } + if (mappings) { + roleMappings = this.configureRoleMappings(...mappings); + } + new CfnIdentityPoolRoleAttachment(this, 'Resource', { + identityPoolId: this.identityPoolId, + roles, + roleMappings, + }); + } + + /** + * Configures Role Mappings for Identity Pool Role Attachment + */ + private configureRoleMappings( + ...props: IdentityPoolRoleMapping[] + ): { [name:string]: CfnIdentityPoolRoleAttachment.RoleMappingProperty } | undefined { + if (!props || !props.length) return undefined; + return props.reduce((acc, prop) => { + let roleMapping: any = { + ambiguousRoleResolution: prop.resolveAmbiguousRoles ? 'AuthenticatedRole' : 'Deny', + type: prop.useToken ? 'Token' : 'Rules', + identityProvider: prop.providerUrl.value, + }; + if (roleMapping.type === 'Rules') { + if (!prop.rules) { + throw new Error('IdentityPoolRoleMapping.rules is required when useToken is false'); + } + roleMapping.rulesConfiguration = { + rules: prop.rules.map(rule => { + return { + claim: rule.claim, + value: rule.claimValue, + matchType: rule.matchType || RoleMappingMatchType.EQUALS, + roleArn: rule.mappedRole.roleArn, + }; + }), + }; + }; + acc[prop.providerUrl.value] = roleMapping; + return acc; + }, {} as { [name:string]: CfnIdentityPoolRoleAttachment.RoleMappingProperty }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool-user-pool-authentication-provider.ts b/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool-user-pool-authentication-provider.ts new file mode 100644 index 0000000000000..831d223250258 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool-user-pool-authentication-provider.ts @@ -0,0 +1,118 @@ +import { + IUserPool, + IUserPoolClient, +} from '@aws-cdk/aws-cognito'; +import { Stack } from '@aws-cdk/core'; +import { + Construct, Node, +} from 'constructs'; +import { IIdentityPool } from './identitypool'; + +/** + * Represents the concept of a User Pool Authentication Provider. + * You use user pool authentication providers to configure User Pools + * and User Pool Clients for use with Identity Pools + */ +export interface IUserPoolAuthenticationProvider { + /** + * The method called when a given User Pool Authentication Provider is added + * (for the first time) to an Identity Pool. + */ + bind( + scope: Construct, + identityPool: IIdentityPool, + options?: UserPoolAuthenticationProviderBindOptions + ): UserPoolAuthenticationProviderBindConfig; +} + +/** + * Props for the User Pool Authentication Provider + */ +export interface UserPoolAuthenticationProviderProps { + /** + * The User Pool of the Associated Identity Providers + */ + readonly userPool: IUserPool; + + /** + * The User Pool Client for the provided User Pool + * @default - A default user pool client will be added to User Pool + */ + readonly userPoolClient?: IUserPoolClient; + + /** + * Setting this to true turns off identity pool checks for this user pool to make sure the user has not been globally signed out or deleted before the identity pool provides an OIDC token or AWS credentials for the user + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-identitypool-cognitoidentityprovider.html + * @default false + */ + readonly disableServerSideTokenCheck?: boolean; + +} + +/** + * Represents UserPoolAuthenticationProvider Bind Options + */ +export interface UserPoolAuthenticationProviderBindOptions {} + +/** + * Represents a UserPoolAuthenticationProvider Bind Configuration + */ +export interface UserPoolAuthenticationProviderBindConfig { + /** + * Client Id of the Associated User Pool Client + */ + readonly clientId: string + + /** + * The identity providers associated with the UserPool + */ + readonly providerName: string; + + /** + * Whether to enable the identity pool's server side token check + */ + readonly serverSideTokenCheck: boolean; +} + +/** + * Defines a User Pool Authentication Provider + */ +export class UserPoolAuthenticationProvider implements IUserPoolAuthenticationProvider { + + /** + * The User Pool of the Associated Identity Providers + */ + private userPool: IUserPool; + + /** + * The User Pool Client for the provided User Pool + */ + private userPoolClient: IUserPoolClient; + + /** + * Whether to disable the pool's default server side token check + */ + private disableServerSideTokenCheck: boolean + constructor(props: UserPoolAuthenticationProviderProps) { + this.userPool = props.userPool; + this.userPoolClient = props.userPoolClient || this.userPool.addClient('UserPoolAuthenticationProviderClient'); + this.disableServerSideTokenCheck = props.disableServerSideTokenCheck ?? false; + } + + public bind( + scope: Construct, + identityPool: IIdentityPool, + _options?: UserPoolAuthenticationProviderBindOptions, + ): UserPoolAuthenticationProviderBindConfig { + Node.of(identityPool).addDependency(this.userPool); + Node.of(identityPool).addDependency(this.userPoolClient); + const region = Stack.of(scope).region; + const urlSuffix = Stack.of(scope).urlSuffix; + + return { + clientId: this.userPoolClient.userPoolClientId, + providerName: `cognito-idp.${region}.${urlSuffix}/${this.userPool.userPoolId}`, + serverSideTokenCheck: !this.disableServerSideTokenCheck, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool.ts b/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool.ts new file mode 100644 index 0000000000000..1fd89f238e0b0 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool.ts @@ -0,0 +1,500 @@ +import { + CfnIdentityPool, +} from '@aws-cdk/aws-cognito'; +import { + IOpenIdConnectProvider, + ISamlProvider, + Role, + FederatedPrincipal, + IRole, +} from '@aws-cdk/aws-iam'; +import { + Resource, + IResource, + Stack, + ArnFormat, + Lazy, +} from '@aws-cdk/core'; +import { + Construct, +} from 'constructs'; +import { + IdentityPoolRoleAttachment, + IdentityPoolRoleMapping, +} from './identitypool-role-attachment'; +import { + IUserPoolAuthenticationProvider, +} from './identitypool-user-pool-authentication-provider'; + +/** + * Represents a Cognito IdentityPool + */ +export interface IIdentityPool extends IResource { + /** + * The id of the Identity Pool in the format REGION:GUID + * @attribute + */ + readonly identityPoolId: string; + + /** + * The ARN of the Identity Pool + * @attribute + */ + readonly identityPoolArn: string; + + /** + * Name of the Identity Pool + * @attribute + */ + readonly identityPoolName: string +} + +/** + * Props for the IdentityPool construct + */ +export interface IdentityPoolProps { + + /** + * The name of the Identity Pool + * @default - automatically generated name by CloudFormation at deploy time + */ + readonly identityPoolName?: string; + + /** + * The Default Role to be assumed by Authenticated Users + * @default - A Default Authenticated Role will be added + */ + readonly authenticatedRole?: IRole; + + /** + * The Default Role to be assumed by Unauthenticated Users + * @default - A Default Unauthenticated Role will be added + */ + readonly unauthenticatedRole?: IRole; + + /** + * Wwhether the identity pool supports unauthenticated logins + * @default - false + */ + readonly allowUnauthenticatedIdentities?: boolean + + /** + * Rules for mapping roles to users + * @default - no Role Mappings + */ + readonly roleMappings?: IdentityPoolRoleMapping[]; + + /** + * Enables the Basic (Classic) authentication flow + * @default - Classic Flow not allowed + */ + readonly allowClassicFlow?: boolean; + + /** + * Authentication providers for using in identity pool. + * @default - No Authentication Providers passed directly to Identity Pool + */ + readonly authenticationProviders?: IdentityPoolAuthenticationProviders +} + +/** + * Types of Identity Pool Login Providers + */ +export enum IdentityPoolProviderType { + /** Facebook Provider type */ + FACEBOOK = 'Facebook', + /** Google Provider Type */ + GOOGLE = 'Google', + /** Amazon Provider Type */ + AMAZON = 'Amazon', + /** Apple Provider Type */ + APPLE = 'Apple', + /** Twitter Provider Type */ + TWITTER = 'Twitter', + /** Digits Provider Type */ + DIGITS = 'Digits', + /** Open Id Provider Type */ + OPEN_ID = 'OpenId', + /** Saml Provider Type */ + SAML = 'Saml', + /** User Pool Provider Type */ + USER_POOL = 'UserPool', + /** Custom Provider Type */ + CUSTOM = 'Custom', +} + +/** + * Keys for Login Providers - correspond to client id's of respective federation identity providers + */ +export class IdentityPoolProviderUrl { + /** Facebook Provider Url */ + public static readonly FACEBOOK = new IdentityPoolProviderUrl(IdentityPoolProviderType.FACEBOOK, 'graph.facebook.com'); + + /** Google Provider Url */ + public static readonly GOOGLE = new IdentityPoolProviderUrl(IdentityPoolProviderType.GOOGLE, 'accounts.google.com'); + + /** Amazon Provider Url */ + public static readonly AMAZON = new IdentityPoolProviderUrl(IdentityPoolProviderType.AMAZON, 'www.amazon.com'); + + /** Apple Provider Url */ + public static readonly APPLE = new IdentityPoolProviderUrl(IdentityPoolProviderType.APPLE, 'appleid.apple.com'); + + /** Twitter Provider Url */ + public static readonly TWITTER = new IdentityPoolProviderUrl(IdentityPoolProviderType.TWITTER, 'api.twitter.com'); + + /** Digits Provider Url */ + public static readonly DIGITS = new IdentityPoolProviderUrl(IdentityPoolProviderType.DIGITS, 'www.digits.com'); + + /** OpenId Provider Url */ + public static openId(url: string): IdentityPoolProviderUrl { + return new IdentityPoolProviderUrl(IdentityPoolProviderType.OPEN_ID, url); + } + + /** Saml Provider Url */ + public static saml(url: string): IdentityPoolProviderUrl { + return new IdentityPoolProviderUrl(IdentityPoolProviderType.SAML, url); + } + + /** User Pool Provider Url */ + public static userPool(url: string): IdentityPoolProviderUrl { + return new IdentityPoolProviderUrl(IdentityPoolProviderType.USER_POOL, url); + } + + /** Custom Provider Url */ + public static custom(url: string): IdentityPoolProviderUrl { + return new IdentityPoolProviderUrl(IdentityPoolProviderType.CUSTOM, url); + } + + constructor( + /** type of Provider Url */ + public readonly type: IdentityPoolProviderType, + /** value of Provider Url */ + public readonly value: string, + ) {} +} + +/** + * Login Provider for Identity Federation using Amazon Credentials + */ +export interface IdentityPoolAmazonLoginProvider { + /** + * App Id for Amazon Identity Federation + */ + readonly appId: string +} + +/** + * Login Provider for Identity Federation using Facebook Credentials + */ +export interface IdentityPoolFacebookLoginProvider { + /** + * App Id for Facebook Identity Federation + */ + readonly appId: string +} + +/** + * Login Provider for Identity Federation using Apple Credentials +*/ +export interface IdentityPoolAppleLoginProvider { + /** + * App Id for Apple Identity Federation + */ + readonly servicesId: string +} + +/** + * Login Provider for Identity Federation using Google Credentials + */ +export interface IdentityPoolGoogleLoginProvider { + /** + * App Id for Google Identity Federation + */ + readonly clientId: string +} + +/** + * Login Provider for Identity Federation using Twitter Credentials + */ +export interface IdentityPoolTwitterLoginProvider { + /** + * App Id for Twitter Identity Federation + */ + readonly consumerKey: string + + /** + * App Secret for Twitter Identity Federation + */ + readonly consumerSecret: string +} + +/** + * Login Provider for Identity Federation using Digits Credentials + */ +export interface IdentityPoolDigitsLoginProvider extends IdentityPoolTwitterLoginProvider {} + +/** + * External Identity Providers To Connect to User Pools and Identity Pools + */ +export interface IdentityPoolProviders { + + /** App Id for Facebook Identity Federation + * @default - No Facebook Authentication Provider used without OpenIdConnect or a User Pool + */ + readonly facebook?: IdentityPoolFacebookLoginProvider + + /** Client Id for Google Identity Federation + * @default - No Google Authentication Provider used without OpenIdConnect or a User Pool + */ + readonly google?: IdentityPoolGoogleLoginProvider + + /** App Id for Amazon Identity Federation + * @default - No Amazon Authentication Provider used without OpenIdConnect or a User Pool + */ + readonly amazon?: IdentityPoolAmazonLoginProvider + + /** Services Id for Apple Identity Federation + * @default - No Apple Authentication Provider used without OpenIdConnect or a User Pool + */ + readonly apple?: IdentityPoolAppleLoginProvider + + /** Consumer Key and Secret for Twitter Identity Federation + * @default - No Twitter Authentication Provider used without OpenIdConnect or a User Pool + */ + readonly twitter?: IdentityPoolTwitterLoginProvider + + /** Consumer Key and Secret for Digits Identity Federation + * @default - No Digits Authentication Provider used without OpenIdConnect or a User Pool + */ + readonly digits?: IdentityPoolDigitsLoginProvider +} + +/** +* Authentication providers for using in identity pool. +* @see https://docs.aws.amazon.com/cognito/latest/developerguide/external-identity-providers.html +*/ +export interface IdentityPoolAuthenticationProviders extends IdentityPoolProviders { + + /** + * The User Pool Authentication Providers associated with this Identity Pool + * @default - no User Pools Associated + */ + readonly userPools?: IUserPoolAuthenticationProvider[]; + + /** + * The OpenIdConnect Provider associated with this Identity Pool + * @default - no OpenIdConnectProvider + */ + readonly openIdConnectProviders?: IOpenIdConnectProvider[]; + + /** + * The Security Assertion Markup Language Provider associated with this Identity Pool + * @default - no SamlProvider + */ + readonly samlProviders?: ISamlProvider[]; + + /** + * The Developer Provider Name to associate with this Identity Pool + * @default - no Custom Provider + */ + readonly customProvider?: string; +} + +/** + * Define a Cognito Identity Pool + * + * @resource AWS::Cognito::IdentityPool + */ +export class IdentityPool extends Resource implements IIdentityPool { + + /** + * Import an existing Identity Pool from its id + */ + public static fromIdentityPoolId(scope: Construct, id: string, identityPoolId: string): IIdentityPool { + const identityPoolArn = Stack.of(scope).formatArn({ + service: 'cognito-identity', + resource: 'identitypool', + resourceName: identityPoolId, + arnFormat: ArnFormat.SLASH_RESOURCE_NAME, + }); + + return IdentityPool.fromIdentityPoolArn(scope, id, identityPoolArn); + } + + /** + * Import an existing Identity Pool from its Arn + */ + public static fromIdentityPoolArn(scope: Construct, id: string, identityPoolArn: string): IIdentityPool { + const pool = Stack.of(scope).splitArn(identityPoolArn, ArnFormat.SLASH_RESOURCE_NAME); + const res = pool.resourceName || ''; + if (!res) { + throw new Error('Invalid Identity Pool ARN'); + } + const idParts = res.split(':'); + if (!(idParts.length === 2)) throw new Error('Invalid Identity Pool Id: Identity Pool Ids must follow the format :'); + if (idParts[0] !== pool.region) throw new Error('Invalid Identity Pool Id: Region in Identity Pool Id must match stack region'); + class ImportedIdentityPool extends Resource implements IIdentityPool { + public readonly identityPoolId = res; + public readonly identityPoolArn = identityPoolArn; + public readonly identityPoolName: string + constructor() { + super(scope, id, { + account: pool.account, + region: pool.region, + }); + this.identityPoolName = this.physicalName; + } + } + return new ImportedIdentityPool(); + } + + /** + * The id of the Identity Pool in the format REGION:GUID + * @attribute + */ + public readonly identityPoolId: string; + + /** + * The ARN of the Identity Pool + * @attribute + */ + public readonly identityPoolArn: string; + + /** + * The name of the Identity Pool + * @attribute + */ + public readonly identityPoolName: string; + + /** + * Default role for authenticated users + */ + public readonly authenticatedRole: IRole; + + /** + * Default role for unauthenticated users + */ + public readonly unauthenticatedRole: IRole; + + /** + * List of Identity Providers added in constructor for use with property overrides + */ + private cognitoIdentityProviders: CfnIdentityPool.CognitoIdentityProviderProperty[] = []; + + /** + * Running count of added role attachments + */ + private roleAttachmentCount: number = 0; + + constructor(scope: Construct, id: string, props:IdentityPoolProps = {}) { + super(scope, id, { + physicalName: props.identityPoolName, + }); + const authProviders: IdentityPoolAuthenticationProviders = props.authenticationProviders || {}; + const providers = authProviders.userPools ? authProviders.userPools.map(userPool => userPool.bind(this, this)) : undefined; + if (providers && providers.length) this.cognitoIdentityProviders = providers; + const openIdConnectProviderArns = authProviders.openIdConnectProviders ? + authProviders.openIdConnectProviders.map(openIdProvider => + openIdProvider.openIdConnectProviderArn, + ) : undefined; + const samlProviderArns = authProviders.samlProviders ? + authProviders.samlProviders.map(samlProvider => + samlProvider.samlProviderArn, + ) : undefined; + + let supportedLoginProviders:any = {}; + if (authProviders.amazon) supportedLoginProviders[IdentityPoolProviderUrl.AMAZON.value] = authProviders.amazon.appId; + if (authProviders.facebook) supportedLoginProviders[IdentityPoolProviderUrl.FACEBOOK.value] = authProviders.facebook.appId; + if (authProviders.google) supportedLoginProviders[IdentityPoolProviderUrl.GOOGLE.value] = authProviders.google.clientId; + if (authProviders.apple) supportedLoginProviders[IdentityPoolProviderUrl.APPLE.value] = authProviders.apple.servicesId; + if (authProviders.twitter) supportedLoginProviders[IdentityPoolProviderUrl.TWITTER.value] = `${authProviders.twitter.consumerKey};${authProviders.twitter.consumerSecret}`; + if (authProviders.digits) supportedLoginProviders[IdentityPoolProviderUrl.DIGITS.value] = `${authProviders.digits.consumerKey};${authProviders.digits.consumerSecret}`; + if (!Object.keys(supportedLoginProviders).length) supportedLoginProviders = undefined; + + const cfnIdentityPool = new CfnIdentityPool(this, 'Resource', { + allowUnauthenticatedIdentities: props.allowUnauthenticatedIdentities ? true : false, + allowClassicFlow: props.allowClassicFlow, + identityPoolName: this.physicalName, + developerProviderName: authProviders.customProvider, + openIdConnectProviderArns, + samlProviderArns, + supportedLoginProviders, + cognitoIdentityProviders: Lazy.any({ produce: () => this.cognitoIdentityProviders }), + }); + this.identityPoolName = cfnIdentityPool.attrName; + this.identityPoolId = cfnIdentityPool.ref; + this.identityPoolArn = Stack.of(scope).formatArn({ + service: 'cognito-identity', + resource: 'identitypool', + resourceName: this.identityPoolId, + arnFormat: ArnFormat.SLASH_RESOURCE_NAME, + }); + this.authenticatedRole = props.authenticatedRole ? props.authenticatedRole : this.configureDefaultRole('Authenticated'); + this.unauthenticatedRole = props.unauthenticatedRole ? props.unauthenticatedRole : this.configureDefaultRole('Unauthenticated'); + const attachment = new IdentityPoolRoleAttachment(this, 'DefaultRoleAttachment', { + identityPool: this, + authenticatedRole: this.authenticatedRole, + unauthenticatedRole: this.unauthenticatedRole, + roleMappings: props.roleMappings, + }); + + // This added by the original author, but it's causing cyclic dependencies. + // Don't know why this was added in the first place, but I'm disabling it for now and if + // no complaints come from this, we're probably safe to remove it altogether. + // attachment.node.addDependency(this); + Array.isArray(attachment); + } + + /** + * Add a User Pool to the IdentityPool and configure User Pool Client to handle identities + */ + public addUserPoolAuthentication(userPool: IUserPoolAuthenticationProvider): void { + const providers = userPool.bind(this, this); + this.cognitoIdentityProviders = this.cognitoIdentityProviders.concat(providers); + } + + /** + * Adds Role Mappings to Identity Pool + */ + public addRoleMappings(...roleMappings: IdentityPoolRoleMapping[]): void { + if (!roleMappings || !roleMappings.length) return; + this.roleAttachmentCount++; + const name = 'RoleMappingAttachment' + this.roleAttachmentCount.toString(); + const attachment = new IdentityPoolRoleAttachment(this, name, { + identityPool: this, + authenticatedRole: this.authenticatedRole, + unauthenticatedRole: this.unauthenticatedRole, + roleMappings, + }); + + // This added by the original author, but it's causing cyclic dependencies. + // Don't know why this was added in the first place, but I'm disabling it for now and if + // no complaints come from this, we're probably safe to remove it altogether. + // attachment.node.addDependency(this); + Array.isArray(attachment); + } + + /** + * Configure Default Roles For Identity Pool + */ + private configureDefaultRole(type: string): IRole { + const assumedBy = this.configureDefaultGrantPrincipal(type.toLowerCase()); + const role = new Role(this, `${type}Role`, { + description: `Default ${type} Role for Identity Pool ${this.identityPoolName}`, + assumedBy, + }); + + return role; + } + + private configureDefaultGrantPrincipal(type: string) { + return new FederatedPrincipal('cognito-identity.amazonaws.com', { + 'StringEquals': { + 'cognito-identity.amazonaws.com:aud': this.identityPoolId, + }, + 'ForAnyValue:StringLike': { + 'cognito-identity.amazonaws.com:amr': type, + }, + }, 'sts:AssumeRoleWithWebIdentity'); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito-identitypool/lib/index.ts b/packages/@aws-cdk/aws-cognito-identitypool/lib/index.ts new file mode 100644 index 0000000000000..f24f1f580225b --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/lib/index.ts @@ -0,0 +1,3 @@ +export * from './identitypool'; +export * from './identitypool-role-attachment'; +export * from './identitypool-user-pool-authentication-provider'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito-identitypool/package.json b/packages/@aws-cdk/aws-cognito-identitypool/package.json new file mode 100644 index 0000000000000..19171391ad7be --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/package.json @@ -0,0 +1,117 @@ +{ + "name": "@aws-cdk/aws-cognito-identitypool", + "version": "0.0.0", + "description": "The CDK Construct Library for AWS::Cognito Identity Pools", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awscdk.services.cognito.identitypool", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "cognito-identitypool" + } + }, + "dotnet": { + "namespace": "Amazon.CDK.AWS.Cognito.IdentityPool", + "packageId": "Amazon.CDK.AWS.Cognito.IdentityPool", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-cdk.aws-cognito-identitypool", + "module": "aws_cdk.aws_cognito_identitypool", + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 1" + ] + } + }, + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-cognito-identitypool" + }, + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "build+test+package": "yarn build+test && yarn package", + "build+test": "yarn build && yarn test", + "compat": "cdk-compat", + "rosetta:extract": "yarn --silent jsii-rosetta extract", + "build+extract": "yarn build && yarn rosetta:extract", + "build+test+extract": "yarn build+test && yarn rosetta:extract" + }, + "cdk-build": { + "env": { + "AWSLINT_BASE_CONSTRUCT": true + } + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "cognito", + "identitypool" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assertions": "0.0.0", + "@aws-cdk/cdk-build-tools": "0.0.0", + "@aws-cdk/cdk-integ-tools": "0.0.0", + "@aws-cdk/pkglint": "0.0.0", + "@types/jest": "^27.4.1", + "jest": "^27.5.1" + }, + "dependencies": { + "@aws-cdk/aws-cognito": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.3.69" + }, + "homepage": "https://github.com/aws/aws-cdk", + "peerDependencies": { + "@aws-cdk/aws-cognito": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.3.69" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "awslint": { + "exclude": [ + "no-unused-type:@aws-cdk/aws-cognito.IdentityPoolProviderType", + "props-physical-name:@aws-cdk/aws-cognito-identitypool.IdentityPoolRoleAttachmentProps" + ] + }, + "stability": "experimental", + "maturity": "experimental", + "awscdkio": { + "announce": false + }, + "publishConfig": { + "tag": "latest" + } +} diff --git a/packages/@aws-cdk/aws-cognito-identitypool/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-cognito-identitypool/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..a362c0eca88f3 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/rosetta/default.ts-fixture @@ -0,0 +1,13 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as cognito from '@aws-cdk/aws-cognito'; +import * as iam from '@aws-cdk/aws-iam'; +import { IdentityPool, UserPoolAuthenticationProvider } from '@aws-cdk/aws-cognito-identitypool'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito-identitypool/test/identitypool.test.ts b/packages/@aws-cdk/aws-cognito-identitypool/test/identitypool.test.ts new file mode 100644 index 0000000000000..903c3915a0e11 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/test/identitypool.test.ts @@ -0,0 +1,611 @@ +import { + Template, +} from '@aws-cdk/assertions'; +import { + UserPool, + UserPoolIdentityProvider, +} from '@aws-cdk/aws-cognito'; +import { + Role, + ServicePrincipal, + ArnPrincipal, + AnyPrincipal, + OpenIdConnectProvider, + SamlProvider, + SamlMetadataDocument, + PolicyStatement, + Effect, + PolicyDocument, +} from '@aws-cdk/aws-iam'; +import { + Stack, +} from '@aws-cdk/core'; +import { + IdentityPool, + IdentityPoolProviderUrl, +} from '../lib/identitypool'; +import { + RoleMappingMatchType, +} from '../lib/identitypool-role-attachment'; +import { UserPoolAuthenticationProvider } from '../lib/identitypool-user-pool-authentication-provider'; + +describe('identity pool', () => { + test('minimal setup', () => { + const stack = new Stack(); + new IdentityPool(stack, 'TestIdentityPoolMinimal'); + const temp = Template.fromStack(stack); + + temp.hasResourceProperties('AWS::Cognito::IdentityPool', { + AllowUnauthenticatedIdentities: false, + }); + + temp.resourceCountIs('AWS::IAM::Role', 2); + temp.resourceCountIs('AWS::IAM::Policy', 0); + temp.hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRoleWithWebIdentity', + Condition: { + 'StringEquals': { + 'cognito-identity.amazonaws.com:aud': { + Ref: 'TestIdentityPoolMinimal44837852', + }, + }, + 'ForAnyValue:StringLike': { + 'cognito-identity.amazonaws.com:amr': 'authenticated', + }, + }, + Effect: 'Allow', + Principal: { + Federated: 'cognito-identity.amazonaws.com', + }, + }, + ], + }, + }); + + temp.hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRoleWithWebIdentity', + Condition: { + 'StringEquals': { + 'cognito-identity.amazonaws.com:aud': { + Ref: 'TestIdentityPoolMinimal44837852', + }, + }, + 'ForAnyValue:StringLike': { + 'cognito-identity.amazonaws.com:amr': 'unauthenticated', + }, + }, + Effect: 'Allow', + Principal: { + Federated: 'cognito-identity.amazonaws.com', + }, + }, + ], + }, + }); + }); + + test('providing default roles directly', () => { + const stack = new Stack(); + const authenticatedRole = new Role(stack, 'authRole', { + assumedBy: new ServicePrincipal('service.amazonaws.com'), + }); + const unauthenticatedRole = new Role(stack, 'unauthRole', { + assumedBy: new ServicePrincipal('service.amazonaws.com'), + }); + const identityPool = new IdentityPool(stack, 'TestIdentityPoolActions', { + authenticatedRole, unauthenticatedRole, allowUnauthenticatedIdentities: true, + }); + identityPool.authenticatedRole.addToPrincipalPolicy(new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['execute-api:*', 'dynamodb:*'], + resources: ['*'], + })); + identityPool.unauthenticatedRole.addToPrincipalPolicy(new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['execute-api:*'], + resources: ['arn:aws:execute-api:us-east-1:*:my-api/prod'], + })); + const temp = Template.fromStack(stack); + temp.resourceCountIs('AWS::IAM::Role', 2); + temp.resourceCountIs('AWS::IAM::Policy', 2); + temp.hasResourceProperties('AWS::Cognito::IdentityPool', { + AllowUnauthenticatedIdentities: true, + }); + temp.hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'service.amazonaws.com', + }, + }, + ], + }, + }); + temp.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'execute-api:*', + Effect: 'Allow', + Resource: 'arn:aws:execute-api:us-east-1:*:my-api/prod', + }, + ], + }, + }); + + temp.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: ['execute-api:*', 'dynamodb:*'], + Effect: 'Allow', + Resource: '*', + }, + ], + }, + }); + }); + test('adding actions and resources to default roles', () => { + const stack = new Stack(); + const identityPool = new IdentityPool(stack, 'TestIdentityPoolActions'); + identityPool.authenticatedRole.addToPrincipalPolicy(new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['execute-api:*', 'dynamodb:*'], + resources: ['*'], + })); + identityPool.unauthenticatedRole.addToPrincipalPolicy(new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['execute-api:*'], + resources: ['arn:aws:execute-api:us-east-1:*:my-api/prod'], + })); + const temp = Template.fromStack(stack); + temp.resourceCountIs('AWS::IAM::Role', 2); + temp.resourceCountIs('AWS::IAM::Policy', 2); + temp.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'execute-api:*', + Effect: 'Allow', + Resource: 'arn:aws:execute-api:us-east-1:*:my-api/prod', + }, + ], + }, + }); + + temp.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: ['execute-api:*', 'dynamodb:*'], + Effect: 'Allow', + Resource: '*', + }, + ], + }, + }); + }); + + test('from static', () => { + const stack = new Stack(undefined, undefined, { + env: { + region: 'my-region', + account: '1234567891011', + }, + }); + expect(() => IdentityPool.fromIdentityPoolId(stack, 'idPoolIdError', 'idPool')).toThrowError('Invalid Identity Pool Id: Identity Pool Ids must follow the format :'); + expect(() => IdentityPool.fromIdentityPoolArn(stack, 'idPoolArnError', 'arn:aws:cognito-identity:my-region:1234567891011:identitypool\/your-region:idPool/')).toThrowError('Invalid Identity Pool Id: Region in Identity Pool Id must match stack region'); + const idPool = IdentityPool.fromIdentityPoolId(stack, 'staticIdPool', 'my-region:idPool'); + + expect(idPool.identityPoolId).toEqual('my-region:idPool'); + expect(idPool.identityPoolArn).toMatch(/cognito-identity:my-region:1234567891011:identitypool\/my-region:idPool/); + }); + + test('user pools are properly configured', () => { + const stack = new Stack(); + const poolProvider = UserPoolIdentityProvider.fromProviderName(stack, 'poolProvider', 'poolProvider'); + const otherPoolProvider = UserPoolIdentityProvider.fromProviderName(stack, 'otherPoolProvider', 'otherPoolProvider'); + const pool = new UserPool(stack, 'Pool'); + const otherPool = new UserPool(stack, 'OtherPool'); + pool.registerIdentityProvider(poolProvider); + otherPool.registerIdentityProvider(otherPoolProvider); + const idPool = new IdentityPool(stack, 'TestIdentityPoolUserPools', { + authenticationProviders: { + userPools: [new UserPoolAuthenticationProvider({ userPool: pool })], + }, + }); + idPool.addUserPoolAuthentication( + new UserPoolAuthenticationProvider({ + userPool: otherPool, + disableServerSideTokenCheck: true, + }), + ); + const temp = Template.fromStack(stack); + temp.resourceCountIs('AWS::Cognito::UserPool', 2); + temp.resourceCountIs('AWS::Cognito::UserPoolClient', 2); + temp.hasResourceProperties('AWS::Cognito::UserPoolClient', { + UserPoolId: { + Ref: 'PoolD3F588B8', + }, + AllowedOAuthFlows: [ + 'implicit', + 'code', + ], + AllowedOAuthFlowsUserPoolClient: true, + AllowedOAuthScopes: [ + 'profile', + 'phone', + 'email', + 'openid', + 'aws.cognito.signin.user.admin', + ], + CallbackURLs: [ + 'https://example.com', + ], + SupportedIdentityProviders: [ + 'poolProvider', + 'COGNITO', + ], + }); + temp.hasResourceProperties('AWS::Cognito::UserPoolClient', { + UserPoolId: { + Ref: 'OtherPool7DA7F2F7', + }, + AllowedOAuthFlows: [ + 'implicit', + 'code', + ], + AllowedOAuthFlowsUserPoolClient: true, + AllowedOAuthScopes: [ + 'profile', + 'phone', + 'email', + 'openid', + 'aws.cognito.signin.user.admin', + ], + CallbackURLs: [ + 'https://example.com', + ], + SupportedIdentityProviders: [ + 'otherPoolProvider', + 'COGNITO', + ], + }); + temp.hasResourceProperties('AWS::Cognito::IdentityPool', { + AllowUnauthenticatedIdentities: false, + CognitoIdentityProviders: [ + { + ClientId: { + Ref: 'PoolUserPoolAuthenticationProviderClient20F2FFC4', + }, + ProviderName: { + 'Fn::Join': [ + '', + [ + 'cognito-idp.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/', + { + Ref: 'PoolD3F588B8', + }, + ], + ], + }, + ServerSideTokenCheck: true, + }, + { + ClientId: { + Ref: 'OtherPoolUserPoolAuthenticationProviderClient08F670F8', + }, + ProviderName: { + 'Fn::Join': [ + '', + [ + 'cognito-idp.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/', + { + Ref: 'OtherPool7DA7F2F7', + }, + ], + ], + }, + ServerSideTokenCheck: false, + }, + ], + }); + }); + + test('openId, saml, classicFlow, customProviders', () => { + const stack = new Stack(); + const openId = new OpenIdConnectProvider(stack, 'OpenId', { + url: 'https://example.com', + clientIds: ['client1', 'client2'], + thumbprints: ['thumbprint'], + }); + const saml = new SamlProvider(stack, 'Provider', { + metadataDocument: SamlMetadataDocument.fromXml('document'), + }); + new IdentityPool(stack, 'TestIdentityPoolCustomProviders', { + authenticationProviders: { + openIdConnectProviders: [openId], + samlProviders: [saml], + customProvider: 'my-custom-provider.com', + }, + allowClassicFlow: true, + }); + const temp = Template.fromStack(stack); + temp.resourceCountIs('Custom::AWSCDKOpenIdConnectProvider', 1); + temp.resourceCountIs('AWS::IAM::SAMLProvider', 1); + temp.hasResourceProperties('AWS::Cognito::IdentityPool', { + AllowUnauthenticatedIdentities: false, + AllowClassicFlow: true, + DeveloperProviderName: 'my-custom-provider.com', + OpenIdConnectProviderARNs: [ + { + Ref: 'OpenId76D94D20', + }, + ], + SamlProviderARNs: [ + { + Ref: 'Provider2281708E', + }, + ], + }); + }); + + test('cognito authentication providers', () => { + const stack = new Stack(); + new IdentityPool(stack, 'TestIdentityPoolauthproviders', { + identityPoolName: 'my-id-pool', + authenticationProviders: { + amazon: { appId: 'amzn1.application.12312k3j234j13rjiwuenf' }, + google: { clientId: '12345678012.apps.googleusercontent.com' }, + }, + }); + const temp = Template.fromStack(stack); + temp.resourceCountIs('AWS::IAM::Role', 2); + temp.hasResourceProperties('AWS::Cognito::IdentityPool', { + IdentityPoolName: 'my-id-pool', + SupportedLoginProviders: { + 'www.amazon.com': 'amzn1.application.12312k3j234j13rjiwuenf', + 'accounts.google.com': '12345678012.apps.googleusercontent.com', + }, + }); + }); +}); + +describe('role mappings', () => { + test('using token', () => { + const stack = new Stack(); + new IdentityPool(stack, 'TestIdentityPoolRoleMappingToken', { + roleMappings: [{ + providerUrl: IdentityPoolProviderUrl.AMAZON, + useToken: true, + }], + }); + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::IdentityPoolRoleAttachment', { + IdentityPoolId: { + Ref: 'TestIdentityPoolRoleMappingToken0B11D690', + }, + RoleMappings: { + 'www.amazon.com': { + AmbiguousRoleResolution: 'Deny', + IdentityProvider: 'www.amazon.com', + Type: 'Token', + }, + }, + Roles: { + authenticated: { + 'Fn::GetAtt': [ + 'TestIdentityPoolRoleMappingTokenAuthenticatedRoleD99CE043', + 'Arn', + ], + }, + unauthenticated: { + 'Fn::GetAtt': [ + 'TestIdentityPoolRoleMappingTokenUnauthenticatedRole1D86D800', + 'Arn', + ], + }, + }, + }); + }); + + test('rules type without rules throws', () => { + const stack = new Stack(); + expect(() => new IdentityPool(stack, 'TestIdentityPoolRoleMappingErrors', { + roleMappings: [{ + providerUrl: IdentityPoolProviderUrl.AMAZON, + }], + })).toThrowError('IdentityPoolRoleMapping.rules is required when useToken is false'); + }); + + test('role mapping with rules configuration', () => { + const stack = new Stack(); + const adminRole = new Role(stack, 'adminRole', { + assumedBy: new ServicePrincipal('admin.amazonaws.com'), + }); + const nonAdminRole = new Role(stack, 'nonAdminRole', { + assumedBy: new AnyPrincipal(), + inlinePolicies: { + DenyAll: new PolicyDocument({ + statements: [ + new PolicyStatement({ + effect: Effect.DENY, + actions: ['update:*', 'put:*', 'delete:*'], + resources: ['*'], + }), + ], + }), + }, + }); + const facebookRole = new Role(stack, 'facebookRole', { + assumedBy: new ArnPrincipal('arn:aws:iam::123456789012:user/FacebookUser'), + }); + const customRole = new Role(stack, 'customRole', { + assumedBy: new ArnPrincipal('arn:aws:iam::123456789012:user/CustomUser'), + }); + const idPool = new IdentityPool(stack, 'TestIdentityPoolRoleMappingRules', { + roleMappings: [{ + providerUrl: IdentityPoolProviderUrl.AMAZON, + resolveAmbiguousRoles: true, + rules: [ + { + claim: 'custom:admin', + claimValue: 'admin', + mappedRole: adminRole, + }, + { + claim: 'custom:admin', + claimValue: 'admin', + matchType: RoleMappingMatchType.NOTEQUAL, + mappedRole: nonAdminRole, + }, + ], + }], + }); + idPool.addRoleMappings({ + providerUrl: IdentityPoolProviderUrl.FACEBOOK, + rules: [ + { + claim: 'iss', + claimValue: 'https://graph.facebook.com', + mappedRole: facebookRole, + }, + ], + }, + { + providerUrl: IdentityPoolProviderUrl.custom('example.com'), + rules: [ + { + claim: 'iss', + claimValue: 'https://example.com', + mappedRole: customRole, + }, + ], + }); + const temp = Template.fromStack(stack); + temp.resourceCountIs('AWS::Cognito::IdentityPoolRoleAttachment', 2); + temp.hasResourceProperties('AWS::Cognito::IdentityPoolRoleAttachment', { + IdentityPoolId: { + Ref: 'TestIdentityPoolRoleMappingRulesC8C07BC3', + }, + RoleMappings: { + 'www.amazon.com': { + AmbiguousRoleResolution: 'AuthenticatedRole', + IdentityProvider: 'www.amazon.com', + RulesConfiguration: { + Rules: [ + { + Claim: 'custom:admin', + MatchType: 'Equals', + RoleARN: { + 'Fn::GetAtt': [ + 'adminRoleC345D70B', + 'Arn', + ], + }, + Value: 'admin', + }, + { + Claim: 'custom:admin', + MatchType: 'NotEqual', + RoleARN: { + 'Fn::GetAtt': [ + 'nonAdminRole43C19D5C', + 'Arn', + ], + }, + Value: 'admin', + }, + ], + }, + Type: 'Rules', + }, + }, + Roles: { + authenticated: { + 'Fn::GetAtt': [ + 'TestIdentityPoolRoleMappingRulesAuthenticatedRole14D102C7', + 'Arn', + ], + }, + unauthenticated: { + 'Fn::GetAtt': [ + 'TestIdentityPoolRoleMappingRulesUnauthenticatedRole79A7AF99', + 'Arn', + ], + }, + }, + }); + temp.hasResourceProperties('AWS::Cognito::IdentityPoolRoleAttachment', { + IdentityPoolId: { + Ref: 'TestIdentityPoolRoleMappingRulesC8C07BC3', + }, + RoleMappings: { + 'graph.facebook.com': { + AmbiguousRoleResolution: 'Deny', + IdentityProvider: 'graph.facebook.com', + RulesConfiguration: { + Rules: [ + { + Claim: 'iss', + MatchType: 'Equals', + RoleARN: { + 'Fn::GetAtt': [ + 'facebookRole9D649CD8', + 'Arn', + ], + }, + Value: 'https://graph.facebook.com', + }, + ], + }, + Type: 'Rules', + }, + 'example.com': { + AmbiguousRoleResolution: 'Deny', + IdentityProvider: 'example.com', + RulesConfiguration: { + Rules: [ + { + Claim: 'iss', + MatchType: 'Equals', + RoleARN: { + 'Fn::GetAtt': [ + 'customRole4C920FF0', + 'Arn', + ], + }, + Value: 'https://example.com', + }, + ], + }, + Type: 'Rules', + }, + }, + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito-identitypool/test/integ.identitypool.expected.json b/packages/@aws-cdk/aws-cognito-identitypool/test/integ.identitypool.expected.json new file mode 100644 index 0000000000000..b2ec1baf42d81 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/test/integ.identitypool.expected.json @@ -0,0 +1,412 @@ +{ + "Resources": { + "PoolD3F588B8": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsVerificationMessage": "The verification code to your new account is {####}", + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "PoolUserPoolAuthenticationProviderClient20F2FFC4": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "PoolD3F588B8" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + { + "Ref": "PoolProviderGoogle76A1E8D0" + }, + "COGNITO" + ] + } + }, + "PoolProviderGoogle76A1E8D0": { + "Type": "AWS::Cognito::UserPoolIdentityProvider", + "Properties": { + "ProviderName": "Google", + "ProviderType": "Google", + "UserPoolId": { + "Ref": "PoolD3F588B8" + }, + "AttributeMapping": { + "given_name": "given_name", + "family_name": "family_name", + "email": "email", + "gender": "gender", + "names": "names" + }, + "ProviderDetails": { + "client_id": "google-client-id", + "client_secret": "google-client-secret", + "authorize_scopes": "profile" + } + } + }, + "OtherPool7DA7F2F7": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsVerificationMessage": "The verification code to your new account is {####}", + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "OtherPoolUserPoolAuthenticationProviderClient08F670F8": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "OtherPool7DA7F2F7" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + { + "Ref": "OtherPoolProviderAmazon4EB0592F" + }, + "COGNITO" + ] + } + }, + "OtherPoolProviderAmazon4EB0592F": { + "Type": "AWS::Cognito::UserPoolIdentityProvider", + "Properties": { + "ProviderName": "LoginWithAmazon", + "ProviderType": "LoginWithAmazon", + "UserPoolId": { + "Ref": "OtherPool7DA7F2F7" + }, + "AttributeMapping": { + "given_name": "name", + "email": "email", + "userId": "user_id" + }, + "ProviderDetails": { + "client_id": "amzn-client-id", + "client_secret": "amzn-client-secret", + "authorize_scopes": "profile" + } + } + }, + "identitypoolE2A6D099": { + "Type": "AWS::Cognito::IdentityPool", + "Properties": { + "AllowUnauthenticatedIdentities": false, + "AllowClassicFlow": true, + "CognitoIdentityProviders": [ + { + "ClientId": { + "Ref": "PoolUserPoolAuthenticationProviderClient20F2FFC4" + }, + "ProviderName": { + "Fn::Join": [ + "", + [ + "cognito-idp.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "PoolD3F588B8" + } + ] + ] + }, + "ServerSideTokenCheck": true + }, + { + "ClientId": { + "Ref": "OtherPoolUserPoolAuthenticationProviderClient08F670F8" + }, + "ProviderName": { + "Fn::Join": [ + "", + [ + "cognito-idp.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "OtherPool7DA7F2F7" + } + ] + ] + }, + "ServerSideTokenCheck": true + } + ], + "IdentityPoolName": "my-id-pool", + "SupportedLoginProviders": { + "www.amazon.com": "amzn1.application.12312k3j234j13rjiwuenf", + "accounts.google.com": "12345678012.apps.googleusercontent.com" + } + }, + "DependsOn": [ + "OtherPool7DA7F2F7", + "OtherPoolUserPoolAuthenticationProviderClient08F670F8", + "PoolD3F588B8", + "PoolUserPoolAuthenticationProviderClient20F2FFC4" + ] + }, + "identitypoolAuthenticatedRoleB074B49D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "cognito-identity.amazonaws.com:aud": { + "Ref": "identitypoolE2A6D099" + } + }, + "ForAnyValue:StringLike": { + "cognito-identity.amazonaws.com:amr": "authenticated" + } + }, + "Effect": "Allow", + "Principal": { + "Federated": "cognito-identity.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Description": { + "Fn::Join": [ + "", + [ + "Default Authenticated Role for Identity Pool ", + { + "Fn::GetAtt": [ + "identitypoolE2A6D099", + "Name" + ] + } + ] + ] + } + }, + "DependsOn": [ + "OtherPool7DA7F2F7", + "OtherPoolUserPoolAuthenticationProviderClient08F670F8", + "PoolD3F588B8", + "PoolUserPoolAuthenticationProviderClient20F2FFC4" + ] + }, + "identitypoolAuthenticatedRoleDefaultPolicyCB4D2992": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:*", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "identitypoolAuthenticatedRoleDefaultPolicyCB4D2992", + "Roles": [ + { + "Ref": "identitypoolAuthenticatedRoleB074B49D" + } + ] + }, + "DependsOn": [ + "OtherPool7DA7F2F7", + "OtherPoolUserPoolAuthenticationProviderClient08F670F8", + "PoolD3F588B8", + "PoolUserPoolAuthenticationProviderClient20F2FFC4" + ] + }, + "identitypoolUnauthenticatedRoleE61CAC70": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "cognito-identity.amazonaws.com:aud": { + "Ref": "identitypoolE2A6D099" + } + }, + "ForAnyValue:StringLike": { + "cognito-identity.amazonaws.com:amr": "unauthenticated" + } + }, + "Effect": "Allow", + "Principal": { + "Federated": "cognito-identity.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Description": { + "Fn::Join": [ + "", + [ + "Default Unauthenticated Role for Identity Pool ", + { + "Fn::GetAtt": [ + "identitypoolE2A6D099", + "Name" + ] + } + ] + ] + } + }, + "DependsOn": [ + "OtherPool7DA7F2F7", + "OtherPoolUserPoolAuthenticationProviderClient08F670F8", + "PoolD3F588B8", + "PoolUserPoolAuthenticationProviderClient20F2FFC4" + ] + }, + "identitypoolUnauthenticatedRoleDefaultPolicyBFACCE98": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:Get*", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "identitypoolUnauthenticatedRoleDefaultPolicyBFACCE98", + "Roles": [ + { + "Ref": "identitypoolUnauthenticatedRoleE61CAC70" + } + ] + }, + "DependsOn": [ + "OtherPool7DA7F2F7", + "OtherPoolUserPoolAuthenticationProviderClient08F670F8", + "PoolD3F588B8", + "PoolUserPoolAuthenticationProviderClient20F2FFC4" + ] + }, + "identitypoolDefaultRoleAttachment6BCAB114": { + "Type": "AWS::Cognito::IdentityPoolRoleAttachment", + "Properties": { + "IdentityPoolId": { + "Ref": "identitypoolE2A6D099" + }, + "Roles": { + "authenticated": { + "Fn::GetAtt": [ + "identitypoolAuthenticatedRoleB074B49D", + "Arn" + ] + }, + "unauthenticated": { + "Fn::GetAtt": [ + "identitypoolUnauthenticatedRoleE61CAC70", + "Arn" + ] + } + } + }, + "DependsOn": [ + "OtherPool7DA7F2F7", + "OtherPoolUserPoolAuthenticationProviderClient08F670F8", + "PoolD3F588B8", + "PoolUserPoolAuthenticationProviderClient20F2FFC4" + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito-identitypool/test/integ.identitypool.ts b/packages/@aws-cdk/aws-cognito-identitypool/test/integ.identitypool.ts new file mode 100644 index 0000000000000..5fc7ee1027084 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/test/integ.identitypool.ts @@ -0,0 +1,73 @@ +import { + UserPool, + UserPoolIdentityProviderGoogle, + UserPoolIdentityProviderAmazon, + ProviderAttribute, +} from '@aws-cdk/aws-cognito'; +import { + Effect, + PolicyStatement, +} from '@aws-cdk/aws-iam'; +import { + App, + Stack, +} from '@aws-cdk/core'; +import { + IdentityPool, +} from '../lib/identitypool'; +import { + UserPoolAuthenticationProvider, +} from '../lib/identitypool-user-pool-authentication-provider'; + +const app = new App(); +const stack = new Stack(app, 'integ-identitypool'); + +const userPool = new UserPool(stack, 'Pool'); +new UserPoolIdentityProviderGoogle(stack, 'PoolProviderGoogle', { + userPool, + clientId: 'google-client-id', + clientSecret: 'google-client-secret', + attributeMapping: { + givenName: ProviderAttribute.GOOGLE_GIVEN_NAME, + familyName: ProviderAttribute.GOOGLE_FAMILY_NAME, + email: ProviderAttribute.GOOGLE_EMAIL, + gender: ProviderAttribute.GOOGLE_GENDER, + custom: { + names: ProviderAttribute.GOOGLE_NAMES, + }, + }, +}); +const otherPool = new UserPool(stack, 'OtherPool'); +new UserPoolIdentityProviderAmazon(stack, 'OtherPoolProviderAmazon', { + userPool: otherPool, + clientId: 'amzn-client-id', + clientSecret: 'amzn-client-secret', + attributeMapping: { + givenName: ProviderAttribute.AMAZON_NAME, + email: ProviderAttribute.AMAZON_EMAIL, + custom: { + userId: ProviderAttribute.AMAZON_USER_ID, + }, + }, +}); +const idPool = new IdentityPool(stack, 'identitypool', { + authenticationProviders: { + userPools: [new UserPoolAuthenticationProvider({ userPool })], + amazon: { appId: 'amzn1.application.12312k3j234j13rjiwuenf' }, + google: { clientId: '12345678012.apps.googleusercontent.com' }, + }, + allowClassicFlow: true, + identityPoolName: 'my-id-pool', +}); +idPool.authenticatedRole.addToPrincipalPolicy(new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['dynamodb:*'], + resources: ['*'], +})); +idPool.unauthenticatedRole.addToPrincipalPolicy(new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['dynamodb:Get*'], + resources: ['*'], +})); +idPool.addUserPoolAuthentication(new UserPoolAuthenticationProvider({ userPool: otherPool })); +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 2cb86ba2885dd..96629cf13decd 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -31,7 +31,7 @@ The two main components of Amazon Cognito are [user pools](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html) and [identity pools](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-identity.html). User pools are user directories that provide sign-up and sign-in options for your app users. Identity pools enable you to grant your users access to -other AWS services. +other AWS services. Identity Pool L2 Constructs can be found [here](../aws-cognito-identitypool). This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. @@ -91,7 +91,7 @@ new cognito.UserPool(this, 'myuserpool', { emailBody: 'Thanks for signing up to our awesome app! Your verification code is {####}', emailStyle: cognito.VerificationEmailStyle.CODE, smsMessage: 'Thanks for signing up to our awesome app! Your verification code is {####}', - } + }, }); ``` @@ -108,8 +108,8 @@ new cognito.UserPool(this, 'myuserpool', { userInvitation: { emailSubject: 'Invite to join our awesome app!', emailBody: 'Hello {username}, you have been invited to join our awesome app! Your temporary password is {####}', - smsMessage: 'Hello {username}, your temporary password for our awesome app is {####}' - } + smsMessage: 'Hello {username}, your temporary password for our awesome app is {####}', + }, }); ``` @@ -136,7 +136,7 @@ new cognito.UserPool(this, 'myuserpool', { // ... signInAliases: { username: true, - email: true + email: true, }, }); ``` @@ -165,7 +165,7 @@ new cognito.UserPool(this, 'myuserpool', { // ... // ... signInAliases: { username: true, email: true }, - autoVerify: { email: true, phone: true } + autoVerify: { email: true, phone: true }, }); ``` @@ -241,7 +241,7 @@ const poolSmsRole = new iam.Role(this, 'userpoolsmsrole', { new cognito.UserPool(this, 'myuserpool', { // ... smsRole: poolSmsRole, - smsRoleExternalId: 'c87467be-4f34-11ea-b77f-2e728ce88125' + smsRoleExternalId: 'c87467be-4f34-11ea-b77f-2e728ce88125', }); ``` @@ -327,7 +327,7 @@ Cognito to send emails through Amazon SES, which is detailed below. ```ts new cognito.UserPool(this, 'myuserpool', { - email: UserPoolEmail.withCognito('support@myawesomeapp.com'), + email: cognito.UserPoolEmail.withCognito('support@myawesomeapp.com'), }); ``` @@ -341,7 +341,7 @@ Once the SES setup is complete, the UserPool can be configured to use the SES em ```ts new cognito.UserPool(this, 'myuserpool', { - email: UserPoolEmail.withSES({ + email: cognito.UserPoolEmail.withSES({ fromEmail: 'noreply@myawesomeapp.com', fromName: 'Awesome App', replyTo: 'support@myawesomeapp.com', @@ -354,7 +354,7 @@ If the UserPool is being created in a different region, `sesRegion` must be used ```ts new cognito.UserPool(this, 'myuserpool', { - email: UserPoolEmail.withSES({ + email: cognito.UserPoolEmail.withSES({ sesRegion: 'us-east-1', fromEmail: 'noreply@myawesomeapp.com', fromName: 'Awesome App', @@ -395,7 +395,7 @@ on the construct, as so - const authChallengeFn = new lambda.Function(this, 'authChallengeFn', { runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', - code: lambda.Code.fromAsset(/* path to lambda asset */), + code: lambda.Code.fromAsset(path.join(__dirname, 'path/to/asset')), }); const userpool = new cognito.UserPool(this, 'myuserpool', { @@ -403,13 +403,13 @@ const userpool = new cognito.UserPool(this, 'myuserpool', { lambdaTriggers: { createAuthChallenge: authChallengeFn, // ... - } + }, }); userpool.addTrigger(cognito.UserPoolOperation.USER_MIGRATION, new lambda.Function(this, 'userMigrationFn', { runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', - code: lambda.Code.fromAsset(/* path to lambda asset */), + code: lambda.Code.fromAsset(path.join(__dirname, 'path/to/asset')), })); ``` @@ -428,7 +428,15 @@ Error message when running `cdk synth` or `cdk deploy`: To work around the circular dependency issue, use the `attachInlinePolicy()` API instead, as shown below. -```ts fixture=with-lambda-trigger +```ts +declare const postAuthFn: lambda.Function; + +const userpool = new cognito.UserPool(this, 'myuserpool', { + lambdaTriggers: { + postAuthentication: postAuthFn, + }, +}); + // provide permissions to describe the user pool scoped to the ARN the user pool postAuthFn.role?.attachInlinePolicy(new iam.Policy(this, 'userpool-policy', { statements: [new iam.PolicyStatement({ @@ -504,8 +512,8 @@ new cognito.UserPoolIdentityProviderAmazon(this, 'Amazon', { custom: { // custom user pool attributes go here uniqueId: cognito.ProviderAttribute.AMAZON_USER_ID, - } - } + }, + }, }); ``` @@ -530,7 +538,7 @@ and imported user pools, clients can also be created via the `UserPoolClient` co ```ts const importedPool = cognito.UserPool.fromUserPoolId(this, 'imported-pool', 'us-east-1_oiuR12Abd'); new cognito.UserPoolClient(this, 'customer-app-client', { - userPool: importedPool + userPool: importedPool, }); ``` @@ -547,7 +555,7 @@ pool.addClient('app-client', { authFlows: { userPassword: true, userSrp: true, - } + }, }); ``` @@ -575,7 +583,7 @@ pool.addClient('app-client', { scopes: [ cognito.OAuthScope.OPENID ], callbackUrls: [ 'https://my-app-domain.com/welcome' ], logoutUrls: [ 'https://my-app-domain.com/signin' ], - } + }, }); ``` @@ -605,22 +613,29 @@ pool.addClient('app-client', { supportedIdentityProviders: [ cognito.UserPoolClientIdentityProvider.AMAZON, cognito.UserPoolClientIdentityProvider.COGNITO, - ] + ], }); ``` -If the identity provider and the app client are created in the same stack, specify the dependency between both constructs to make sure that the identity provider already exists when the app client will be created. The app client cannot handle the dependency to the identity provider automatically because the client does not have access to the provider's construct. +If the identity provider and the app client are created in the same stack, specify the dependency between both constructs to +make sure that the identity provider already exists when the app client will be created. The app client cannot handle the +dependency to the identity provider automatically because the client does not have access to the provider's construct. ```ts +const pool = new cognito.UserPool(this, 'Pool'); const provider = new cognito.UserPoolIdentityProviderAmazon(this, 'Amazon', { - // ... + userPool: pool, + clientId: 'amzn-client-id', + clientSecret: 'amzn-client-secret', }); + const client = pool.addClient('app-client', { // ... supportedIdentityProviders: [ cognito.UserPoolClientIdentityProvider.AMAZON, ], -} +}); + client.node.addDependency(provider); ``` @@ -638,16 +653,17 @@ pool.addClient('app-client', { }); ``` -Clients can (and should) be allowed to read and write relevant user attributes only. Usually every client can be allowed to read the `given_name` -attribute but not every client should be allowed to set the `email_verified` attribute. +Clients can (and should) be allowed to read and write relevant user attributes only. Usually every client can be allowed to +read the `given_name` attribute but not every client should be allowed to set the `email_verified` attribute. The same criteria applies for both standard and custom attributes, more info is available at [Attribute Permissions and Scopes](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#user-pool-settings-attribute-permissions-and-scopes). -The default behaviour is to allow read and write permissions on all attributes. The following code shows how this can be configured for a client. +The default behaviour is to allow read and write permissions on all attributes. The following code shows how this can be +configured for a client. ```ts const pool = new cognito.UserPool(this, 'Pool'); -const clientWriteAttributes = (new ClientAttributes()) +const clientWriteAttributes = (new cognito.ClientAttributes()) .withStandardAttributes({fullname: true, email: true}) .withCustomAttributes('favouritePizza', 'favouriteBeverage'); @@ -662,8 +678,9 @@ pool.addClient('app-client', { }); ``` -[Token revocation](https://docs.aws.amazon.com/cognito/latest/developerguide/token-revocation.html -) can be configured to be able to revoke refresh tokens in app clients. By default, token revocation is enabled for new user pools. The property can be used to enable the token revocation in existing app clients or to change the default behavior. +[Token revocation](https://docs.aws.amazon.com/cognito/latest/developerguide/token-revocation.html) +can be configured to be able to revoke refresh tokens in app clients. By default, token revocation is enabled for new user +pools. The property can be used to enable the token revocation in existing app clients or to change the default behavior. ```ts const pool = new cognito.UserPool(this, 'Pool'); @@ -687,8 +704,8 @@ app clients and configures the clients to use these scopes. ```ts const pool = new cognito.UserPool(this, 'Pool'); -const readOnlyScope = new ResourceServerScope({ scopeName: 'read', scopeDescription: 'Read-only access' }); -const fullAccessScope = new ResourceServerScope({ scopeName: '*', scopeDescription: 'Full access' }); +const readOnlyScope = new cognito.ResourceServerScope({ scopeName: 'read', scopeDescription: 'Read-only access' }); +const fullAccessScope = new cognito.ResourceServerScope({ scopeName: '*', scopeDescription: 'Full access' }); const userServer = pool.addResourceServer('ResourceServer', { identifier: 'users', @@ -699,7 +716,7 @@ const readOnlyClient = pool.addClient('read-only-client', { // ... oAuth: { // ... - scopes: [ OAuthScope.resourceServer(userServer, readOnlyScope) ], + scopes: [ cognito.OAuthScope.resourceServer(userServer, readOnlyScope) ], }, }); @@ -707,7 +724,7 @@ const fullAccessClient = pool.addClient('full-access-client', { // ... oAuth: { // ... - scopes: [ OAuthScope.resourceServer(userServer, fullAccessScope) ], + scopes: [ cognito.OAuthScope.resourceServer(userServer, fullAccessScope) ], }, }); ``` @@ -720,7 +737,8 @@ configured using domains. There are two ways to set up a domain - either the Ama with an available domain prefix, or a custom domain name can be chosen. The custom domain must be one that is already owned, and whose certificate is registered in AWS Certificate Manager. -The following code sets up a user pool domain in Amazon Cognito hosted domain with the prefix 'my-awesome-app', and another domain with the custom domain 'user.myapp.com' - +The following code sets up a user pool domain in Amazon Cognito hosted domain with the prefix 'my-awesome-app', and +another domain with the custom domain 'user.myapp.com' - ```ts const pool = new cognito.UserPool(this, 'Pool'); @@ -763,15 +781,15 @@ const client = userpool.addClient('Client', { callbackUrls: [ 'https://myapp.com/home', 'https://myapp.com/users', - ] - } -}) + ], + }, +}); const domain = userpool.addDomain('Domain', { // ... }); const signInUrl = domain.signInUrl(client, { redirectUri: 'https://myapp.com/home', // must be a URL configured under 'callbackUrls' with the client -}) +}); ``` Existing domains can be imported into CDK apps using `UserPoolDomain.fromDomainName()` API diff --git a/packages/@aws-cdk/aws-cognito/package.json b/packages/@aws-cdk/aws-cognito/package.json index a8970f8b094d8..7f38517f6abae 100644 --- a/packages/@aws-cdk/aws-cognito/package.json +++ b/packages/@aws-cdk/aws-cognito/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -77,9 +84,9 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "@types/punycode": "^2.1.0", - "jest": "^27.4.5" + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-certificatemanager": "0.0.0", diff --git a/packages/@aws-cdk/aws-cognito/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-cognito/rosetta/default.ts-fixture index e0b5bfc8747a3..996a448f8bfc6 100644 --- a/packages/@aws-cdk/aws-cognito/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/aws-cognito/rosetta/default.ts-fixture @@ -5,6 +5,7 @@ import * as certificatemanager from '@aws-cdk/aws-certificatemanager'; import * as cognito from '@aws-cdk/aws-cognito'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; +import * as path from 'path'; class Fixture extends Stack { constructor(scope: Construct, id: string) { diff --git a/packages/@aws-cdk/aws-cognito/rosetta/with-lambda-trigger.ts-fixture b/packages/@aws-cdk/aws-cognito/rosetta/with-lambda-trigger.ts-fixture deleted file mode 100644 index de9aa90eedfc2..0000000000000 --- a/packages/@aws-cdk/aws-cognito/rosetta/with-lambda-trigger.ts-fixture +++ /dev/null @@ -1,26 +0,0 @@ -// Fixture with packages imported, but nothing else -import { Stack } from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import * as cognito from '@aws-cdk/aws-cognito'; -import * as iam from '@aws-cdk/aws-iam'; -import * as lambda from '@aws-cdk/aws-lambda'; - -class Fixture extends Stack { - constructor(scope: Construct, id: string) { - super(scope, id); - - const postAuthFn = new lambda.Function(this, 'postAuthFn', { - code: lambda.Code.fromInline('post authentication'), - runtime: lambda.Runtime.NODEJS_12_X, - handler: 'index.handler', - }); - - const userpool = new cognito.UserPool(this, 'myuserpool', { - lambdaTriggers: { - postAuthentication: postAuthFn, - }, - }); - - /// here - } -} diff --git a/packages/@aws-cdk/aws-config/README.md b/packages/@aws-cdk/aws-config/README.md index 0a8219a8c3f53..3a66934a43d69 100644 --- a/packages/@aws-cdk/aws-config/README.md +++ b/packages/@aws-cdk/aws-config/README.md @@ -59,16 +59,15 @@ For example, you could create a managed rule that checks whether active access k within the number of days specified. ```ts -import * as config from '@aws-cdk/aws-config'; -import * as cdk from '@aws-cdk/core'; - // https://docs.aws.amazon.com/config/latest/developerguide/access-keys-rotated.html new config.ManagedRule(this, 'AccessKeysRotated', { identifier: config.ManagedRuleIdentifiers.ACCESS_KEYS_ROTATED, inputParameters: { - maxAccessKeyAge: 60 // default is 90 days + maxAccessKeyAge: 60, // default is 90 days }, - maximumExecutionFrequency: config.MaximumExecutionFrequency.TWELVE_HOURS // default is 24 hours + + // default is 24 hours + maximumExecutionFrequency: config.MaximumExecutionFrequency.TWELVE_HOURS, }); ``` @@ -82,9 +81,6 @@ The following higher level constructs for AWS managed rules are available. Checks whether your active access keys are rotated within the number of days specified. ```ts -import * as config from '@aws-cdk/aws-config'; -import * as cdk from '@aws-cdk/aws-cdk'; - // compliant if access keys have been rotated within the last 90 days new config.AccessKeysRotated(this, 'AccessKeyRotated'); ``` @@ -95,12 +91,9 @@ Checks whether your CloudFormation stack's actual configuration differs, or has from it's expected configuration. ```ts -import * as config from '@aws-cdk/aws-config'; -import * as cdk from '@aws-cdk/aws-cdk'; - // compliant if stack's status is 'IN_SYNC' // non-compliant if the stack's drift status is 'DRIFTED' -new config.CloudFormationStackDriftDetectionCheck(stack, 'Drift', { +new config.CloudFormationStackDriftDetectionCheck(this, 'Drift', { ownStackOnly: true, // checks only the stack containing the rule }); ``` @@ -110,17 +103,14 @@ new config.CloudFormationStackDriftDetectionCheck(stack, 'Drift', { Checks whether your CloudFormation stacks are sending event notifications to a SNS topic. ```ts -import * as config from '@aws-cdk/aws-config'; -import * as cdk from '@aws-cdk/aws-cdk'; - // topics to which CloudFormation stacks may send event notifications -const topic1 = new sns.Topic(stack, 'AllowedTopic1'); -const topic2 = new sns.Topic(stack, 'AllowedTopic2'); +const topic1 = new sns.Topic(this, 'AllowedTopic1'); +const topic2 = new sns.Topic(this, 'AllowedTopic2'); // non-compliant if CloudFormation stack does not send notifications to 'topic1' or 'topic2' new config.CloudFormationStackNotificationCheck(this, 'NotificationCheck', { topics: [topic1, topic2], -}) +}); ``` ### Custom rules @@ -140,13 +130,15 @@ To create a custom rule, define a `CustomRule` and specify the Lambda Function to run and the trigger types. ```ts -import * as config from '@aws-cdk/aws-config'; +declare const evalComplianceFn: lambda.Function; new config.CustomRule(this, 'CustomRule', { lambdaFunction: evalComplianceFn, configurationChanges: true, periodic: true, - maximumExecutionFrequency: config.MaximumExecutionFrequency.SIX_HOURS, // default is 24 hours + + // default is 24 hours + maximumExecutionFrequency: config.MaximumExecutionFrequency.SIX_HOURS, }); ``` @@ -165,22 +157,21 @@ Use the `RuleScope` APIs (`fromResource()`, `fromResources()` or `fromTag()`) to the scope of both managed and custom rules: ```ts -import * as config from '@aws-cdk/aws-config'; - const sshRule = new config.ManagedRule(this, 'SSH', { identifier: config.ManagedRuleIdentifiers.EC2_SECURITY_GROUPS_INCOMING_SSH_DISABLED, ruleScope: config.RuleScope.fromResource(config.ResourceType.EC2_SECURITY_GROUP, 'sg-1234567890abcdefgh'), // restrict to specific security group }); +declare const evalComplianceFn: lambda.Function; const customRule = new config.CustomRule(this, 'Lambda', { lambdaFunction: evalComplianceFn, - configurationChanges: true + configurationChanges: true, ruleScope: config.RuleScope.fromResources([config.ResourceType.CLOUDFORMATION_STACK, config.ResourceType.S3_BUCKET]), // restrict to all CloudFormation stacks and S3 buckets }); const tagRule = new config.CustomRule(this, 'CostCenterTagRule', { lambdaFunction: evalComplianceFn, - configurationChanges: true + configurationChanges: true, ruleScope: config.RuleScope.fromTag('Cost Center', 'MyApp'), // restrict to a specific tag }); ``` @@ -194,10 +185,6 @@ Use the `onComplianceChange()` APIs to trigger an EventBridge event when a compl of your AWS Config Rule fails: ```ts -import * as config from '@aws-cdk/aws-config'; -import * as sns from '@aws-cdk/aws-sns'; -import * as targets from '@aws-cdk/aws-events-targets'; - // Topic to which compliance notification events will be published const complianceTopic = new sns.Topic(this, 'ComplianceTopic'); @@ -211,15 +198,13 @@ Use the `onReEvaluationStatus()` status to trigger an EventBridge event when an rule is re-evaluated. ```ts -import * as config from '@aws-cdk/aws-config'; -import * as sns from '@aws-cdk/aws-sns'; -import * as targets from '@aws-cdk/aws-events-targets'; - // Topic to which re-evaluation notification events will be published const reEvaluationTopic = new sns.Topic(this, 'ComplianceTopic'); + +const rule = new config.CloudFormationStackDriftDetectionCheck(this, 'Drift'); rule.onReEvaluationStatus('ReEvaluationEvent', { target: new targets.SnsTopic(reEvaluationTopic), -}) +}); ``` ### Example @@ -228,11 +213,6 @@ The following example creates a custom rule that evaluates whether EC2 instances Compliance events are published to an SNS topic. ```ts -import * as config from '@aws-cdk/aws-config'; -import * as lambda from '@aws-cdk/aws-lambda'; -import * as sns from '@aws-cdk/aws-sns'; -import * as targets from '@aws-cdk/aws-events-targets'; - // Lambda function containing logic that evaluates compliance with the rule. const evalComplianceFn = new lambda.Function(this, 'CustomFunction', { code: lambda.AssetCode.fromInline('exports.handler = (event) => console.log(event);'), @@ -244,7 +224,7 @@ const evalComplianceFn = new lambda.Function(this, 'CustomFunction', { const customRule = new config.CustomRule(this, 'Custom', { configurationChanges: true, lambdaFunction: evalComplianceFn, - ruleScope: config.RuleScope.fromResource([config.ResourceType.EC2_INSTANCE]), + ruleScope: config.RuleScope.fromResource(config.ResourceType.EC2_INSTANCE), }); // A rule to detect stack drifts diff --git a/packages/@aws-cdk/aws-config/lib/rule.ts b/packages/@aws-cdk/aws-config/lib/rule.ts index e1ac4d107ec9e..d4d176bc69c3b 100644 --- a/packages/@aws-cdk/aws-config/lib/rule.ts +++ b/packages/@aws-cdk/aws-config/lib/rule.ts @@ -1136,6 +1136,13 @@ export class ManagedRuleIdentifiers { * @see https://docs.aws.amazon.com/config/latest/developerguide/s3-account-level-public-access-blocks.html */ public static readonly S3_ACCOUNT_LEVEL_PUBLIC_ACCESS_BLOCKS = 'S3_ACCOUNT_LEVEL_PUBLIC_ACCESS_BLOCKS'; + /** + * Checks if Amazon Simple Storage Service (Amazon S3) buckets are publicly accessible. This rule is + * NON_COMPLIANT if an Amazon S3 bucket is not listed in the excludedPublicBuckets parameter and bucket level + * settings are public. + * @see https://docs.aws.amazon.com/config/latest/developerguide/s3-bucket-level-public-access-prohibited.html + */ + public static readonly S3_BUCKET_LEVEL_PUBLIC_ACCESS_PROHIBITED = 'S3_BUCKET_LEVEL_PUBLIC_ACCESS_PROHIBITED'; /** * Checks that the Amazon Simple Storage Service bucket policy does not allow * blocked bucket-level and object-level actions on resources in the bucket diff --git a/packages/@aws-cdk/aws-config/package.json b/packages/@aws-cdk/aws-config/package.json index 3579910453b7a..8a121a8927bd4 100644 --- a/packages/@aws-cdk/aws-config/package.json +++ b/packages/@aws-cdk/aws-config/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -72,14 +79,14 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-events-targets": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-events": "0.0.0", diff --git a/packages/@aws-cdk/aws-config/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-config/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..f644a3f9c8157 --- /dev/null +++ b/packages/@aws-cdk/aws-config/rosetta/default.ts-fixture @@ -0,0 +1,14 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as config from '@aws-cdk/aws-config'; +import * as targets from '@aws-cdk/aws-events-targets'; +import * as sns from '@aws-cdk/aws-sns'; +import * as lambda from '@aws-cdk/aws-lambda'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-config/test/managed-rules.test.ts b/packages/@aws-cdk/aws-config/test/managed-rules.test.ts index 98dd3fbd34262..b360267c97352 100644 --- a/packages/@aws-cdk/aws-config/test/managed-rules.test.ts +++ b/packages/@aws-cdk/aws-config/test/managed-rules.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as sns from '@aws-cdk/aws-sns'; import * as cdk from '@aws-cdk/core'; import * as config from '../lib'; @@ -12,7 +12,7 @@ describe('access keys', () => { new config.AccessKeysRotated(stack, 'AccessKeys'); // THEN - expect(stack).toHaveResource('AWS::Config::ConfigRule', { + Template.fromStack(stack).hasResourceProperties('AWS::Config::ConfigRule', { Source: { Owner: 'AWS', SourceIdentifier: 'ACCESS_KEYS_ROTATED', @@ -30,7 +30,7 @@ describe('access keys', () => { }); // THEN - expect(stack).toHaveResource('AWS::Config::ConfigRule', { + Template.fromStack(stack).hasResourceProperties('AWS::Config::ConfigRule', { Source: { Owner: 'AWS', SourceIdentifier: 'ACCESS_KEYS_ROTATED', @@ -51,7 +51,7 @@ describe('cloudformation stack', () => { new config.CloudFormationStackDriftDetectionCheck(stack, 'Drift'); // THEN - expect(stack).toHaveResource('AWS::Config::ConfigRule', { + Template.fromStack(stack).hasResourceProperties('AWS::Config::ConfigRule', { Source: { Owner: 'AWS', SourceIdentifier: 'CLOUDFORMATION_STACK_DRIFT_DETECTION_CHECK', @@ -71,7 +71,7 @@ describe('cloudformation stack', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -113,7 +113,7 @@ describe('cloudformation stack', () => { }); // THEN - expect(stack).toHaveResource('AWS::Config::ConfigRule', { + Template.fromStack(stack).hasResourceProperties('AWS::Config::ConfigRule', { Source: { Owner: 'AWS', SourceIdentifier: 'CLOUDFORMATION_STACK_NOTIFICATION_CHECK', @@ -157,7 +157,7 @@ describe('ec2 instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::Config::ConfigRule', { + Template.fromStack(stack).hasResourceProperties('AWS::Config::ConfigRule', { Source: { Owner: 'AWS', SourceIdentifier: config.ManagedRuleIdentifiers.EC2_INSTANCE_PROFILE_ATTACHED, @@ -165,3 +165,23 @@ describe('ec2 instance', () => { }); }); }); + +describe('s3 bucket level', () => { + test('public access prohibited', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new config.ManagedRule(stack, 'S3BucketLevelPublicAccessProhibited', { + identifier: config.ManagedRuleIdentifiers.S3_BUCKET_LEVEL_PUBLIC_ACCESS_PROHIBITED, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Config::ConfigRule', { + Source: { + Owner: 'AWS', + SourceIdentifier: config.ManagedRuleIdentifiers.S3_BUCKET_LEVEL_PUBLIC_ACCESS_PROHIBITED, + }, + }); + }); +}); diff --git a/packages/@aws-cdk/aws-config/test/rule.test.ts b/packages/@aws-cdk/aws-config/test/rule.test.ts index 259727982a330..a6e125b4d89ec 100644 --- a/packages/@aws-cdk/aws-config/test/rule.test.ts +++ b/packages/@aws-cdk/aws-config/test/rule.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as targets from '@aws-cdk/aws-events-targets'; import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; @@ -22,7 +21,7 @@ describe('rule', () => { }); // THEN - expect(stack).toHaveResource('AWS::Config::ConfigRule', { + Template.fromStack(stack).hasResourceProperties('AWS::Config::ConfigRule', { Source: { Owner: 'AWS', SourceIdentifier: 'AWS_SUPER_COOL', @@ -59,7 +58,7 @@ describe('rule', () => { }); // THEN - expect(stack).toHaveResource('AWS::Config::ConfigRule', { + Template.fromStack(stack).hasResource('AWS::Config::ConfigRule', { Properties: { Source: { Owner: 'CUSTOM_LAMBDA', @@ -97,16 +96,16 @@ describe('rule', () => { 'Function76856677', 'FunctionServiceRole675BB04A', ], - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Principal: 'config.amazonaws.com', SourceAccount: { Ref: 'AWS::AccountId', }, }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { ManagedPolicyArns: [ { 'Fn::Join': [ @@ -147,7 +146,7 @@ describe('rule', () => { }); // THEN - expect(stack).toHaveResource('AWS::Config::ConfigRule', { + Template.fromStack(stack).hasResourceProperties('AWS::Config::ConfigRule', { Scope: { ComplianceResourceId: 'i-1234', ComplianceResourceTypes: [ @@ -168,7 +167,7 @@ describe('rule', () => { }); // THEN - expect(stack).toHaveResource('AWS::Config::ConfigRule', { + Template.fromStack(stack).hasResourceProperties('AWS::Config::ConfigRule', { Scope: { ComplianceResourceTypes: [ 'AWS::S3::Bucket', @@ -189,7 +188,7 @@ describe('rule', () => { }); // THEN - expect(stack).toHaveResource('AWS::Config::ConfigRule', { + Template.fromStack(stack).hasResourceProperties('AWS::Config::ConfigRule', { Scope: { TagKey: 'key', TagValue: 'value', @@ -247,7 +246,7 @@ describe('rule', () => { target: new targets.LambdaFunction(fn), }); - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { EventPattern: { 'source': [ 'aws.config', diff --git a/packages/@aws-cdk/aws-connect/package.json b/packages/@aws-cdk/aws-connect/package.json index ac75d47895514..e9d851b1824a9 100644 --- a/packages/@aws-cdk/aws-connect/package.json +++ b/packages/@aws-cdk/aws-connect/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-connect", "module": "aws_cdk.aws_connect" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-cur/package.json b/packages/@aws-cdk/aws-cur/package.json index c17803344f565..00adc1d4d1602 100644 --- a/packages/@aws-cdk/aws-cur/package.json +++ b/packages/@aws-cdk/aws-cur/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-cur", "module": "aws_cdk.aws_cur" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-customerprofiles/package.json b/packages/@aws-cdk/aws-customerprofiles/package.json index 717eda9dee83f..d9a6f1632efab 100644 --- a/packages/@aws-cdk/aws-customerprofiles/package.json +++ b/packages/@aws-cdk/aws-customerprofiles/package.json @@ -7,6 +7,13 @@ "jsii": { "outdir": "dist", "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + }, "targets": { "dotnet": { "namespace": "Amazon.CDK.AWS.CustomerProfiles", @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-databrew/package.json b/packages/@aws-cdk/aws-databrew/package.json index acd0f10ef451c..749abf6bab9b7 100644 --- a/packages/@aws-cdk/aws-databrew/package.json +++ b/packages/@aws-cdk/aws-databrew/package.json @@ -28,6 +28,13 @@ "distName": "aws-cdk.aws-databrew", "module": "aws_cdk.aws_databrew" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-datapipeline/package.json b/packages/@aws-cdk/aws-datapipeline/package.json index d59036075a0a1..24d3144f7c33d 100644 --- a/packages/@aws-cdk/aws-datapipeline/package.json +++ b/packages/@aws-cdk/aws-datapipeline/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -76,7 +83,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-datasync/package.json b/packages/@aws-cdk/aws-datasync/package.json index c0349de237aa4..ff9c4cdd4699b 100644 --- a/packages/@aws-cdk/aws-datasync/package.json +++ b/packages/@aws-cdk/aws-datasync/package.json @@ -28,6 +28,13 @@ "distName": "aws-cdk.aws-datasync", "module": "aws_cdk.aws_datasync" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-dax/package.json b/packages/@aws-cdk/aws-dax/package.json index 6a180b888c0f6..2901f449cf06e 100644 --- a/packages/@aws-cdk/aws-dax/package.json +++ b/packages/@aws-cdk/aws-dax/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -76,7 +83,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-detective/package.json b/packages/@aws-cdk/aws-detective/package.json index e844cca4e6587..cc061a9a00648 100644 --- a/packages/@aws-cdk/aws-detective/package.json +++ b/packages/@aws-cdk/aws-detective/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-devopsguru/package.json b/packages/@aws-cdk/aws-devopsguru/package.json index f79e524f0e302..9937e175014f4 100644 --- a/packages/@aws-cdk/aws-devopsguru/package.json +++ b/packages/@aws-cdk/aws-devopsguru/package.json @@ -28,6 +28,13 @@ "distName": "aws-cdk.aws-devopsguru", "module": "aws_cdk.aws_devopsguru" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-directoryservice/package.json b/packages/@aws-cdk/aws-directoryservice/package.json index d3a7c9e583209..4d37918cd7b3a 100644 --- a/packages/@aws-cdk/aws-directoryservice/package.json +++ b/packages/@aws-cdk/aws-directoryservice/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -76,7 +83,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-dlm/package.json b/packages/@aws-cdk/aws-dlm/package.json index ac1bed18c542f..f1601bd7cc135 100644 --- a/packages/@aws-cdk/aws-dlm/package.json +++ b/packages/@aws-cdk/aws-dlm/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -77,7 +84,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-dms/package.json b/packages/@aws-cdk/aws-dms/package.json index f74d5e6e8636c..3f20999bb1e0a 100644 --- a/packages/@aws-cdk/aws-dms/package.json +++ b/packages/@aws-cdk/aws-dms/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -76,7 +83,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-docdb/README.md b/packages/@aws-cdk/aws-docdb/README.md index 4791d99a138f7..6fcd235a513c8 100644 --- a/packages/@aws-cdk/aws-docdb/README.md +++ b/packages/@aws-cdk/aws-docdb/README.md @@ -18,17 +18,18 @@ always launch a database in a VPC. Use the `vpcSubnets` attribute to control whe your instances will be launched privately or publicly: ```ts -const cluster = new DatabaseCluster(this, 'Database', { - masterUser: { - username: 'myuser' // NOTE: 'admin' is reserved by DocumentDB - excludeCharacters: '\"@/:', // optional, defaults to the set "\"@/" and is also used for eventually created rotations - secretName: '/myapp/mydocdb/masteruser', // optional, if you prefer to specify the secret name - }, - instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.LARGE), - vpcSubnets: { - subnetType: ec2.SubnetType.PUBLIC, - }, - vpc +declare const vpc: ec2.Vpc; +const cluster = new docdb.DatabaseCluster(this, 'Database', { + masterUser: { + username: 'myuser', // NOTE: 'admin' is reserved by DocumentDB + excludeCharacters: '\"@/:', // optional, defaults to the set "\"@/" and is also used for eventually created rotations + secretName: '/myapp/mydocdb/masteruser', // optional, if you prefer to specify the secret name + }, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.LARGE), + vpcSubnets: { + subnetType: ec2.SubnetType.PUBLIC, + }, + vpc, }); ``` @@ -42,6 +43,7 @@ To control who can access the cluster, use the `.connections` attribute. Documen you don't need to specify the port: ```ts +declare const cluster: docdb.DatabaseCluster; cluster.connections.allowDefaultPortFromAnyIpv4('Open to the world'); ``` @@ -49,6 +51,7 @@ The endpoints to access your database cluster will be available as the `.cluster attributes: ```ts +declare const cluster: docdb.DatabaseCluster; const writeAddress = cluster.clusterEndpoint.socketAddress; // "HOSTNAME:PORT" ``` @@ -56,7 +59,10 @@ If you have existing security groups you would like to add to the cluster, use t groups added in this way will not be managed by the `Connections` object of the cluster. ```ts -const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { +declare const vpc: ec2.Vpc; +declare const cluster: docdb.DatabaseCluster; + +const securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { vpc, }); cluster.addSecurityGroups(securityGroup); @@ -67,16 +73,17 @@ cluster.addSecurityGroups(securityGroup); Deletion protection can be enabled on an Amazon DocumentDB cluster to prevent accidental deletion of the cluster: ```ts -const cluster = new DatabaseCluster(this, 'Database', { - masterUser: { - username: 'myuser' - }, - instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.LARGE), - vpcSubnets: { - subnetType: ec2.SubnetType.PUBLIC, - }, - vpc, - deletionProtection: true // Enable deletion protection. +declare const vpc: ec2.Vpc; +const cluster = new docdb.DatabaseCluster(this, 'Database', { + masterUser: { + username: 'myuser', + }, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.LARGE), + vpcSubnets: { + subnetType: ec2.SubnetType.PUBLIC, + }, + vpc, + deletionProtection: true, // Enable deletion protection. }); ``` @@ -85,6 +92,7 @@ const cluster = new DatabaseCluster(this, 'Database', { When the master password is generated and stored in AWS Secrets Manager, it can be rotated automatically: ```ts +declare const cluster: docdb.DatabaseCluster; cluster.addRotationSingleUser(); // Will rotate automatically after 30 days ``` @@ -93,22 +101,28 @@ cluster.addRotationSingleUser(); // Will rotate automatically after 30 days The multi user rotation scheme is also available: ```ts +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; + +declare const myImportedSecret: secretsmanager.Secret; +declare const cluster: docdb.DatabaseCluster; + cluster.addRotationMultiUser('MyUser', { - secret: myImportedSecret // This secret must have the `masterarn` key + secret: myImportedSecret, // This secret must have the `masterarn` key }); ``` It's also possible to create user credentials together with the cluster and add rotation: ```ts +declare const cluster: docdb.DatabaseCluster; const myUserSecret = new docdb.DatabaseSecret(this, 'MyUserSecret', { username: 'myuser', - masterSecret: cluster.secret + masterSecret: cluster.secret, }); const myUserSecretAttached = myUserSecret.attach(cluster); // Adds DB connections information in the secret cluster.addRotationMultiUser('MyUser', { // Add rotation using the multi user scheme - secret: myUserSecretAttached // This secret must have the `masterarn` key + secret: myUserSecretAttached, // This secret must have the `masterarn` key }); ``` @@ -126,8 +140,21 @@ Sending audit or profiler needs to be configured in two places: 2. Enable the corresponding option(s) when creating the `DatabaseCluster`: ```ts -const cluster = new DatabaseCluster(this, 'Database', { - ..., +import * as iam from '@aws-cdk/aws-iam'; +import * as logs from'@aws-cdk/aws-logs'; + +declare const myLogsPublishingRole: iam.Role; +declare const vpc: ec2.Vpc; + +const cluster = new docdb.DatabaseCluster(this, 'Database', { + masterUser: { + username: 'myuser', + }, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.LARGE), + vpcSubnets: { + subnetType: ec2.SubnetType.PUBLIC, + }, + vpc, exportProfilerLogsToCloudWatch: true, // Enable sending profiler logs exportAuditLogsToCloudWatch: true, // Enable sending audit logs cloudWatchLogsRetention: logs.RetentionDays.THREE_MONTHS, // Optional - default is to never expire logs diff --git a/packages/@aws-cdk/aws-docdb/package.json b/packages/@aws-cdk/aws-docdb/package.json index 5e145b8fa06f7..02721228f2097 100644 --- a/packages/@aws-cdk/aws-docdb/package.json +++ b/packages/@aws-cdk/aws-docdb/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -74,12 +81,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-ec2": "0.0.0", diff --git a/packages/@aws-cdk/aws-docdb/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-docdb/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..f5b4d71caa2a5 --- /dev/null +++ b/packages/@aws-cdk/aws-docdb/rosetta/default.ts-fixture @@ -0,0 +1,12 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as docdb from '@aws-cdk/aws-docdb'; +import * as ec2 from '@aws-cdk/aws-ec2'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-docdb/test/cluster.test.ts b/packages/@aws-cdk/aws-docdb/test/cluster.test.ts index b812d2b3e4742..4a6c44156f70f 100644 --- a/packages/@aws-cdk/aws-docdb/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-docdb/test/cluster.test.ts @@ -1,13 +1,11 @@ -import { expect as expectCDK, haveResource, ResourcePart, arrayWith, haveResourceLike, objectLike } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as kms from '@aws-cdk/aws-kms'; import * as logs from '@aws-cdk/aws-logs'; import * as cdk from '@aws-cdk/core'; - import { ClusterParameterGroup, DatabaseCluster, DatabaseSecret } from '../lib'; describe('DatabaseCluster', () => { - test('check that instantiation works', () => { // GIVEN const stack = testStack(); @@ -24,7 +22,7 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + Template.fromStack(stack).hasResource('AWS::DocDB::DBCluster', { Properties: { DBSubnetGroupName: { Ref: 'DatabaseSubnets56F17B9A' }, MasterUsername: 'admin', @@ -34,20 +32,20 @@ describe('DatabaseCluster', () => { }, DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition)); + }); - expectCDK(stack).to(haveResource('AWS::DocDB::DBInstance', { + Template.fromStack(stack).hasResource('AWS::DocDB::DBInstance', { DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition)); + }); - expectCDK(stack).to(haveResource('AWS::DocDB::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBSubnetGroup', { SubnetIds: [ { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, { Ref: 'VPCPrivateSubnet3Subnet3EDCD457' }, ], - })); + }); }); test('can create a cluster with a single instance', () => { @@ -67,12 +65,12 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBCluster', { DBSubnetGroupName: { Ref: 'DatabaseSubnets56F17B9A' }, MasterUsername: 'admin', MasterUserPassword: 'tooshort', VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], - })); + }); }); test('errors when less than one instance is specified', () => { @@ -132,11 +130,11 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::SecretsManager::SecretTargetAttachment', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::SecretTargetAttachment', { SecretId: { Ref: 'DatabaseSecret3B817195' }, TargetId: { Ref: 'DatabaseB269D8BB' }, TargetType: 'AWS::DocDB::DBCluster', - })); + }); }); test('can create a cluster with imported vpc and security group', () => { @@ -160,12 +158,12 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBCluster', { DBSubnetGroupName: { Ref: 'DatabaseSubnets56F17B9A' }, MasterUsername: 'admin', MasterUserPassword: 'tooshort', VpcSecurityGroupIds: ['SecurityGroupId12345'], - })); + }); }); test('can configure cluster deletion protection', () => { @@ -184,9 +182,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBCluster', { DeletionProtection: true, - })); + }); }); test('cluster with parameter group', () => { @@ -213,9 +211,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBCluster', { DBClusterParameterGroupName: { Ref: 'ParamsA8366201' }, - })); + }); }); test('cluster with imported parameter group', () => { @@ -237,9 +235,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBCluster', { DBClusterParameterGroupName: 'ParamGroupName', - })); + }); }); test('creates a secret when master credentials are not specified', () => { @@ -257,7 +255,7 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBCluster', { MasterUsername: { 'Fn::Join': [ '', @@ -282,16 +280,16 @@ describe('DatabaseCluster', () => { ], ], }, - })); + }); - expectCDK(stack).to(haveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { GenerateSecretString: { ExcludeCharacters: '\"@/', GenerateStringKey: 'password', PasswordLength: 41, SecretStringTemplate: '{"username":"admin"}', }, - })); + }); }); test('creates a secret with excludeCharacters', () => { @@ -310,11 +308,11 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResourceLike('AWS::SecretsManager::Secret', { - GenerateSecretString: objectLike({ + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { + GenerateSecretString: Match.objectLike({ ExcludeCharacters: '\"@/()[]', }), - })); + }); }); test('creates a secret with secretName set', () => { @@ -333,9 +331,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResourceLike('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { Name: '/myapp/mydocdb/masteruser', - })); + }); }); test('create an encrypted cluster with custom KMS key', () => { @@ -354,7 +352,7 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBCluster', { KmsKeyId: { 'Fn::GetAtt': [ 'Key961B73FD', @@ -362,7 +360,7 @@ describe('DatabaseCluster', () => { ], }, StorageEncrypted: true, - })); + }); }); test('creating a cluster defaults to using encryption', () => { @@ -380,9 +378,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBCluster', { StorageEncrypted: true, - })); + }); }); test('supplying a KMS key with storageEncryption false throws an error', () => { @@ -442,9 +440,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::DocDB::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBInstance', { DBInstanceIdentifier: `${instanceIdentifierBase}1`, - })); + }); }); test('cluster identifier used', () => { @@ -464,9 +462,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::DocDB::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBInstance', { DBInstanceIdentifier: `${clusterIdentifier}instance1`, - })); + }); }); test('imported cluster has supplied attributes', () => { @@ -515,9 +513,9 @@ describe('DatabaseCluster', () => { cluster.connections.allowToAnyIpv4(ec2.Port.tcp(443)); // THEN - expectCDK(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: 'sg-123456789', - })); + }); }); test('backup retention period respected', () => { @@ -538,9 +536,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBCluster', { BackupRetentionPeriod: 20, - })); + }); }); test('backup maintenance window respected', () => { @@ -562,10 +560,10 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBCluster', { BackupRetentionPeriod: 20, PreferredBackupWindow: '07:34-08:04', - })); + }); }); test('regular maintenance window respected', () => { @@ -584,9 +582,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBCluster', { PreferredMaintenanceWindow: '07:34-08:04', - })); + }); }); test('can configure CloudWatchLogs for audit', () => { @@ -605,9 +603,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBCluster', { EnableCloudwatchLogsExports: ['audit'], - })); + }); }); test('can configure CloudWatchLogs for profiler', () => { @@ -626,9 +624,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBCluster', { EnableCloudwatchLogsExports: ['profiler'], - })); + }); }); test('can configure CloudWatchLogs for all logs', () => { @@ -648,9 +646,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBCluster', { EnableCloudwatchLogsExports: ['audit', 'profiler'], - })); + }); }); test('can set CloudWatch log retention', () => { @@ -671,7 +669,7 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('Custom::LogRetention', { + Template.fromStack(stack).hasResourceProperties('Custom::LogRetention', { ServiceToken: { 'Fn::GetAtt': [ 'LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A', @@ -680,8 +678,8 @@ describe('DatabaseCluster', () => { }, LogGroupName: { 'Fn::Join': ['', ['/aws/docdb/', { Ref: 'DatabaseB269D8BB' }, '/audit']] }, RetentionInDays: 90, - })); - expectCDK(stack).to(haveResource('Custom::LogRetention', { + }); + Template.fromStack(stack).hasResourceProperties('Custom::LogRetention', { ServiceToken: { 'Fn::GetAtt': [ 'LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A', @@ -690,7 +688,7 @@ describe('DatabaseCluster', () => { }, LogGroupName: { 'Fn::Join': ['', ['/aws/docdb/', { Ref: 'DatabaseB269D8BB' }, '/profiler']] }, RetentionInDays: 90, - })); + }); }); test('single user rotation', () => { @@ -709,7 +707,7 @@ describe('DatabaseCluster', () => { cluster.addRotationSingleUser(cdk.Duration.days(5)); // THEN - expectCDK(stack).to(haveResource('AWS::Serverless::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::Serverless::Application', { Location: { ApplicationId: { 'Fn::FindInMap': ['DatabaseRotationSingleUserSARMapping9AEB3E55', { Ref: 'AWS::Partition' }, 'applicationId'] }, SemanticVersion: { 'Fn::FindInMap': ['DatabaseRotationSingleUserSARMapping9AEB3E55', { Ref: 'AWS::Partition' }, 'semanticVersion'] }, @@ -742,8 +740,8 @@ describe('DatabaseCluster', () => { 'Fn::GetAtt': ['DatabaseRotationSingleUserSecurityGroupAC6E0E73', 'GroupId'], }, }, - })); - expectCDK(stack).to(haveResource('AWS::SecretsManager::RotationSchedule', { + }); + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { SecretId: { Ref: 'DatabaseSecretAttachmentE5D1B020' }, RotationLambdaARN: { 'Fn::GetAtt': ['DatabaseRotationSingleUser65F55654', 'Outputs.RotationLambdaARN'], @@ -751,7 +749,7 @@ describe('DatabaseCluster', () => { RotationRules: { AutomaticallyAfterDays: 5, }, - })); + }); }); test('single user rotation requires secret', () => { @@ -822,7 +820,7 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Serverless::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::Serverless::Application', { Location: { ApplicationId: { 'Fn::FindInMap': ['DatabaseRotationSARMappingE46CFA92', { Ref: 'AWS::Partition' }, 'applicationId'] }, SemanticVersion: { 'Fn::FindInMap': ['DatabaseRotationSARMappingE46CFA92', { Ref: 'AWS::Partition' }, 'semanticVersion'] }, @@ -856,8 +854,8 @@ describe('DatabaseCluster', () => { }, masterSecretArn: { Ref: 'DatabaseSecretAttachmentE5D1B020' }, }, - })); - expectCDK(stack).to(haveResource('AWS::SecretsManager::RotationSchedule', { + }); + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { SecretId: { Ref: 'UserSecret0463E4F5' }, RotationLambdaARN: { 'Fn::GetAtt': ['DatabaseRotation6B6E1D86', 'Outputs.RotationLambdaARN'], @@ -865,7 +863,7 @@ describe('DatabaseCluster', () => { RotationRules: { AutomaticallyAfterDays: 5, }, - })); + }); }); test('multi user rotation requires secret', () => { @@ -916,9 +914,9 @@ describe('DatabaseCluster', () => { cluster.addSecurityGroups(securityGroup); // THEN - expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { - VpcSecurityGroupIds: arrayWith(stack.resolve(securityGroup.securityGroupId)), - })); + Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBCluster', { + VpcSecurityGroupIds: Match.arrayWith([stack.resolve(securityGroup.securityGroupId)]), + }); }); }); diff --git a/packages/@aws-cdk/aws-docdb/test/instance.test.ts b/packages/@aws-cdk/aws-docdb/test/instance.test.ts index f1382746db07d..d7e90af3e3e90 100644 --- a/packages/@aws-cdk/aws-docdb/test/instance.test.ts +++ b/packages/@aws-cdk/aws-docdb/test/instance.test.ts @@ -1,8 +1,7 @@ -import { expect as expectCDK, haveOutput, haveResource, ResourcePart } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as constructs from 'constructs'; - import { DatabaseCluster, DatabaseInstance } from '../lib'; const CLUSTER_INSTANCE_TYPE = ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.LARGE); @@ -21,7 +20,7 @@ describe('DatabaseInstance', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::DocDB::DBInstance', { + Template.fromStack(stack).hasResource('AWS::DocDB::DBInstance', { Properties: { DBClusterIdentifier: { Ref: 'DatabaseB269D8BB' }, DBInstanceClass: EXPECTED_SYNTH_INSTANCE_TYPE, @@ -29,7 +28,7 @@ describe('DatabaseInstance', () => { }, DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition)); + }); }); test.each([ @@ -48,7 +47,7 @@ describe('DatabaseInstance', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::DocDB::DBInstance', { + Template.fromStack(stack).hasResource('AWS::DocDB::DBInstance', { Properties: { DBClusterIdentifier: { Ref: 'DatabaseB269D8BB' }, DBInstanceClass: EXPECTED_SYNTH_INSTANCE_TYPE, @@ -56,7 +55,7 @@ describe('DatabaseInstance', () => { }, DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition)); + }); }); test('check that the endpoint works', () => { @@ -75,9 +74,11 @@ describe('DatabaseInstance', () => { }); // THEN - expectCDK(stack).to(haveOutput({ - exportName, - outputValue: { + Template.fromStack(stack).hasOutput(exportName, { + Export: { + Name: exportName, + }, + Value: { 'Fn::Join': [ '', [ @@ -87,7 +88,7 @@ describe('DatabaseInstance', () => { ], ], }, - })); + }); }); test('check that instanceArn property works', () => { @@ -106,9 +107,11 @@ describe('DatabaseInstance', () => { }); // THEN - expectCDK(stack).to(haveOutput({ - exportName, - outputValue: { + Template.fromStack(stack).hasOutput(exportName, { + Export: { + Name: exportName, + }, + Value: { 'Fn::Join': [ '', [ @@ -119,7 +122,7 @@ describe('DatabaseInstance', () => { ], ], }, - })); + }); }); test('check importing works as expected', () => { @@ -147,9 +150,11 @@ describe('DatabaseInstance', () => { }); // THEN - expectCDK(stack).to(haveOutput({ - exportName: arnExportName, - outputValue: { + Template.fromStack(stack).hasOutput('ArnOutput', { + Export: { + Name: arnExportName, + }, + Value: { 'Fn::Join': [ '', [ @@ -159,11 +164,13 @@ describe('DatabaseInstance', () => { ], ], }, - })); - expectCDK(stack).to(haveOutput({ - exportName: endpointExportName, - outputValue: `${instanceEndpointAddress}:${port}`, - })); + }); + Template.fromStack(stack).hasOutput('EndpointOutput', { + Export: { + Name: endpointExportName, + }, + Value: `${instanceEndpointAddress}:${port}`, + }); }); }); @@ -191,4 +198,4 @@ class TestStack extends cdk.Stack { function testStack() { const stack = new TestStack(undefined, undefined, { env: { account: '12345', region: 'us-test-1' } }); return stack; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-docdb/test/parameter-group.test.ts b/packages/@aws-cdk/aws-docdb/test/parameter-group.test.ts index 66cf65e5ece60..03aaff0fb2e77 100644 --- a/packages/@aws-cdk/aws-docdb/test/parameter-group.test.ts +++ b/packages/@aws-cdk/aws-docdb/test/parameter-group.test.ts @@ -1,9 +1,8 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import { ClusterParameterGroup } from '../lib'; describe('ClusterParameterGroup', () => { - test('check that instantiation works', () => { // GIVEN const stack = new Stack(); @@ -18,14 +17,13 @@ describe('ClusterParameterGroup', () => { }); // THEN - expect(stack).to(haveResource('AWS::DocDB::DBClusterParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBClusterParameterGroup', { Description: 'desc', Family: 'hello', Parameters: { key: 'value', }, - })); - + }); }); test('check automatically generated descriptions', () => { @@ -41,13 +39,12 @@ describe('ClusterParameterGroup', () => { }); // THEN - expect(stack).to(haveResource('AWS::DocDB::DBClusterParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBClusterParameterGroup', { Description: 'Cluster parameter group for hello', Family: 'hello', Parameters: { key: 'value', }, - })); - + }); }); }); diff --git a/packages/@aws-cdk/aws-dynamodb-global/README.md b/packages/@aws-cdk/aws-dynamodb-global/README.md index a31c37eb3e114..77f2725e76797 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/README.md +++ b/packages/@aws-cdk/aws-dynamodb-global/README.md @@ -26,9 +26,9 @@ import { App } from '@aws-cdk/core'; const app = new App(); new GlobalTable(app, 'globdynamodb', { - partitionKey: { name: 'hashKey', type: AttributeType.String }, + partitionKey: { name: 'hashKey', type: AttributeType.STRING }, tableName: 'GlobalTable', - regions: [ "us-east-1", "us-east-2", "us-west-2" ] + regions: [ "us-east-1", "us-east-2", "us-west-2" ], }); app.synth(); ``` diff --git a/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json b/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json index 6f56e7032625a..0a5527985d286 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json +++ b/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json @@ -30,15 +30,15 @@ "license": "Apache-2.0", "devDependencies": { "aws-sdk": "^2.596.0", - "aws-sdk-mock": "^5.5.0", + "aws-sdk-mock": "5.6.0", "eslint": "^7.32.0", "eslint-config-standard": "^14.1.1", - "eslint-plugin-import": "^2.25.3", + "eslint-plugin-import": "^2.25.4", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.3.1", "eslint-plugin-standard": "^4.1.0", - "jest": "^27.4.5", + "jest": "^27.5.1", "lambda-tester": "^3.6.0", - "nock": "^13.2.1" + "nock": "^13.2.4" } } diff --git a/packages/@aws-cdk/aws-dynamodb-global/package.json b/packages/@aws-cdk/aws-dynamodb-global/package.json index 5fc4fef4ae023..5618565bc6119 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/package.json +++ b/packages/@aws-cdk/aws-dynamodb-global/package.json @@ -39,7 +39,14 @@ "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "keywords": [ "aws", @@ -56,12 +63,12 @@ "constructs": "^3.3.69" }, "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "peerDependencies": { "@aws-cdk/aws-dynamodb": "0.0.0", diff --git a/packages/@aws-cdk/aws-dynamodb-global/test/dynamodb-global.test.ts b/packages/@aws-cdk/aws-dynamodb-global/test/dynamodb-global.test.ts index 5a6a45d5be3a3..1d6f90d4a16ad 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/test/dynamodb-global.test.ts +++ b/packages/@aws-cdk/aws-dynamodb-global/test/dynamodb-global.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Attribute, AttributeType, StreamViewType, Table } from '@aws-cdk/aws-dynamodb'; import { describeDeprecated, testDeprecated } from '@aws-cdk/cdk-build-tools'; import { App, CfnOutput, Stack } from '@aws-cdk/core'; @@ -26,7 +26,7 @@ describeDeprecated('Default Global DynamoDB stack', () => { const topStack = stack.node.findChild(CONSTRUCT_NAME) as Stack; for ( const reg of STACK_PROPS.regions ) { const tableStack = topStack.node.findChild(CONSTRUCT_NAME + '-' + reg) as Stack; - expect(tableStack).toHaveResource('AWS::DynamoDB::Table', { + Template.fromStack(tableStack).hasResourceProperties('AWS::DynamoDB::Table', { 'KeySchema': [ { 'AttributeName': 'hashKey', @@ -46,12 +46,12 @@ describeDeprecated('Default Global DynamoDB stack', () => { }); } const customResourceStack = stack.node.findChild(CONSTRUCT_NAME + '-CustomResource') as Stack; - expect(customResourceStack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(customResourceStack).hasResourceProperties('AWS::Lambda::Function', { Description: 'Lambda to make DynamoDB a global table', Handler: 'index.handler', Timeout: 300, }); - expect(customResourceStack).toHaveResource('AWS::CloudFormation::CustomResource', { + Template.fromStack(customResourceStack).hasResourceProperties('AWS::CloudFormation::CustomResource', { Regions: STACK_PROPS.regions, ResourceType: 'Custom::DynamoGlobalTableCoordinator', TableName: TABLE_NAME, @@ -75,7 +75,7 @@ testDeprecated('GlobalTable generated stacks inherit their account from the pare value: globalTable.regionalTables[0].tableStreamArn!, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Outputs': { 'DynamoDbOutput': { 'Value': { diff --git a/packages/@aws-cdk/aws-dynamodb/README.md b/packages/@aws-cdk/aws-dynamodb/README.md index b79b1e0efe465..f006eb277d984 100644 --- a/packages/@aws-cdk/aws-dynamodb/README.md +++ b/packages/@aws-cdk/aws-dynamodb/README.md @@ -58,6 +58,23 @@ const table = new dynamodb.Table(this, 'Table', { Further reading: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode. +## Table Class + +DynamoDB supports two table classes: + +* STANDARD - the default mode, and is recommended for the vast majority of workloads. +* STANDARD_INFREQUENT_ACCESS - optimized for tables where storage is the dominant cost. + +```ts +const table = new dynamodb.Table(this, 'Table', { + partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, + tableClass: dynamodb.TableClass.STANDARD_INFREQUENT_ACCESS, +}); +``` + +Further reading: +https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.TableClasses.html + ## Configure AutoScaling for your table You can have DynamoDB automatically raise and lower the read and write capacities diff --git a/packages/@aws-cdk/aws-dynamodb/lib/scalable-table-attribute.ts b/packages/@aws-cdk/aws-dynamodb/lib/scalable-table-attribute.ts index fc2a538e2545d..8c5f6467f4715 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/scalable-table-attribute.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/scalable-table-attribute.ts @@ -25,7 +25,7 @@ export class ScalableTableAttribute extends appscaling.BaseScalableAttribute { } this.scalingPolicyCreated = true; const predefinedMetric = this.props.dimension.indexOf('ReadCapacity') === -1 - ? appscaling.PredefinedMetric.DYANMODB_WRITE_CAPACITY_UTILIZATION + ? appscaling.PredefinedMetric.DYNAMODB_WRITE_CAPACITY_UTILIZATION : appscaling.PredefinedMetric.DYNAMODB_READ_CAPACITY_UTILIZATION; super.doScaleToTrackMetric('Tracking', { diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 020ca091aa777..868796644b2be 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -117,6 +117,12 @@ export enum TableEncryption { /** * Server-side KMS encryption with a customer master key managed by customer. * If `encryptionKey` is specified, this key will be used, otherwise, one will be defined. + * + * > **NOTE**: if `encryptionKey` is not specified and the `Table` construct creates + * > a KMS key for you, the key will be created with default permissions. If you are using + * > CDKv2, these permissions will be sufficient to enable the key for use with DynamoDB tables. + * > If you are using CDKv1, make sure the feature flag `@aws-cdk/aws-kms:defaultKeyPolicies` + * > is set to `true` in your `cdk.json`. */ CUSTOMER_MANAGED = 'CUSTOMER_MANAGED', @@ -193,11 +199,24 @@ export interface TableOptions extends SchemaOptions { */ readonly serverSideEncryption?: boolean; + /** + * Specify the table class. + * @default STANDARD + */ + readonly tableClass?: TableClass; + /** * Whether server-side encryption with an AWS managed customer master key is enabled. * * This property cannot be set if `serverSideEncryption` is set. * + * > **NOTE**: if you set this to `CUSTOMER_MANAGED` and `encryptionKey` is not + * > specified, the key that the Tablet generates for you will be created with + * > default permissions. If you are using CDKv2, these permissions will be + * > sufficient to enable the key for use with DynamoDB tables. If you are + * > using CDKv1, make sure the feature flag + * > `@aws-cdk/aws-kms:defaultKeyPolicies` is set to `true` in your `cdk.json`. + * * @default - server-side encryption is enabled with an AWS owned customer master key */ readonly encryption?: TableEncryption; @@ -713,7 +732,8 @@ abstract class TableBase extends Resource implements ITable { * @param grantee The principal to grant access to */ public grantWriteData(grantee: iam.IGrantable): iam.Grant { - return this.combinedGrant(grantee, { keyActions: perms.KEY_WRITE_ACTIONS, tableActions: perms.WRITE_DATA_ACTIONS }); + const keyActions = perms.KEY_READ_ACTIONS.concat(perms.KEY_WRITE_ACTIONS); + return this.combinedGrant(grantee, { keyActions, tableActions: perms.WRITE_DATA_ACTIONS }); } /** @@ -1169,6 +1189,7 @@ export class Table extends TableBase { }, sseSpecification, streamSpecification, + tableClass: props.tableClass, timeToLiveSpecification: props.timeToLiveAttribute ? { attributeName: props.timeToLiveAttribute, enabled: true } : undefined, contributorInsightsSpecification: props.contributorInsightsEnabled !== undefined ? { enabled: props.contributorInsightsEnabled } : undefined, kinesisStreamSpecification: props.kinesisStream ? { streamArn: props.kinesisStream.streamArn } : undefined, @@ -1760,6 +1781,19 @@ export enum StreamViewType { KEYS_ONLY = 'KEYS_ONLY' } +/** + * DynamoDB's table class. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.TableClasses.html + */ +export enum TableClass { + /** Default table class for DynamoDB. */ + STANDARD = 'STANDARD', + + /** Table class for DynamoDB that reduces storage costs compared to existing DynamoDB Standard tables. */ + STANDARD_INFREQUENT_ACCESS = 'STANDARD_INFREQUENT_ACCESS', +} + /** * Just a convenient way to keep track of both attributes */ diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index da8456e834c22..f058c3d573fff 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -79,19 +79,19 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/aws-lambda": "^8.10.89", - "@types/jest": "^27.0.3", + "@types/aws-lambda": "^8.10.92", + "@types/jest": "^27.4.1", "@types/sinon": "^9.0.11", "aws-sdk": "^2.848.0", - "aws-sdk-mock": "^5.5.0", - "jest": "^27.4.5", + "aws-sdk-mock": "5.6.0", + "jest": "^27.5.1", "sinon": "^9.2.4", - "ts-jest": "^27.1.2" + "ts-jest": "^27.1.3" }, "dependencies": { "@aws-cdk/aws-applicationautoscaling": "0.0.0", diff --git a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts index 3c125c1bc0061..144fe017c6966 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts @@ -1,5 +1,4 @@ -import { arrayWith, ABSENT, ResourcePart, SynthUtils } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; import * as iam from '@aws-cdk/aws-iam'; import * as kinesis from '@aws-cdk/aws-kinesis'; @@ -18,6 +17,7 @@ import { ProjectionType, StreamViewType, Table, + TableClass, TableEncryption, Operation, CfnTable, @@ -84,20 +84,20 @@ describe('default properties', () => { test('hash key only', () => { new Table(stack, CONSTRUCT_NAME, { partitionKey: TABLE_PARTITION_KEY }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', { + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { AttributeDefinitions: [{ AttributeName: 'hashKey', AttributeType: 'S' }], KeySchema: [{ AttributeName: 'hashKey', KeyType: 'HASH' }], ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 }, }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', { DeletionPolicy: CfnDeletionPolicy.RETAIN }, ResourcePart.CompleteDefinition); + Template.fromStack(stack).hasResource('AWS::DynamoDB::Table', { DeletionPolicy: CfnDeletionPolicy.RETAIN }); }); test('removalPolicy is DESTROY', () => { new Table(stack, CONSTRUCT_NAME, { partitionKey: TABLE_PARTITION_KEY, removalPolicy: RemovalPolicy.DESTROY }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', { DeletionPolicy: CfnDeletionPolicy.DELETE }, ResourcePart.CompleteDefinition); + Template.fromStack(stack).hasResource('AWS::DynamoDB::Table', { DeletionPolicy: CfnDeletionPolicy.DELETE }); }); @@ -107,7 +107,7 @@ describe('default properties', () => { sortKey: TABLE_SORT_KEY, }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', { + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, { AttributeName: 'sortKey', AttributeType: 'N' }, @@ -126,7 +126,7 @@ describe('default properties', () => { sortKey: TABLE_SORT_KEY, }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, @@ -146,7 +146,7 @@ describe('default properties', () => { sortKey: TABLE_SORT_KEY, }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, @@ -167,7 +167,7 @@ describe('default properties', () => { sortKey: TABLE_SORT_KEY, }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, @@ -188,7 +188,7 @@ describe('default properties', () => { sortKey: TABLE_SORT_KEY, }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, @@ -209,7 +209,7 @@ describe('default properties', () => { sortKey: TABLE_SORT_KEY, }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, @@ -234,7 +234,7 @@ describe('default properties', () => { sortKey: TABLE_SORT_KEY, }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, @@ -261,7 +261,7 @@ describe('default properties', () => { sortKey: TABLE_SORT_KEY, }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { KeySchema: [ { AttributeName: 'hashKey', KeyType: 'HASH' }, @@ -288,7 +288,7 @@ describe('default properties', () => { sortKey: TABLE_SORT_KEY, }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { KeySchema: [ { AttributeName: 'hashKey', KeyType: 'HASH' }, @@ -313,8 +313,8 @@ describe('default properties', () => { // since the resource has not been used in a cross-environment manner, // so the name should not be filled - expect(stack).toHaveResourceLike('AWS::DynamoDB::Table', { - TableName: ABSENT, + Template.fromStack(stack).hasResource('AWS::DynamoDB::Table', { + TableName: Match.absent(), }); }); }); @@ -338,7 +338,7 @@ testDeprecated('when specifying every property', () => { }); Tags.of(table).add('Environment', 'Production'); - expect(stack).toHaveResource('AWS::DynamoDB::Table', + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, @@ -377,7 +377,7 @@ test('when specifying sse with customer managed CMK', () => { }); Tags.of(table).add('Environment', 'Production'); - expect(stack).toHaveResource('AWS::DynamoDB::Table', { + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { 'SSESpecification': { 'KMSMasterKeyId': { 'Fn::GetAtt': [ @@ -403,7 +403,7 @@ test('when specifying only encryptionKey', () => { }); Tags.of(table).add('Environment', 'Production'); - expect(stack).toHaveResource('AWS::DynamoDB::Table', { + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { 'SSESpecification': { 'KMSMasterKeyId': { 'Fn::GetAtt': [ @@ -430,7 +430,7 @@ test('when specifying sse with customer managed CMK with encryptionKey provided }); Tags.of(table).add('Environment', 'Production'); - expect(stack).toHaveResource('AWS::DynamoDB::Table', { + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { 'SSESpecification': { 'KMSMasterKeyId': { 'Fn::GetAtt': [ @@ -514,15 +514,15 @@ testLegacyBehavior('if an encryption key is included, encrypt/decrypt permission }); const user = new iam.User(stack, 'MyUser'); table.grantReadWriteData(user); - expect(stack).toMatchTemplate({ - 'Resources': { - 'TableAKey07CC09EC': { - 'Type': 'AWS::KMS::Key', - 'Properties': { - 'KeyPolicy': { - 'Statement': [ + Template.fromStack(stack).templateMatches({ + Resources: { + TableAKey07CC09EC: { + Type: 'AWS::KMS::Key', + Properties: { + KeyPolicy: { + Statement: [ { - 'Action': [ + Action: [ 'kms:Create*', 'kms:Describe*', 'kms:Enable*', @@ -539,99 +539,99 @@ testLegacyBehavior('if an encryption key is included, encrypt/decrypt permission 'kms:TagResource', 'kms:UntagResource', ], - 'Effect': 'Allow', - 'Principal': { - 'AWS': { + Effect: 'Allow', + Principal: { + AWS: { 'Fn::Join': [ '', [ 'arn:', { - 'Ref': 'AWS::Partition', + Ref: 'AWS::Partition', }, ':iam::', { - 'Ref': 'AWS::AccountId', + Ref: 'AWS::AccountId', }, ':root', ], ], }, }, - 'Resource': '*', + Resource: '*', }, { - 'Action': [ + Action: [ 'kms:Decrypt', 'kms:DescribeKey', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', ], - 'Effect': 'Allow', - 'Principal': { - 'AWS': { + Effect: 'Allow', + Principal: { + AWS: { 'Fn::GetAtt': [ 'MyUserDC45028B', 'Arn', ], }, }, - 'Resource': '*', + Resource: '*', }, ], - 'Version': '2012-10-17', + Version: '2012-10-17', }, - 'Description': 'Customer-managed key auto-created for encrypting DynamoDB table at Default/Table A', - 'EnableKeyRotation': true, + Description: 'Customer-managed key auto-created for encrypting DynamoDB table at Default/Table A', + EnableKeyRotation: true, }, - 'UpdateReplacePolicy': 'Retain', - 'DeletionPolicy': 'Retain', + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', }, - 'TableA3D7B5AFA': { - 'Type': 'AWS::DynamoDB::Table', - 'Properties': { - 'KeySchema': [ + TableA3D7B5AFA: { + Type: 'AWS::DynamoDB::Table', + Properties: { + KeySchema: [ { - 'AttributeName': 'hashKey', - 'KeyType': 'HASH', + AttributeName: 'hashKey', + KeyType: 'HASH', }, ], - 'AttributeDefinitions': [ + AttributeDefinitions: [ { - 'AttributeName': 'hashKey', - 'AttributeType': 'S', + AttributeName: 'hashKey', + AttributeType: 'S', }, ], - 'ProvisionedThroughput': { - 'ReadCapacityUnits': 5, - 'WriteCapacityUnits': 5, + ProvisionedThroughput: { + ReadCapacityUnits: 5, + WriteCapacityUnits: 5, }, - 'SSESpecification': { - 'KMSMasterKeyId': { + SSESpecification: { + KMSMasterKeyId: { 'Fn::GetAtt': [ 'TableAKey07CC09EC', 'Arn', ], }, - 'SSEEnabled': true, - 'SSEType': 'KMS', + SSEEnabled: true, + SSEType: 'KMS', }, - 'TableName': 'MyTable', + TableName: 'MyTable', }, - 'UpdateReplacePolicy': 'Retain', - 'DeletionPolicy': 'Retain', + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', }, - 'MyUserDC45028B': { - 'Type': 'AWS::IAM::User', + MyUserDC45028B: { + Type: 'AWS::IAM::User', }, - 'MyUserDefaultPolicy7B897426': { - 'Type': 'AWS::IAM::Policy', - 'Properties': { - 'PolicyDocument': { - 'Statement': [ + MyUserDefaultPolicy7B897426: { + Type: 'AWS::IAM::Policy', + Properties: { + PolicyDocument: { + Statement: [ { - 'Action': [ + Action: [ 'dynamodb:BatchGetItem', 'dynamodb:GetRecords', 'dynamodb:GetShardIterator', @@ -644,8 +644,8 @@ testLegacyBehavior('if an encryption key is included, encrypt/decrypt permission 'dynamodb:UpdateItem', 'dynamodb:DeleteItem', ], - 'Effect': 'Allow', - 'Resource': [ + Effect: 'Allow', + Resource: [ { 'Fn::GetAtt': [ 'TableA3D7B5AFA', @@ -653,20 +653,20 @@ testLegacyBehavior('if an encryption key is included, encrypt/decrypt permission ], }, { - 'Ref': 'AWS::NoValue', + Ref: 'AWS::NoValue', }, ], }, { - 'Action': [ + Action: [ 'kms:Decrypt', 'kms:DescribeKey', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', ], - 'Effect': 'Allow', - 'Resource': { + Effect: 'Allow', + Resource: { 'Fn::GetAtt': [ 'TableAKey07CC09EC', 'Arn', @@ -674,12 +674,12 @@ testLegacyBehavior('if an encryption key is included, encrypt/decrypt permission }, }, ], - 'Version': '2012-10-17', + Version: '2012-10-17', }, - 'PolicyName': 'MyUserDefaultPolicy7B897426', - 'Users': [ + PolicyName: 'MyUserDefaultPolicy7B897426', + Users: [ { - 'Ref': 'MyUserDC45028B', + Ref: 'MyUserDC45028B', }, ], }, @@ -698,28 +698,101 @@ test('if an encryption key is included, encrypt/decrypt permissions are added to const user = new iam.User(stack, 'MyUser'); table.grantReadWriteData(user); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { - 'PolicyDocument': { - 'Statement': arrayWith({ - 'Action': [ + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: Match.arrayWith([{ + Action: [ + 'kms:Decrypt', + 'kms:DescribeKey', + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', + ], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'TableAKey07CC09EC', + 'Arn', + ], + }, + }]), + }, + }); +}); + +test('if an encryption key is included, encrypt/decrypt permissions are added to the principal for grantWriteData', () => { + const stack = new Stack(); + const table = new Table(stack, 'Table A', { + tableName: TABLE_NAME, + partitionKey: TABLE_PARTITION_KEY, + encryption: TableEncryption.CUSTOMER_MANAGED, + }); + const user = new iam.User(stack, 'MyUser'); + table.grantWriteData(user); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: Match.arrayWith([{ + Action: [ 'kms:Decrypt', 'kms:DescribeKey', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', ], - 'Effect': 'Allow', - 'Resource': { + Effect: 'Allow', + Resource: { 'Fn::GetAtt': [ 'TableAKey07CC09EC', 'Arn', ], }, - }), + }]), }, }); }); +test('when specifying STANDARD_INFREQUENT_ACCESS table class', () => { + const stack = new Stack(); + new Table(stack, CONSTRUCT_NAME, { + partitionKey: TABLE_PARTITION_KEY, + tableClass: TableClass.STANDARD_INFREQUENT_ACCESS, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', + { + TableClass: 'STANDARD_INFREQUENT_ACCESS', + }, + ); +}); + +test('when specifying STANDARD table class', () => { + const stack = new Stack(); + new Table(stack, CONSTRUCT_NAME, { + partitionKey: TABLE_PARTITION_KEY, + tableClass: TableClass.STANDARD, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', + { + TableClass: 'STANDARD', + }, + ); +}); + +test('when specifying no table class', () => { + const stack = new Stack(); + new Table(stack, CONSTRUCT_NAME, { + partitionKey: TABLE_PARTITION_KEY, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', + { + TableClass: Match.absent(), + }, + ); +}); + test('when specifying PAY_PER_REQUEST billing mode', () => { const stack = new Stack(); new Table(stack, CONSTRUCT_NAME, { @@ -728,7 +801,7 @@ test('when specifying PAY_PER_REQUEST billing mode', () => { partitionKey: TABLE_PARTITION_KEY, }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { KeySchema: [ { AttributeName: 'hashKey', KeyType: 'HASH' }, @@ -891,7 +964,7 @@ test('when adding a global secondary index with hash key only', () => { writeCapacity: 1337, }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, @@ -933,7 +1006,7 @@ test('when adding a global secondary index with hash + range key', () => { writeCapacity: 1337, }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, @@ -975,7 +1048,7 @@ test('when adding a global secondary index with projection type KEYS_ONLY', () = projectionType: ProjectionType.KEYS_ONLY, }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, @@ -1017,7 +1090,7 @@ test('when adding a global secondary index with projection type INCLUDE', () => writeCapacity: 1337, }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, @@ -1056,7 +1129,7 @@ test('when adding a global secondary index on a table with PAY_PER_REQUEST billi partitionKey: GSI_PARTITION_KEY, }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, @@ -1171,7 +1244,7 @@ test('when adding multiple global secondary indexes', () => { table.addGlobalSecondaryIndex(gsiGenerator.next().value); } - expect(stack).toHaveResource('AWS::DynamoDB::Table', + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, @@ -1242,7 +1315,7 @@ test('when adding a global secondary index without specifying read and write cap partitionKey: GSI_PARTITION_KEY, }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, @@ -1277,7 +1350,7 @@ test('when adding a local secondary index with hash + range key', () => { sortKey: LSI_SORT_KEY, }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, @@ -1312,7 +1385,7 @@ test('when adding a local secondary index with projection type KEYS_ONLY', () => projectionType: ProjectionType.KEYS_ONLY, }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, @@ -1349,7 +1422,7 @@ test('when adding a local secondary index with projection type INCLUDE', () => { nonKeyAttributes: [lsiNonKeyAttributeGenerator.next().value, lsiNonKeyAttributeGenerator.next().value], }); - expect(stack).toHaveResource('AWS::DynamoDB::Table', + Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, @@ -1426,13 +1499,13 @@ test('can enable Read AutoScaling', () => { table.autoScaleReadCapacity({ minCapacity: 50, maxCapacity: 500 }).scaleOnUtilization({ targetUtilizationPercent: 75 }); // THEN - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { MaxCapacity: 500, MinCapacity: 50, ScalableDimension: 'dynamodb:table:ReadCapacityUnits', ServiceNamespace: 'dynamodb', }); - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'DynamoDBReadCapacityUtilization' }, @@ -1450,13 +1523,13 @@ test('can enable Write AutoScaling', () => { table.autoScaleWriteCapacity({ minCapacity: 50, maxCapacity: 500 }).scaleOnUtilization({ targetUtilizationPercent: 75 }); // THEN - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { MaxCapacity: 500, MinCapacity: 50, ScalableDimension: 'dynamodb:table:WriteCapacityUnits', ServiceNamespace: 'dynamodb', }); - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'DynamoDBWriteCapacityUtilization' }, @@ -1537,7 +1610,7 @@ test('can autoscale on a schedule', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { ScheduledActions: [ { ScalableTargetAction: { 'MaxCapacity': 10 }, @@ -1806,7 +1879,7 @@ describe('grants', () => { table.grant(user, 'dynamodb:action1', 'dynamodb:action2'); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -1874,7 +1947,7 @@ describe('grants', () => { Table.grantListStreams(user); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -1920,7 +1993,7 @@ describe('grants', () => { table.grantTableListStreams(user); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -1966,7 +2039,7 @@ describe('grants', () => { table.grantStreamRead(user); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -2007,7 +2080,7 @@ describe('grants', () => { table.grantReadData(user); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -2066,7 +2139,7 @@ describe('grants', () => { table.grant(user, 'dynamodb:*'); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2159,7 +2232,7 @@ describe('import', () => { table.grantReadData(role); // it is possible to obtain a permission statement for a ref - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -2201,7 +2274,7 @@ describe('import', () => { table.grantReadWriteData(role); // it is possible to obtain a permission statement for a ref - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -2284,7 +2357,7 @@ describe('import', () => { expect(table.grantTableListStreams(role)).toBeDefined(); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2312,7 +2385,7 @@ describe('import', () => { expect(table.grantStreamRead(role)).toBeDefined(); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2347,7 +2420,7 @@ describe('import', () => { table.grantReadData(role); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2410,7 +2483,7 @@ describe('global', () => { }); // THEN - expect(stack).toHaveResource('Custom::DynamoDBReplica', { + Template.fromStack(stack).hasResource('Custom::DynamoDBReplica', { Properties: { TableName: { Ref: 'TableCD117FA1', @@ -2418,9 +2491,9 @@ describe('global', () => { Region: 'eu-west-2', }, Condition: 'TableStackRegionNotEqualseuwest2A03859E7', - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('Custom::DynamoDBReplica', { + Template.fromStack(stack).hasResource('Custom::DynamoDBReplica', { Properties: { TableName: { Ref: 'TableCD117FA1', @@ -2428,19 +2501,18 @@ describe('global', () => { Region: 'eu-central-1', }, Condition: 'TableStackRegionNotEqualseucentral199D46FC0', - }, ResourcePart.CompleteDefinition); + }); - expect(SynthUtils.toCloudFormation(stack).Conditions).toEqual({ - TableStackRegionNotEqualseuwest2A03859E7: { - 'Fn::Not': [ - { 'Fn::Equals': ['eu-west-2', { Ref: 'AWS::Region' }] }, - ], - }, - TableStackRegionNotEqualseucentral199D46FC0: { - 'Fn::Not': [ - { 'Fn::Equals': ['eu-central-1', { Ref: 'AWS::Region' }] }, - ], - }, + Template.fromStack(stack).hasCondition('TableStackRegionNotEqualseuwest2A03859E7', { + 'Fn::Not': [ + { 'Fn::Equals': ['eu-west-2', { Ref: 'AWS::Region' }] }, + ], + }); + + Template.fromStack(stack).hasCondition('TableStackRegionNotEqualseucentral199D46FC0', { + 'Fn::Not': [ + { 'Fn::Equals': ['eu-central-1', { Ref: 'AWS::Region' }] }, + ], }); }); @@ -2462,7 +2534,7 @@ describe('global', () => { }); // THEN - expect(stack).toHaveResource('Custom::DynamoDBReplica', { + Template.fromStack(stack).hasResource('Custom::DynamoDBReplica', { Properties: { TableName: { Ref: 'TableCD117FA1', @@ -2471,9 +2543,9 @@ describe('global', () => { SkipReplicationCompletedWait: 'true', }, Condition: 'TableStackRegionNotEqualseuwest2A03859E7', - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('Custom::DynamoDBReplica', { + Template.fromStack(stack).hasResource('Custom::DynamoDBReplica', { Properties: { TableName: { Ref: 'TableCD117FA1', @@ -2482,19 +2554,18 @@ describe('global', () => { SkipReplicationCompletedWait: 'true', }, Condition: 'TableStackRegionNotEqualseucentral199D46FC0', - }, ResourcePart.CompleteDefinition); + }); - expect(SynthUtils.toCloudFormation(stack).Conditions).toEqual({ - TableStackRegionNotEqualseuwest2A03859E7: { - 'Fn::Not': [ - { 'Fn::Equals': ['eu-west-2', { Ref: 'AWS::Region' }] }, - ], - }, - TableStackRegionNotEqualseucentral199D46FC0: { - 'Fn::Not': [ - { 'Fn::Equals': ['eu-central-1', { Ref: 'AWS::Region' }] }, - ], - }, + Template.fromStack(stack).hasCondition('TableStackRegionNotEqualseuwest2A03859E7', { + 'Fn::Not': [ + { 'Fn::Equals': ['eu-west-2', { Ref: 'AWS::Region' }] }, + ], + }); + + Template.fromStack(stack).hasCondition('TableStackRegionNotEqualseucentral199D46FC0', { + 'Fn::Not': [ + { 'Fn::Equals': ['eu-central-1', { Ref: 'AWS::Region' }] }, + ], }); }); @@ -2523,7 +2594,7 @@ describe('global', () => { table.grantReadData(user); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2677,7 +2748,7 @@ describe('global', () => { table.grantReadData(user); // THEN - expect(stack2).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack2).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2822,7 +2893,7 @@ describe('global', () => { table.grantTableListStreams(user); // THEN - expect(stack2).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack2).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2855,7 +2926,7 @@ describe('global', () => { // THEN expect(() => { - SynthUtils.synthesize(stack); + Template.fromStack(stack); }).toThrow(/A global Table that uses PROVISIONED as the billing mode needs auto-scaled write capacity/); }); @@ -2882,7 +2953,7 @@ describe('global', () => { // THEN expect(() => { - SynthUtils.synthesize(stack); + Template.fromStack(stack); }).toThrow(/A global Table that uses PROVISIONED as the billing mode needs auto-scaled write capacity with a policy/); }); @@ -2907,8 +2978,8 @@ describe('global', () => { maxCapacity: 10, }).scaleOnUtilization({ targetUtilizationPercent: 75 }); - expect(stack).toHaveResourceLike('AWS::DynamoDB::Table', { - BillingMode: ABSENT, // PROVISIONED is the default + Template.fromStack(stack).hasResource('AWS::DynamoDB::Table', { + BillingMode: Match.absent(), // PROVISIONED is the default }); }); @@ -2971,7 +3042,8 @@ describe('global', () => { }); // THEN - expect(SynthUtils.toCloudFormation(stack).Conditions).toBeUndefined(); + const conditions = Template.fromStack(stack).findConditions('*'); + expect(Object.keys(conditions).length).toEqual(0); }); test('can configure timeout', () => { @@ -3014,7 +3086,7 @@ test('L1 inside L2 expects removalpolicy to have been set', () => { new FakeTableL2(stack, 'Table'); expect(() => { - SynthUtils.toCloudFormation(stack); + Template.fromStack(stack); }).toThrow(/is a stateful resource type/); }); @@ -3029,7 +3101,7 @@ function testGrant(expectedActions: string[], invocation: (user: iam.IPrincipal, // THEN const action = expectedActions.length > 1 ? expectedActions.map(a => `dynamodb:${a}`) : `dynamodb:${expectedActions[0]}`; - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.kinesis-stream.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.kinesis-stream.expected.json index 77d522466ccf5..8f3ade0dc63d5 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.kinesis-stream.expected.json +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.kinesis-stream.expected.json @@ -4,6 +4,9 @@ "Type": "AWS::Kinesis::Stream", "Properties": { "ShardCount": 1, + "StreamModeDetails": { + "StreamMode": "PROVISIONED" + }, "RetentionPeriodHours": 24, "StreamEncryption": { "Fn::If": [ @@ -73,4 +76,4 @@ ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index 16c999efde724..fb2db769617e7 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -854,6 +854,46 @@ You can use the `Instance` class to start up a single EC2 instance. For producti you use an `AutoScalingGroup` from the `aws-autoscaling` module instead, as AutoScalingGroups will take care of restarting your instance if it ever fails. +```ts +declare const vpc: ec2.Vpc; +declare const instanceType: ec2.InstanceType; + +// AWS Linux +new ec2.Instance(this, 'Instance1', { + vpc, + instanceType, + machineImage: new ec2.AmazonLinuxImage(), +}); + +// AWS Linux 2 +new ec2.Instance(this, 'Instance2', { + vpc, + instanceType, + machineImage: new ec2.AmazonLinuxImage({ + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, + }), +}); + +// AWS Linux 2 with kernel 5.x +new ec2.Instance(this, 'Instance3', { + vpc, + instanceType, + machineImage: new ec2.AmazonLinuxImage({ + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, + kernel: ec2.AmazonLinuxKernel.KERNEL5_X, + }), +}); + +// AWS Linux 2022 +new ec2.Instance(this, 'Instance4', { + vpc, + instanceType, + machineImage: new ec2.AmazonLinuxImage({ + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2022, + }), +}); +``` + ### Configuring Instances using CloudFormation Init (cfn-init) CloudFormation Init allows you to configure your instances by writing files to them, installing software @@ -1011,6 +1051,37 @@ new ec2.Instance(this, 'Instance', { ``` +It is also possible to encrypt the block devices. In this example we will create an customer managed key encrypted EBS-backed root device: + +```ts +import { Key } from '@aws-cdk/aws-kms'; + +declare const vpc: ec2.Vpc; +declare const instanceType: ec2.InstanceType; +declare const machineImage: ec2.IMachineImage; + +const kmsKey = new Key(this, 'KmsKey') + +new ec2.Instance(this, 'Instance', { + vpc, + instanceType, + machineImage, + + // ... + + blockDevices: [ + { + deviceName: '/dev/sda1', + volume: ec2.BlockDeviceVolume.ebs(50, { + encrypted: true, + kmsKey: kmsKey, + }), + }, + ], +}); + +``` + ### Volumes Whereas a `BlockDeviceVolume` is an EBS volume that is created and destroyed as part of the creation and destruction of a specific instance. A `Volume` is for when you want an EBS volume separate from any particular instance. A `Volume` is an EBS block device that can be attached to, or detached from, any instance at any time. Some types of `Volume`s can also be attached to multiple instances at the same time to allow you to have shared storage between those instances. diff --git a/packages/@aws-cdk/aws-ec2/lib/aspects/require-imdsv2-aspect.ts b/packages/@aws-cdk/aws-ec2/lib/aspects/require-imdsv2-aspect.ts index f1a5270f1fb08..abad030c160a8 100644 --- a/packages/@aws-cdk/aws-ec2/lib/aspects/require-imdsv2-aspect.ts +++ b/packages/@aws-cdk/aws-ec2/lib/aspects/require-imdsv2-aspect.ts @@ -1,4 +1,6 @@ import * as cdk from '@aws-cdk/core'; +import { FeatureFlags } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { CfnLaunchTemplate } from '../ec2.generated'; import { Instance } from '../instance'; import { LaunchTemplate } from '../launch-template'; @@ -83,17 +85,20 @@ export class InstanceRequireImdsv2Aspect extends RequireImdsv2Aspect { return; } - const name = `${node.node.id}LaunchTemplate`; const launchTemplate = new CfnLaunchTemplate(node, 'LaunchTemplate', { launchTemplateData: { metadataOptions: { httpTokens: 'required', }, }, - launchTemplateName: name, }); + if (FeatureFlags.of(node).isEnabled(cxapi.EC2_UNIQUE_IMDSV2_LAUNCH_TEMPLATE_NAME)) { + launchTemplate.launchTemplateName = cdk.Names.uniqueId(launchTemplate); + } else { + launchTemplate.launchTemplateName = `${node.node.id}LaunchTemplate`; + } node.instance.launchTemplate = { - launchTemplateName: name, + launchTemplateName: launchTemplate.launchTemplateName, version: launchTemplate.getAtt('LatestVersionNumber').toString(), }; } diff --git a/packages/@aws-cdk/aws-ec2/lib/bastion-host.ts b/packages/@aws-cdk/aws-ec2/lib/bastion-host.ts index 5f1d7d3c8709f..c956b25bdd2a6 100644 --- a/packages/@aws-cdk/aws-ec2/lib/bastion-host.ts +++ b/packages/@aws-cdk/aws-ec2/lib/bastion-host.ts @@ -97,6 +97,13 @@ export interface BastionHostLinuxProps { * @default - default options */ readonly initOptions?: ApplyCloudFormationInitOptions; + + /** + * Whether IMDSv2 should be required on this instance + * + * @default - false + */ + readonly requireImdsv2?: boolean; } /** @@ -147,14 +154,17 @@ export class BastionHostLinux extends Resource implements IInstance { * @attribute */ public readonly instancePrivateDnsName: string; + /** * @attribute */ public readonly instancePrivateIp: string; + /** * @attribute */ public readonly instancePublicDnsName: string; + /** * @attribute */ @@ -178,6 +188,7 @@ export class BastionHostLinux extends Resource implements IInstance { blockDevices: props.blockDevices ?? undefined, init: props.init, initOptions: props.initOptions, + requireImdsv2: props.requireImdsv2 ?? false, }); this.instance.addToRolePolicy(new PolicyStatement({ actions: [ diff --git a/packages/@aws-cdk/aws-ec2/lib/client-vpn-endpoint.ts b/packages/@aws-cdk/aws-ec2/lib/client-vpn-endpoint.ts index 3ee8e8956d3d1..f28dd43462838 100644 --- a/packages/@aws-cdk/aws-ec2/lib/client-vpn-endpoint.ts +++ b/packages/@aws-cdk/aws-ec2/lib/client-vpn-endpoint.ts @@ -150,6 +150,37 @@ export interface ClientVpnEndpointOptions { * @default true */ readonly authorizeAllUsersToVpcCidr?: boolean; + + /** + * The maximum VPN session duration time. + * + * @default ClientVpnSessionTimeout.TWENTY_FOUR_HOURS + */ + readonly sessionTimeout?: ClientVpnSessionTimeout; + + /** + * Customizable text that will be displayed in a banner on AWS provided clients + * when a VPN session is established. + * + * UTF-8 encoded characters only. Maximum of 1400 characters. + * + * @default - no banner is presented to the client + */ + readonly clientLoginBanner?: string; +} + +/** + * Maximum VPN session duration time + */ +export enum ClientVpnSessionTimeout { + /** 8 hours */ + EIGHT_HOURS = 8, + /** 10 hours */ + TEN_HOURS = 10, + /** 12 hours */ + TWELVE_HOURS = 12, + /** 24 hours */ + TWENTY_FOUR_HOURS = 24, } /** @@ -284,6 +315,12 @@ export class ClientVpnEndpoint extends Resource implements IClientVpnEndpoint { throw new Error('The name of the Lambda function must begin with the `AWSClientVPN-` prefix'); } + if (props.clientLoginBanner + && !Token.isUnresolved(props.clientLoginBanner) + && props.clientLoginBanner.length > 1400) { + throw new Error(`The maximum length for the client login banner is 1400, got ${props.clientLoginBanner.length}`); + } + const logging = props.logging ?? true; const logGroup = logging ? props.logGroup ?? new logs.LogGroup(this, 'LogGroup') @@ -317,6 +354,13 @@ export class ClientVpnEndpoint extends Resource implements IClientVpnEndpoint { transportProtocol: props.transportProtocol, vpcId: props.vpc.vpcId, vpnPort: props.port, + sessionTimeoutHours: props.sessionTimeout, + clientLoginBannerOptions: props.clientLoginBanner + ? { + enabled: true, + bannerText: props.clientLoginBanner, + } + : undefined, }); this.endpointId = endpoint.ref; diff --git a/packages/@aws-cdk/aws-ec2/lib/instance-types.ts b/packages/@aws-cdk/aws-ec2/lib/instance-types.ts index 029219ce9bb79..cf82f9b8e40a5 100644 --- a/packages/@aws-cdk/aws-ec2/lib/instance-types.ts +++ b/packages/@aws-cdk/aws-ec2/lib/instance-types.ts @@ -725,6 +725,16 @@ export enum InstanceClass { * Multi-stream video transcoding instances for resolutions up to 4K UHD, 1st generation */ VT1 = 'vt1', + + /** + * High performance computing based on AMD EPYC, 6th generation + */ + HIGH_PERFORMANCE_COMPUTING6_AMD = 'hpc6a', + + /** + * High performance computing based on AMD EPYC, 6th generation + */ + HPC6A = 'hpc6a', } /** diff --git a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts index 731d9ea8d0c43..789d17c5167bd 100644 --- a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts +++ b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts @@ -309,6 +309,13 @@ export interface AmazonLinuxImageProps { */ readonly edition?: AmazonLinuxEdition; + /** + * What kernel version of Amazon Linux to use + * + * @default - + */ + readonly kernel?: AmazonLinuxKernel; + /** * Virtualization type * @@ -376,13 +383,29 @@ export class AmazonLinuxImage extends GenericSSMParameterImage { public static ssmParameterName(props: AmazonLinuxImageProps = {}) { const generation = (props && props.generation) || AmazonLinuxGeneration.AMAZON_LINUX; const edition = (props && props.edition) || AmazonLinuxEdition.STANDARD; - const virtualization = (props && props.virtualization) || AmazonLinuxVirt.HVM; - const storage = (props && props.storage) || AmazonLinuxStorage.GENERAL_PURPOSE; const cpu = (props && props.cpuType) || AmazonLinuxCpuType.X86_64; + let kernel = (props && props.kernel) || undefined; + let virtualization: AmazonLinuxVirt | undefined; + let storage: AmazonLinuxStorage | undefined; + + if (generation === AmazonLinuxGeneration.AMAZON_LINUX_2022) { + kernel = AmazonLinuxKernel.KERNEL5_X; + if (props && props.storage) { + throw new Error('Storage parameter does not exist in smm parameter name for Amazon Linux 2022.'); + } + if (props && props.virtualization) { + throw new Error('Virtualization parameter does not exist in smm parameter name for Amazon Linux 2022.'); + } + } else { + virtualization = (props && props.virtualization) || AmazonLinuxVirt.HVM; + storage = (props && props.storage) || AmazonLinuxStorage.GENERAL_PURPOSE; + } + const parts: Array = [ generation, 'ami', edition !== AmazonLinuxEdition.STANDARD ? edition : undefined, + kernel, virtualization, cpu, storage, @@ -427,6 +450,21 @@ export enum AmazonLinuxGeneration { * Amazon Linux 2 */ AMAZON_LINUX_2 = 'amzn2', + + /** + * Amazon Linux 2022 + */ + AMAZON_LINUX_2022 = 'al2022', +} + +/** + * Amazon Linux Kernel + */ +export enum AmazonLinuxKernel { + /** + * Standard edition + */ + KERNEL5_X = 'kernel-5.10', } /** @@ -441,7 +479,7 @@ export enum AmazonLinuxEdition { /** * Minimal edition */ - MINIMAL = 'minimal' + MINIMAL = 'minimal', } /** @@ -456,7 +494,7 @@ export enum AmazonLinuxVirt { /** * PV virtualization */ - PV = 'pv' + PV = 'pv', } export enum AmazonLinuxStorage { @@ -468,7 +506,7 @@ export enum AmazonLinuxStorage { /** * S3-backed storage */ - S3 = 'ebs', + S3 = 's3', /** * General Purpose-based storage (recommended) diff --git a/packages/@aws-cdk/aws-ec2/lib/peer.ts b/packages/@aws-cdk/aws-ec2/lib/peer.ts index 333bd66bc91a9..2c0cc4be394c4 100644 --- a/packages/@aws-cdk/aws-ec2/lib/peer.ts +++ b/packages/@aws-cdk/aws-ec2/lib/peer.ts @@ -75,6 +75,13 @@ export class Peer { return new PrefixList(prefixListId); } + /** + * A security group ID + */ + public static securityGroupId(securityGroupId: string, sourceSecurityGroupOwnerId?: string): IPeer { + return new SecurityGroupId(securityGroupId, sourceSecurityGroupOwnerId); + } + protected constructor() { } } @@ -199,3 +206,52 @@ class PrefixList implements IPeer { return { destinationPrefixListId: this.prefixListId }; } } + +/** + * A connection to or from a given security group ID + * + * For ingress rules, a sourceSecurityGroupOwnerId parameter can be specified if + * the security group exists in another account. + * This parameter will be ignored for egress rules. + */ +class SecurityGroupId implements IPeer { + public readonly canInlineRule = true; + public readonly connections: Connections = new Connections({ peer: this }); + public readonly uniqueId: string; + + constructor(private readonly securityGroupId: string, private readonly sourceSecurityGroupOwnerId?: string) { + if (!Token.isUnresolved(securityGroupId)) { + const securityGroupMatch = securityGroupId.match(/^sg-[a-z0-9]{8,17}$/); + + if (!securityGroupMatch) { + throw new Error(`Invalid security group ID: "${securityGroupId}"`); + } + } + + if (sourceSecurityGroupOwnerId && !Token.isUnresolved(sourceSecurityGroupOwnerId)) { + const accountNumberMatch = sourceSecurityGroupOwnerId.match(/^[0-9]{12}$/); + + if (!accountNumberMatch) { + throw new Error(`Invalid security group owner ID: "${sourceSecurityGroupOwnerId}"`); + } + } + this.uniqueId = securityGroupId; + } + + /** + * Produce the ingress rule JSON for the given connection + */ + public toIngressRuleConfig(): any { + return { + sourceSecurityGroupId: this.securityGroupId, + ...(this.sourceSecurityGroupOwnerId && { sourceSecurityGroupOwnerId: this.sourceSecurityGroupOwnerId }), + }; + } + + /** + * Produce the egress rule JSON for the given connection + */ + public toEgressRuleConfig(): any { + return { destinationSecurityGroupId: this.securityGroupId }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/lib/private/ebs-util.ts b/packages/@aws-cdk/aws-ec2/lib/private/ebs-util.ts index dc91f6d795011..52c7738afdbef 100644 --- a/packages/@aws-cdk/aws-ec2/lib/private/ebs-util.ts +++ b/packages/@aws-cdk/aws-ec2/lib/private/ebs-util.ts @@ -24,8 +24,11 @@ function synthesizeBlockDeviceMappings(construct: Construct, blockDevic return blockDevices.map(({ deviceName, volume, mappingEnabled }): RT => { const { virtualName, ebsDevice: ebs } = volume; + let finalEbs: CfnLaunchTemplate.EbsProperty | CfnInstance.EbsProperty | undefined; + if (ebs) { - const { iops, volumeType } = ebs; + + const { iops, volumeType, kmsKey, ...rest } = ebs; if (!iops) { if (volumeType === EbsDeviceVolumeType.IO1) { @@ -34,9 +37,25 @@ function synthesizeBlockDeviceMappings(construct: Construct, blockDevic } else if (volumeType !== EbsDeviceVolumeType.IO1) { Annotations.of(construct).addWarning('iops will be ignored without volumeType: EbsDeviceVolumeType.IO1'); } + + /** + * Because the Ebs properties of the L2 Constructs do not match the Ebs properties of the Cfn Constructs, + * we have to do some transformation and handle all destructed properties + */ + + finalEbs = { + ...rest, + iops, + volumeType, + kmsKeyId: kmsKey?.keyArn, + }; + + } else { + finalEbs = undefined; } + const noDevice = mappingEnabled === false ? noDeviceValue : undefined; - return { deviceName, ebs, virtualName, noDevice } as any; + return { deviceName, ebs: finalEbs, virtualName, noDevice } as any; }); } diff --git a/packages/@aws-cdk/aws-ec2/lib/user-data.ts b/packages/@aws-cdk/aws-ec2/lib/user-data.ts index 1e92eb888e6f8..82e256b9a895a 100644 --- a/packages/@aws-cdk/aws-ec2/lib/user-data.ts +++ b/packages/@aws-cdk/aws-ec2/lib/user-data.ts @@ -1,5 +1,5 @@ import { IBucket } from '@aws-cdk/aws-s3'; -import { CfnElement, Fn, Resource, Stack } from '@aws-cdk/core'; +import { Fn, Resource, Stack, CfnResource } from '@aws-cdk/core'; import { OperatingSystemType } from './machine-image'; /** @@ -178,7 +178,7 @@ class LinuxUserData extends UserData { public addSignalOnExitCommand( resource: Resource ): void { const stack = Stack.of(resource); - const resourceID = stack.getLogicalId(resource.node.defaultChild as CfnElement); + const resourceID = (resource.node.defaultChild as CfnResource).logicalId; this.addOnExitCommands(`/opt/aws/bin/cfn-signal --stack ${stack.stackName} --resource ${resourceID} --region ${stack.region} -e $exitCode || echo 'Failed to send Cloudformation Signal'`); } @@ -235,7 +235,7 @@ class WindowsUserData extends UserData { public addSignalOnExitCommand( resource: Resource ): void { const stack = Stack.of(resource); - const resourceID = stack.getLogicalId(resource.node.defaultChild as CfnElement); + const resourceID = (resource.node.defaultChild as CfnResource).logicalId; this.addOnExitCommands(`cfn-signal --stack ${stack.stackName} --resource ${resourceID} --region ${stack.region} --success ($success.ToString().ToLower())`); } diff --git a/packages/@aws-cdk/aws-ec2/lib/volume.ts b/packages/@aws-cdk/aws-ec2/lib/volume.ts index 87e92b3f5006a..723c0f91e602e 100644 --- a/packages/@aws-cdk/aws-ec2/lib/volume.ts +++ b/packages/@aws-cdk/aws-ec2/lib/volume.ts @@ -89,6 +89,17 @@ export interface EbsDeviceOptions extends EbsDeviceOptionsBase { * @default false */ readonly encrypted?: boolean; + + /** + * The ARN of the AWS Key Management Service (AWS KMS) CMK used for encryption. + * + * You have to ensure that the KMS CMK has the correct permissions to be used by the service launching the ec2 instances. + * + * @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html#ebs-encryption-requirements + * + * @default - If encrypted is true, the default aws/ebs KMS key will be used. + */ + readonly kmsKey?: IKey; } /** @@ -108,7 +119,7 @@ export interface EbsDeviceSnapshotOptions extends EbsDeviceOptionsBase { /** * Properties of an EBS block device */ -export interface EbsDeviceProps extends EbsDeviceSnapshotOptions { +export interface EbsDeviceProps extends EbsDeviceSnapshotOptions, EbsDeviceOptions { /** * The snapshot ID of the volume to use * diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint-service.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint-service.ts index ffd90a32df56b..119f8fdf07d6a 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint-service.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint-service.ts @@ -11,6 +11,8 @@ import { CfnVPCEndpointService, CfnVPCEndpointServicePermissions } from './ec2.g export interface IVpcEndpointServiceLoadBalancer { /** * The ARN of the load balancer that hosts the VPC Endpoint Service + * + * @attribute */ readonly loadBalancerArn: string; } diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts index 1f454f9d63f65..e8b37bb10d15f 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts @@ -574,7 +574,7 @@ export class InterfaceVpcEndpoint extends VpcEndpoint implements IInterfaceVpcEn const subnets = subnetSelection.subnets; // Sanity check the subnet count - if (subnetSelection.subnets.length == 0) { + if (!subnetSelection.isPendingLookup && subnetSelection.subnets.length == 0) { throw new Error('Cannot create a VPC Endpoint with no subnets'); } diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index a9033ef8da94d..8c8d6795fa23c 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -351,6 +351,17 @@ export interface SelectedSubnets { * Whether any of the given subnets are from the VPC's public subnets. */ readonly hasPublic: boolean; + + /** + * The subnet selection is not actually real yet + * + * If this value is true, don't validate anything about the subnets. The count + * or identities are not known yet, and the validation will most likely fail + * which will prevent a successful lookup. + * + * @default false + */ + readonly isPendingLookup?: boolean; } /** @@ -430,6 +441,7 @@ abstract class VpcBase extends Resource implements IVpc { internetConnectivityEstablished: tap(new CompositeDependable(), d => subnets.forEach(s => d.add(s.internetConnectivityEstablished))), subnets, hasPublic: subnets.some(s => pubs.has(s)), + isPendingLookup: this.incompleteSubnetDefinition, }; } @@ -957,6 +969,8 @@ export interface VpcProps { /** * The VPC name. * + * Since the VPC resource doesn't support providing a physical name, the value provided here will be recorded in the `Name` tag + * * @default this.node.path */ readonly vpcName?: string; diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index f9007178afc8e..a874249bf5736 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -79,16 +79,16 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/aws-lambda": "^8.10.89", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/aws-lambda": "^8.10.92", + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-cloudwatch": "0.0.0", @@ -693,7 +693,9 @@ "props-physical-name:@aws-cdk/aws-ec2.VpnGatewayProps", "props-physical-name:@aws-cdk/aws-ec2.ClientVpnEndpointProps", "props-physical-name:@aws-cdk/aws-ec2.ClientVpnAuthorizationRuleProps", - "props-physical-name:@aws-cdk/aws-ec2.ClientVpnRouteProps" + "props-physical-name:@aws-cdk/aws-ec2.ClientVpnRouteProps", + "duration-prop-type:@aws-cdk/aws-ec2.ClientVpnEndpointOptions.sessionTimeout", + "duration-prop-type:@aws-cdk/aws-ec2.ClientVpnEndpointProps.sessionTimeout" ] }, "stability": "stable", diff --git a/packages/@aws-cdk/aws-ec2/test/aspects/require-imdsv2-aspect.test.ts b/packages/@aws-cdk/aws-ec2/test/aspects/require-imdsv2-aspect.test.ts index ade2eaeab1f1d..297de07f16005 100644 --- a/packages/@aws-cdk/aws-ec2/test/aspects/require-imdsv2-aspect.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/aspects/require-imdsv2-aspect.test.ts @@ -1,10 +1,7 @@ -import { - countResources, - expect as expectCDK, - haveResourceLike, -} from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; +import { testFutureBehavior, testLegacyBehavior } from '@aws-cdk/cdk-build-tools'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { CfnLaunchTemplate, Instance, @@ -68,19 +65,19 @@ describe('RequireImdsv2Aspect', () => { // THEN const launchTemplate = instance.node.tryFindChild('LaunchTemplate') as LaunchTemplate; expect(launchTemplate).toBeDefined(); - expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateName: stack.resolve(launchTemplate.launchTemplateName), LaunchTemplateData: { MetadataOptions: { HttpTokens: 'required', }, }, - })); - expectCDK(stack).to(haveResourceLike('AWS::EC2::Instance', { + }); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { LaunchTemplate: { LaunchTemplateName: stack.resolve(launchTemplate.launchTemplateName), }, - })); + }); }); test('does not toggle when Instance has a LaunchTemplate', () => { @@ -102,7 +99,7 @@ describe('RequireImdsv2Aspect', () => { // THEN // Aspect normally creates a LaunchTemplate for the Instance to toggle IMDSv1, // so we can assert that one was not created - expectCDK(stack).to(countResources('AWS::EC2::LaunchTemplate', 0)); + Template.fromStack(stack).resourceCountIs('AWS::EC2::LaunchTemplate', 0); expect(instance.node.metadataEntry).toContainEqual({ data: expect.stringContaining('Cannot toggle IMDSv1 because this Instance is associated with an existing Launch Template.'), type: 'aws:cdk:warning', @@ -135,6 +132,57 @@ describe('RequireImdsv2Aspect', () => { trace: undefined, }); }); + + testFutureBehavior('launch template name is unique with feature flag', { [cxapi.EC2_UNIQUE_IMDSV2_LAUNCH_TEMPLATE_NAME]: true }, cdk.App, (app2) => { + // GIVEN + const otherStack = new cdk.Stack(app2, 'OtherStack'); + const otherVpc = new Vpc(otherStack, 'OtherVpc'); + const otherInstance = new Instance(otherStack, 'OtherInstance', { + vpc: otherVpc, + instanceType: new InstanceType('t2.micro'), + machineImage: MachineImage.latestAmazonLinux(), + }); + const imdsv2Stack = new cdk.Stack(app2, 'RequireImdsv2Stack'); + const imdsv2Vpc = new Vpc(imdsv2Stack, 'Vpc'); + const instance = new Instance(imdsv2Stack, 'Instance', { + vpc: imdsv2Vpc, + instanceType: new InstanceType('t2.micro'), + machineImage: MachineImage.latestAmazonLinux(), + }); + const aspect = new InstanceRequireImdsv2Aspect(); + + // WHEN + cdk.Aspects.of(imdsv2Stack).add(aspect); + cdk.Aspects.of(otherStack).add(aspect); + app2.synth(); + + // THEN + const launchTemplate = instance.node.tryFindChild('LaunchTemplate') as LaunchTemplate; + const otherLaunchTemplate = otherInstance.node.tryFindChild('LaunchTemplate') as LaunchTemplate; + expect(launchTemplate).toBeDefined(); + expect(otherLaunchTemplate).toBeDefined(); + expect(launchTemplate.launchTemplateName !== otherLaunchTemplate.launchTemplateName); + }); + + testLegacyBehavior('launch template name uses legacy id without feature flag', cdk.App, (app2) => { + // GIVEN + const imdsv2Stack = new cdk.Stack(app2, 'RequireImdsv2Stack'); + const imdsv2Vpc = new Vpc(imdsv2Stack, 'Vpc'); + const instance = new Instance(imdsv2Stack, 'Instance', { + vpc: imdsv2Vpc, + instanceType: new InstanceType('t2.micro'), + machineImage: MachineImage.latestAmazonLinux(), + }); + const aspect = new InstanceRequireImdsv2Aspect(); + + // WHEN + cdk.Aspects.of(imdsv2Stack).add(aspect); + app2.synth(); + + // THEN + const launchTemplate = instance.node.tryFindChild('LaunchTemplate') as LaunchTemplate; + expect(launchTemplate.launchTemplateName).toEqual(`${instance.node.id}LaunchTemplate`); + }); }); describe('LaunchTemplateRequireImdsv2Aspect', () => { @@ -185,13 +233,13 @@ describe('RequireImdsv2Aspect', () => { cdk.Aspects.of(stack).add(aspect); // THEN - expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { MetadataOptions: { HttpTokens: 'required', }, }, - })); + }); }); }); }); diff --git a/packages/@aws-cdk/aws-ec2/test/bastion-host.test.ts b/packages/@aws-cdk/aws-ec2/test/bastion-host.test.ts index 38570e3306f8a..2d21d5ac2e57a 100644 --- a/packages/@aws-cdk/aws-ec2/test/bastion-host.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/bastion-host.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import { Duration, Stack } from '@aws-cdk/core'; import { BastionHostLinux, BlockDeviceVolume, CloudFormationInit, InitCommand, InstanceClass, InstanceSize, InstanceType, SubnetType, Vpc } from '../lib'; @@ -15,7 +14,7 @@ describe('bastion host', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::Instance', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { InstanceType: 't3.nano', SubnetId: { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, }); @@ -40,7 +39,7 @@ describe('bastion host', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::Instance', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { InstanceType: 't3.nano', SubnetId: { Ref: 'VPCIsolatedSubnet1SubnetEBD00FC6' }, }); @@ -71,7 +70,7 @@ describe('bastion host', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::Instance', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { BlockDeviceMappings: [ { DeviceName: 'EBSBastionHost', @@ -96,7 +95,7 @@ describe('bastion host', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::Instance', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { ImageId: { Ref: 'SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -116,7 +115,7 @@ describe('bastion host', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::Instance', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { ImageId: { Ref: 'SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmarm64gp2C96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -142,7 +141,7 @@ describe('bastion host', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::Instance', { + Template.fromStack(stack).hasResource('AWS::EC2::Instance', { CreationPolicy: { ResourceSignal: { Timeout: 'PT30M', @@ -159,6 +158,27 @@ describe('bastion host', () => { }, }, }, - }, ResourcePart.CompleteDefinition); + }); + }); + + test('imdsv2 is required', () => { + //GIVEN + const stack = new Stack(); + const vpc = new Vpc(stack, 'VPC'); + + //WHEN + new BastionHostLinux(stack, 'Bastion', { + vpc, + requireImdsv2: true, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + MetadataOptions: { + HttpTokens: 'required', + }, + }, + }); }); }); diff --git a/packages/@aws-cdk/aws-ec2/test/cfn-init.test.ts b/packages/@aws-cdk/aws-ec2/test/cfn-init.test.ts index 2a9cce5e76719..919a3a8c07baa 100644 --- a/packages/@aws-cdk/aws-ec2/test/cfn-init.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/cfn-init.test.ts @@ -1,13 +1,13 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { arrayWith, ResourcePart, stringLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; import { Asset } from '@aws-cdk/aws-s3-assets'; import { AssetStaging, App, Aws, CfnResource, Stack, DefaultStackSynthesizer, IStackSynthesizer, FileAssetSource, FileAssetLocation } from '@aws-cdk/core'; import * as ec2 from '../lib'; +import { stringLike } from './util'; let app: App; let stack: Stack; @@ -305,7 +305,7 @@ const ASSET_STATEMENT = { 'arn:', { Ref: 'AWS::Partition' }, ':s3:::', - { Ref: stringLike('AssetParameter*S3Bucket*') }, + { Ref: stringLike(/AssetParameter.*S3Bucket.*/) }, ]], }, { @@ -313,7 +313,7 @@ const ASSET_STATEMENT = { 'arn:', { Ref: 'AWS::Partition' }, ':s3:::', - { Ref: stringLike('AssetParameter*S3Bucket*') }, + { Ref: stringLike(/AssetParameter.*S3Bucket.*/) }, '/*', ]], }, @@ -338,9 +338,9 @@ describe('assets n buckets', () => { init.attach(resource, linuxOptions()); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith(ASSET_STATEMENT), + Statement: Match.arrayWith([ASSET_STATEMENT]), Version: '2012-10-17', }, }); @@ -354,10 +354,10 @@ describe('assets n buckets', () => { 'https://s3.testregion.', { Ref: 'AWS::URLSuffix' }, '/', - { Ref: stringLike('AssetParameters*') }, + { Ref: stringLike(/AssetParameters.*/) }, '/', - { 'Fn::Select': [0, { 'Fn::Split': ['||', { Ref: stringLike('AssetParameters*') }] }] }, - { 'Fn::Select': [1, { 'Fn::Split': ['||', { Ref: stringLike('AssetParameters*') }] }] }, + { 'Fn::Select': [0, { 'Fn::Split': ['||', { Ref: stringLike(/AssetParameters.*/) }] }] }, + { 'Fn::Select': [1, { 'Fn::Split': ['||', { Ref: stringLike(/AssetParameters.*/) }] }] }, ]], }, }, @@ -369,7 +369,7 @@ describe('assets n buckets', () => { type: 'S3', roleName: { Ref: 'InstanceRole3CCE2F1D' }, buckets: [ - { Ref: stringLike('AssetParameters*S3Bucket*') }, + { Ref: stringLike(/AssetParameters.*S3Bucket.*/) }, ], }, }, @@ -392,9 +392,9 @@ describe('assets n buckets', () => { init.attach(resource, linuxOptions()); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith(ASSET_STATEMENT), + Statement: Match.arrayWith([ASSET_STATEMENT]), Version: '2012-10-17', }, }); @@ -407,10 +407,10 @@ describe('assets n buckets', () => { 'https://s3.testregion.', { Ref: 'AWS::URLSuffix' }, '/', - { Ref: stringLike('AssetParameters*') }, + { Ref: stringLike(/AssetParameters.*/) }, '/', - { 'Fn::Select': [0, { 'Fn::Split': ['||', { Ref: stringLike('AssetParameters*') }] }] }, - { 'Fn::Select': [1, { 'Fn::Split': ['||', { Ref: stringLike('AssetParameters*') }] }] }, + { 'Fn::Select': [0, { 'Fn::Split': ['||', { Ref: stringLike(/AssetParameters.*/) }] }] }, + { 'Fn::Select': [1, { 'Fn::Split': ['||', { Ref: stringLike(/AssetParameters.*/) }] }] }, ]], }, }, @@ -421,7 +421,7 @@ describe('assets n buckets', () => { type: 'S3', roleName: { Ref: 'InstanceRole3CCE2F1D' }, buckets: [ - { Ref: stringLike('AssetParameters*S3Bucket*') }, + { Ref: stringLike(/AssetParameters.*S3Bucket.*/) }, ], }, }, @@ -438,16 +438,16 @@ describe('assets n buckets', () => { init.attach(resource, linuxOptions()); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], Effect: 'Allow', Resource: [ { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':s3:::my-bucket']] }, { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':s3:::my-bucket/file.js']] }, ], - }), + }]), Version: '2012-10-17', }, }); @@ -481,16 +481,16 @@ describe('assets n buckets', () => { init.attach(resource, linuxOptions()); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], Effect: 'Allow', Resource: [ { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':s3:::my-bucket']] }, { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':s3:::my-bucket/file.zip']] }, ], - }), + }]), Version: '2012-10-17', }, }); @@ -528,9 +528,9 @@ describe('assets n buckets', () => { S3AccessCreds: { type: 'S3', roleName: { Ref: 'InstanceRole3CCE2F1D' }, - buckets: [ - { Ref: stringLike('AssetParameters*S3Bucket*') }, - ], + buckets: Match.arrayWith([ + { Ref: stringLike(/AssetParameters.*S3Bucket.*/) }, + ]), }, }, }); @@ -553,10 +553,10 @@ describe('assets n buckets', () => { S3AccessCreds: { type: 'S3', roleName: { Ref: 'InstanceRole3CCE2F1D' }, - buckets: arrayWith( - { Ref: stringLike('AssetParameters*S3Bucket*') }, + buckets: Match.arrayWith([ + { Ref: stringLike(/AssetParameters.*S3Bucket.*/) }, 'my-bucket', - ), + ]), }, }, }); @@ -632,9 +632,9 @@ function linuxOptions() { } function expectMetadataLike(pattern: any) { - expect(stack).toHaveResourceLike('CDK::Test::Resource', { + Template.fromStack(stack).hasResource('CDK::Test::Resource', { Metadata: pattern, - }, ResourcePart.CompleteDefinition); + }); } function expectLine(lines: string[], re: RegExp) { diff --git a/packages/@aws-cdk/aws-ec2/test/client-vpn-authorization-rule.test.ts b/packages/@aws-cdk/aws-ec2/test/client-vpn-authorization-rule.test.ts index 33ac16d355f15..45dc200f9c817 100644 --- a/packages/@aws-cdk/aws-ec2/test/client-vpn-authorization-rule.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/client-vpn-authorization-rule.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import { App, Stack } from '@aws-cdk/core'; import { Connections, IClientVpnEndpoint } from '../lib'; @@ -29,7 +29,7 @@ describe('ClientVpnAuthorizationRule constructor', () => { cidr: '10.0.10.0/32', clientVpnEndpoint, }); - expect(stack).toCountResources('AWS::EC2::ClientVpnAuthorizationRule', 1); + Template.fromStack(stack).resourceCountIs('AWS::EC2::ClientVpnAuthorizationRule', 1); expect(stack.node.children.length).toBe(1); }); test('either clientVpnEndoint (deprecated, typo) or clientVpnEndpoint is required', () => { @@ -97,7 +97,7 @@ describe('ClientVpnAuthorizationRule constructor', () => { cidr: '10.0.10.0/32', clientVpnEndoint, }); - expect(stack).toCountResources('AWS::EC2::ClientVpnAuthorizationRule', 1); + Template.fromStack(stack).resourceCountIs('AWS::EC2::ClientVpnAuthorizationRule', 1); expect(stack.node.children.length).toBe(1); }); }); diff --git a/packages/@aws-cdk/aws-ec2/test/client-vpn-endpoint.test.ts b/packages/@aws-cdk/aws-ec2/test/client-vpn-endpoint.test.ts index bd52982d76fd6..2b087d6da1f10 100644 --- a/packages/@aws-cdk/aws-ec2/test/client-vpn-endpoint.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/client-vpn-endpoint.test.ts @@ -1,5 +1,4 @@ -import { ResourcePart } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { SamlMetadataDocument, SamlProvider } from '@aws-cdk/aws-iam'; import * as logs from '@aws-cdk/aws-logs'; import { Stack } from '@aws-cdk/core'; @@ -27,7 +26,7 @@ test('client vpn endpoint', () => { userBasedAuthentication: ClientVpnUserBasedAuthentication.federated(samlProvider), }); - expect(stack).toHaveResource('AWS::EC2::ClientVpnEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::ClientVpnEndpoint', { AuthenticationOptions: [ { MutualAuthentication: { @@ -73,9 +72,9 @@ test('client vpn endpoint', () => { }, }); - expect(stack).toCountResources('AWS::EC2::ClientVpnTargetNetworkAssociation', 2); + Template.fromStack(stack).resourceCountIs('AWS::EC2::ClientVpnTargetNetworkAssociation', 2); - expect(stack).toHaveResource('AWS::EC2::ClientVpnTargetNetworkAssociation', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::ClientVpnTargetNetworkAssociation', { ClientVpnEndpointId: { Ref: 'VpcEndpoint6FF034F6', }, @@ -84,7 +83,7 @@ test('client vpn endpoint', () => { }, }); - expect(stack).toHaveResource('AWS::EC2::ClientVpnTargetNetworkAssociation', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::ClientVpnTargetNetworkAssociation', { ClientVpnEndpointId: { Ref: 'VpcEndpoint6FF034F6', }, @@ -93,9 +92,8 @@ test('client vpn endpoint', () => { }, }); - expect(stack).toHaveOutput({ - outputName: 'VpcEndpointSelfServicePortalUrl760AFE23', - outputValue: { + Template.fromStack(stack).hasOutput('VpcEndpointSelfServicePortalUrl760AFE23', { + Value: { 'Fn::Join': [ '', [ @@ -108,7 +106,7 @@ test('client vpn endpoint', () => { }, }); - expect(stack).toHaveResource('AWS::EC2::ClientVpnAuthorizationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::ClientVpnAuthorizationRule', { ClientVpnEndpointId: { Ref: 'VpcEndpoint6FF034F6', }, @@ -133,7 +131,7 @@ test('client vpn endpoint with custom security groups', () => { ], }); - expect(stack).toHaveResource('AWS::EC2::ClientVpnEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::ClientVpnEndpoint', { SecurityGroupIds: [ { 'Fn::GetAtt': [ @@ -163,7 +161,7 @@ test('client vpn endpoint with custom logging', () => { logStream: logGroup.addStream('LogStream'), }); - expect(stack).toHaveResource('AWS::EC2::ClientVpnEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::ClientVpnEndpoint', { ConnectionLogOptions: { CloudwatchLogGroup: { Ref: 'LogGroupF5B46931', @@ -184,7 +182,7 @@ test('client vpn endpoint with logging disabled', () => { logging: false, }); - expect(stack).toHaveResource('AWS::EC2::ClientVpnEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::ClientVpnEndpoint', { ConnectionLogOptions: { Enabled: false, }, @@ -204,9 +202,9 @@ test('client vpn endpoint with custom authorization rules', () => { groupId: 'group-id', }); - expect(stack).toCountResources('AWS::EC2::ClientVpnAuthorizationRule', 1); + Template.fromStack(stack).resourceCountIs('AWS::EC2::ClientVpnAuthorizationRule', 1); - expect(stack).toHaveResource('AWS::EC2::ClientVpnAuthorizationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::ClientVpnAuthorizationRule', { ClientVpnEndpointId: { Ref: 'VpcEndpoint6FF034F6', }, @@ -229,7 +227,7 @@ test('client vpn endpoint with custom route', () => { target: ec2.ClientVpnRouteTarget.local(), }); - expect(stack).toHaveResource('AWS::EC2::ClientVpnRoute', { + Template.fromStack(stack).hasResource('AWS::EC2::ClientVpnRoute', { Properties: { ClientVpnEndpointId: { Ref: 'VpcEndpoint6FF034F6', @@ -241,7 +239,36 @@ test('client vpn endpoint with custom route', () => { 'VpcEndpointAssociation06B066321', 'VpcEndpointAssociation12B51A67F', ], - }, ResourcePart.CompleteDefinition); + }); +}); + +test('client vpn endpoint with custom session timeout', () => { + vpc.addClientVpnEndpoint('Endpoint', { + cidr: '10.100.0.0/16', + serverCertificateArn: 'server-certificate-arn', + clientCertificateArn: 'client-certificate-arn', + sessionTimeout: ec2.ClientVpnSessionTimeout.TEN_HOURS, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::EC2::ClientVpnEndpoint', { + SessionTimeoutHours: 10, + }); +}); + +test('client vpn endpoint with client login banner', () => { + vpc.addClientVpnEndpoint('Endpoint', { + cidr: '10.100.0.0/16', + serverCertificateArn: 'server-certificate-arn', + clientCertificateArn: 'client-certificate-arn', + clientLoginBanner: 'Welcome!', + }); + + Template.fromStack(stack).hasResourceProperties('AWS::EC2::ClientVpnEndpoint', { + ClientLoginBannerOptions: { + Enabled: true, + BannerText: 'Welcome!', + }, + }); }); test('throws with more than 2 dns servers', () => { diff --git a/packages/@aws-cdk/aws-ec2/test/client-vpn-route.test.ts b/packages/@aws-cdk/aws-ec2/test/client-vpn-route.test.ts index 5bb1ee9722139..213c10f0b438c 100644 --- a/packages/@aws-cdk/aws-ec2/test/client-vpn-route.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/client-vpn-route.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { SamlMetadataDocument, SamlProvider } from '@aws-cdk/aws-iam'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import { App, Stack } from '@aws-cdk/core'; @@ -42,9 +42,9 @@ describe('ClientVpnRoute constructor', () => { cidr: '0.0.0.0/0', target: ClientVpnRouteTarget.local(), }); - expect(stack).toCountResources('AWS::EC2::VPC', 1); - expect(stack).toCountResources('AWS::EC2::ClientVpnEndpoint', 1); - expect(stack).toCountResources('AWS::EC2::ClientVpnRoute', 1); + Template.fromStack(stack).resourceCountIs('AWS::EC2::VPC', 1); + Template.fromStack(stack).resourceCountIs('AWS::EC2::ClientVpnEndpoint', 1); + Template.fromStack(stack).resourceCountIs('AWS::EC2::ClientVpnRoute', 1); expect(stack.node.children.length).toBe(3); }); testDeprecated('either clientVpnEndoint (deprecated, typo) or clientVpnEndpoint is required', () => { @@ -96,7 +96,7 @@ describe('ClientVpnRoute constructor', () => { target: ClientVpnRouteTarget.local(), }); }).toThrow(); - expect(stack).toCountResources('AWS::EC2::VPC', 1); + Template.fromStack(stack).resourceCountIs('AWS::EC2::VPC', 1); expect(stack.node.children.length).toBe(1); }); testDeprecated('supplying clientVpnEndoint (deprecated due to typo) should still work', () => { @@ -119,9 +119,9 @@ describe('ClientVpnRoute constructor', () => { cidr: '0.0.0.0/0', target: ClientVpnRouteTarget.local(), }); - expect(stack).toCountResources('AWS::EC2::VPC', 1); - expect(stack).toCountResources('AWS::EC2::ClientVpnEndpoint', 1); - expect(stack).toCountResources('AWS::EC2::ClientVpnRoute', 1); + Template.fromStack(stack).resourceCountIs('AWS::EC2::VPC', 1); + Template.fromStack(stack).resourceCountIs('AWS::EC2::ClientVpnEndpoint', 1); + Template.fromStack(stack).resourceCountIs('AWS::EC2::ClientVpnRoute', 1); expect(stack.node.children.length).toBe(3); }); }); diff --git a/packages/@aws-cdk/aws-ec2/test/connections.test.ts b/packages/@aws-cdk/aws-ec2/test/connections.test.ts index e5c48f2740f57..7853bebe58083 100644 --- a/packages/@aws-cdk/aws-ec2/test/connections.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/connections.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { App, Stack } from '@aws-cdk/core'; import { @@ -41,7 +41,7 @@ describe('connections', () => { somethingConnectable.connections.allowTo(securityGroup, Port.allTcp(), 'Connect there'); // THEN: rule to generated security group to connect to imported - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: { 'Fn::GetAtt': ['SomeSecurityGroupEF219AD6', 'GroupId'] }, IpProtocol: 'tcp', Description: 'Connect there', @@ -51,7 +51,7 @@ describe('connections', () => { }); // THEN: rule to imported security group to allow connections from generated - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { IpProtocol: 'tcp', Description: 'Connect there', FromPort: 0, @@ -76,7 +76,7 @@ describe('connections', () => { connections.addSecurityGroup(sg2); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/SecurityGroup1', SecurityGroupIngress: [ { @@ -89,7 +89,7 @@ describe('connections', () => { ], }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/SecurityGroup2', SecurityGroupIngress: [ { @@ -121,14 +121,14 @@ describe('connections', () => { connections2.addSecurityGroup(sg3); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { GroupId: { 'Fn::GetAtt': ['SecurityGroup23BE86BB7', 'GroupId'] }, SourceSecurityGroupId: { 'Fn::GetAtt': ['SecurityGroup1F554B36F', 'GroupId'] }, FromPort: 88, ToPort: 88, }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { GroupId: { 'Fn::GetAtt': ['SecurityGroup3E5E374B9', 'GroupId'] }, SourceSecurityGroupId: { 'Fn::GetAtt': ['SecurityGroup1F554B36F', 'GroupId'] }, FromPort: 88, @@ -151,14 +151,14 @@ describe('connections', () => { connections.addSecurityGroup(sg2); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { GroupId: { 'Fn::GetAtt': ['SecurityGroup1F554B36F', 'GroupId'] }, SourceSecurityGroupId: { 'Fn::GetAtt': ['SecurityGroup1F554B36F', 'GroupId'] }, FromPort: 88, ToPort: 88, }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { DestinationSecurityGroupId: { 'Fn::GetAtt': ['SecurityGroup1F554B36F', 'GroupId'] }, GroupId: { 'Fn::GetAtt': ['SecurityGroup1F554B36F', 'GroupId'] }, FromPort: 88, @@ -186,12 +186,12 @@ describe('connections', () => { // THEN -- both rules are in Stack2 app.synth(); - expect(stack2).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack2).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { GroupId: { 'Fn::GetAtt': ['SecurityGroupDD263621', 'GroupId'] }, SourceSecurityGroupId: { 'Fn::ImportValue': 'Stack1:ExportsOutputFnGetAttSecurityGroupDD263621GroupIdDF6F8B09' }, }); - expect(stack2).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack2).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: { 'Fn::ImportValue': 'Stack1:ExportsOutputFnGetAttSecurityGroupDD263621GroupIdDF6F8B09' }, DestinationSecurityGroupId: { 'Fn::GetAtt': ['SecurityGroupDD263621', 'GroupId'] }, }); @@ -217,12 +217,12 @@ describe('connections', () => { // THEN -- both rules are in Stack2 app.synth(); - expect(stack2).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack2).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { GroupId: { 'Fn::ImportValue': 'Stack1:ExportsOutputFnGetAttSecurityGroupDD263621GroupIdDF6F8B09' }, SourceSecurityGroupId: { 'Fn::GetAtt': ['SecurityGroupDD263621', 'GroupId'] }, }); - expect(stack2).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack2).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: { 'Fn::GetAtt': ['SecurityGroupDD263621', 'GroupId'] }, DestinationSecurityGroupId: { 'Fn::ImportValue': 'Stack1:ExportsOutputFnGetAttSecurityGroupDD263621GroupIdDF6F8B09' }, }); @@ -250,12 +250,12 @@ describe('connections', () => { // THEN -- both egress rules are in Stack2 app.synth(); - expect(stack2).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack2).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: { 'Fn::ImportValue': 'Stack1:ExportsOutputFnGetAttSecurityGroupAED40ADC5GroupId1D10C76A' }, DestinationSecurityGroupId: { 'Fn::GetAtt': ['SecurityGroupDD263621', 'GroupId'] }, }); - expect(stack2).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack2).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: { 'Fn::ImportValue': 'Stack1:ExportsOutputFnGetAttSecurityGroupB04591F90GroupIdFA7208D5' }, DestinationSecurityGroupId: { 'Fn::GetAtt': ['SecurityGroupDD263621', 'GroupId'] }, }); @@ -275,7 +275,7 @@ describe('connections', () => { somethingConnectable.connections.allowFrom(securityGroup, Port.allTcp(), 'Connect there'); // THEN: rule to generated security group to connect to imported - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { GroupId: { 'Fn::GetAtt': ['SomeSecurityGroupEF219AD6', 'GroupId'] }, IpProtocol: 'tcp', Description: 'Connect there', @@ -285,7 +285,7 @@ describe('connections', () => { }); // THEN: rule to imported security group to allow connections from generated - expect(stack).not.toHaveResource('AWS::EC2::SecurityGroupEgress'); + Template.fromStack(stack).resourceCountIs('AWS::EC2::SecurityGroupEgress', 0); }); @@ -304,7 +304,7 @@ describe('connections', () => { somethingConnectable.connections.allowFrom(securityGroup, Port.allTcp(), 'Connect there'); // THEN: rule to generated security group to connect to imported - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { GroupId: { 'Fn::GetAtt': ['SomeSecurityGroupEF219AD6', 'GroupId'] }, IpProtocol: 'tcp', Description: 'Connect there', @@ -314,7 +314,7 @@ describe('connections', () => { }); // THEN: rule to imported security group to allow connections from generated - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { IpProtocol: 'tcp', Description: 'Connect there', FromPort: 0, diff --git a/packages/@aws-cdk/aws-ec2/test/example.images.lit.ts b/packages/@aws-cdk/aws-ec2/test/example.images.lit.ts index 4fbff22fb86a4..2092e0d2fd87b 100644 --- a/packages/@aws-cdk/aws-ec2/test/example.images.lit.ts +++ b/packages/@aws-cdk/aws-ec2/test/example.images.lit.ts @@ -15,7 +15,7 @@ const amznLinux = ec2.MachineImage.latestAmazonLinux({ const windows = ec2.MachineImage.latestWindows(ec2.WindowsVersion.WINDOWS_SERVER_2019_ENGLISH_FULL_BASE); // Read AMI id from SSM parameter store -const ssm = ec2.MachineImage.fromSSMParameter('/my/ami', ec2.OperatingSystemType.LINUX); +const ssm = ec2.MachineImage.fromSsmParameter('/my/ami', { os: ec2.OperatingSystemType.LINUX }); // Look up the most recent image matching a set of AMI filters. // In this case, look up the NAT instance AMI, by using a wildcard diff --git a/packages/@aws-cdk/aws-ec2/test/instance.test.ts b/packages/@aws-cdk/aws-ec2/test/instance.test.ts index afd3570466eee..7f40cd8f202a0 100644 --- a/packages/@aws-cdk/aws-ec2/test/instance.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/instance.test.ts @@ -1,6 +1,6 @@ import * as path from 'path'; -import '@aws-cdk/assert-internal/jest'; -import { arrayWith, ResourcePart, stringLike, SynthUtils } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; +import { Key } from '@aws-cdk/aws-kms'; import { Asset } from '@aws-cdk/aws-s3-assets'; import { StringParameter } from '@aws-cdk/aws-ssm'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; @@ -10,7 +10,6 @@ import { EbsDeviceVolumeType, InitCommand, Instance, InstanceArchitecture, InstanceClass, InstanceSize, InstanceType, LaunchTemplate, UserData, Vpc, } from '../lib'; - let stack: Stack; let vpc: Vpc; beforeEach(() => { @@ -19,34 +18,6 @@ beforeEach(() => { }); describe('instance', () => { - test('instance is created correctly', () => { - // GIVEN - const sampleInstances = [{ - instanceClass: InstanceClass.BURSTABLE4_GRAVITON, - instanceSize: InstanceSize.LARGE, - instanceType: 't4g.large', - }, { - instanceClass: InstanceClass.HIGH_COMPUTE_MEMORY1, - instanceSize: InstanceSize.XLARGE3, - instanceType: 'z1d.3xlarge', - }]; - - for (const [i, sampleInstance] of sampleInstances.entries()) { - // WHEN - new Instance(stack, `Instance${i}`, { - vpc, - machineImage: new AmazonLinuxImage(), - instanceType: InstanceType.of(sampleInstance.instanceClass, sampleInstance.instanceSize), - }); - - // THEN - expect(stack).toHaveResource('AWS::EC2::Instance', { - InstanceType: sampleInstance.instanceType, - }); - } - - - }); test('instance is created with source/dest check switched off', () => { // WHEN new Instance(stack, 'Instance', { @@ -57,7 +28,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::Instance', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { InstanceType: 't3.large', SourceDestCheck: false, }); @@ -77,7 +48,7 @@ describe('instance', () => { param.grantRead(instance); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -207,13 +178,14 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::Instance', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { PropagateTagsToVolumeOnCreation: true, }); }); describe('blockDeviceMappings', () => { test('can set blockDeviceMappings', () => { // WHEN + const kmsKey = new Key(stack, 'EbsKey'); new Instance(stack, 'Instance', { vpc, machineImage: new AmazonLinuxImage(), @@ -227,6 +199,16 @@ describe('instance', () => { volumeType: EbsDeviceVolumeType.IO1, iops: 5000, }), + }, { + deviceName: 'ebs-cmk', + mappingEnabled: true, + volume: BlockDeviceVolume.ebs(15, { + deleteOnTermination: true, + encrypted: true, + kmsKey: kmsKey, + volumeType: EbsDeviceVolumeType.IO1, + iops: 5000, + }), }, { deviceName: 'ebs-snapshot', mappingEnabled: false, @@ -242,7 +224,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::Instance', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { BlockDeviceMappings: [ { DeviceName: 'ebs', @@ -254,6 +236,22 @@ describe('instance', () => { VolumeType: 'io1', }, }, + { + DeviceName: 'ebs-cmk', + Ebs: { + DeleteOnTermination: true, + Encrypted: true, + KmsKeyId: { + 'Fn::GetAtt': [ + 'EbsKeyD3FEE551', + 'Arn', + ], + }, + Iops: 5000, + VolumeSize: 15, + VolumeType: 'io1', + }, + }, { DeviceName: 'ebs-snapshot', Ebs: { @@ -368,7 +366,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::Instance', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { InstanceType: 't3.large', PrivateIpAddress: '10.0.0.2', }); @@ -386,12 +384,12 @@ describe('instance', () => { }); // Force stack synth so the InstanceRequireImdsv2Aspect is applied - SynthUtils.synthesize(stack); + Template.fromStack(stack); // THEN const launchTemplate = instance.node.tryFindChild('LaunchTemplate') as LaunchTemplate; expect(launchTemplate).toBeDefined(); - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateName: stack.resolve(launchTemplate.launchTemplateName), LaunchTemplateData: { MetadataOptions: { @@ -399,7 +397,7 @@ describe('instance', () => { }, }, }); - expect(stack).toHaveResourceLike('AWS::EC2::Instance', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { LaunchTemplate: { LaunchTemplateName: stack.resolve(launchTemplate.launchTemplateName), }, @@ -407,7 +405,6 @@ describe('instance', () => { }); }); - test('add CloudFormation Init to instance', () => { // GIVEN new Instance(stack, 'Instance', { @@ -420,11 +417,11 @@ test('add CloudFormation Init to instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::Instance', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { UserData: { 'Fn::Base64': { 'Fn::Join': ['', [ - stringLike('#!/bin/bash\n# fingerprint: *\n(\n set +e\n /opt/aws/bin/cfn-init -v --region '), + '#!/bin/bash\n# fingerprint: 85ac432b1de1144f\n(\n set +e\n /opt/aws/bin/cfn-init -v --region ', { Ref: 'AWS::Region' }, ' --stack ', { Ref: 'AWS::StackName' }, @@ -437,24 +434,24 @@ test('add CloudFormation Init to instance', () => { }, }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Action: ['cloudformation:DescribeStackResource', 'cloudformation:SignalResource'], Effect: 'Allow', Resource: { Ref: 'AWS::StackId' }, - }), + }]), Version: '2012-10-17', }, }); - expect(stack).toHaveResource('AWS::EC2::Instance', { + Template.fromStack(stack).hasResource('AWS::EC2::Instance', { CreationPolicy: { ResourceSignal: { Count: 1, Timeout: 'PT5M', }, }, - }, ResourcePart.CompleteDefinition); + }); }); test('cause replacement from s3 asset in userdata', () => { @@ -491,10 +488,10 @@ test('cause replacement from s3 asset in userdata', () => { // on the actual asset hash and not accidentally on the token stringification of them. // (which would base the hash on '${Token[1234.bla]}' const hash = 'f88eace39faf39d7'; - expect(SynthUtils.toCloudFormation(stack)).toEqual(expect.objectContaining({ - Resources: expect.objectContaining({ - [`InstanceOne5B821005${hash}`]: expect.objectContaining({ Type: 'AWS::EC2::Instance', Properties: expect.anything() }), - [`InstanceTwoDC29A7A7${hash}`]: expect.objectContaining({ Type: 'AWS::EC2::Instance', Properties: expect.anything() }), + Template.fromStack(stack).templateMatches(Match.objectLike({ + Resources: Match.objectLike({ + [`InstanceOne5B821005${hash}`]: Match.objectLike({ Type: 'AWS::EC2::Instance', Properties: Match.anyValue() }), + [`InstanceTwoDC29A7A7${hash}`]: Match.objectLike({ Type: 'AWS::EC2::Instance', Properties: Match.anyValue() }), }), })); }); diff --git a/packages/@aws-cdk/aws-ec2/test/launch-template.test.ts b/packages/@aws-cdk/aws-ec2/test/launch-template.test.ts index 6196a60541a12..49b979ca7084d 100644 --- a/packages/@aws-cdk/aws-ec2/test/launch-template.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/launch-template.test.ts @@ -1,12 +1,10 @@ -import '@aws-cdk/assert-internal/jest'; -import { - stringLike, -} from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import { CfnInstanceProfile, Role, ServicePrincipal, } from '@aws-cdk/aws-iam'; +import { Key } from '@aws-cdk/aws-kms'; import { App, Duration, @@ -32,6 +30,7 @@ import { WindowsImage, WindowsVersion, } from '../lib'; +import { stringLike } from './util'; /* eslint-disable jest/expect-expect */ @@ -52,7 +51,7 @@ describe('LaunchTemplate', () => { // Note: The following is intentionally a haveResource instead of haveResourceLike // to ensure that only the bare minimum of properties have values when no properties // are given to a LaunchTemplate. - expect(stack).toHaveResource('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { TagSpecifications: [ { @@ -76,7 +75,7 @@ describe('LaunchTemplate', () => { ], }, }); - expect(stack).not.toHaveResource('AWS::IAM::InstanceProfile'); + Template.fromStack(stack).resourceCountIs('AWS::IAM::InstanceProfile', 0); expect(() => { template.grantPrincipal; }).toThrow(); expect(() => { template.connections; }).toThrow(); expect(template.osType).toBeUndefined(); @@ -127,7 +126,7 @@ describe('LaunchTemplate', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateName: 'LTName', }); }); @@ -139,7 +138,7 @@ describe('LaunchTemplate', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { InstanceType: 'tt.test', }, @@ -153,10 +152,10 @@ describe('LaunchTemplate', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { ImageId: { - Ref: stringLike('SsmParameterValueawsserviceamiamazonlinuxlatestamznami*Parameter'), + Ref: stringLike('SsmParameterValueawsserviceamiamazonlinuxlatestamznami.*Parameter'), }, }, }); @@ -171,10 +170,10 @@ describe('LaunchTemplate', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { ImageId: { - Ref: stringLike('SsmParameterValueawsserviceamiwindowslatestWindowsServer2019EnglishFullBase*Parameter'), + Ref: stringLike('SsmParameterValueawsserviceamiwindowslatestWindowsServer2019EnglishFullBase.*Parameter'), }, }, }); @@ -193,7 +192,7 @@ describe('LaunchTemplate', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { UserData: { 'Fn::Base64': '#!/bin/bash\necho Test', @@ -215,15 +214,15 @@ describe('LaunchTemplate', () => { }); // THEN - expect(stack).toCountResources('AWS::IAM::Role', 1); - expect(stack).toHaveResourceLike('AWS::IAM::InstanceProfile', { + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::InstanceProfile', { Roles: [ { Ref: 'TestRole6C9272DF', }, ], }); - expect(stack).toHaveResource('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { IamInstanceProfile: { Arn: stack.resolve((template.node.findChild('Profile') as CfnInstanceProfile).getAtt('Arn')), @@ -256,6 +255,7 @@ describe('LaunchTemplate', () => { test('Given blockDeviceMapping', () => { // GIVEN + const kmsKey = new Key(stack, 'EbsKey'); const blockDevices: BlockDevice[] = [ { deviceName: 'ebs', @@ -266,6 +266,16 @@ describe('LaunchTemplate', () => { volumeType: EbsDeviceVolumeType.IO1, iops: 5000, }), + }, { + deviceName: 'ebs-cmk', + mappingEnabled: true, + volume: BlockDeviceVolume.ebs(15, { + deleteOnTermination: true, + encrypted: true, + kmsKey: kmsKey, + volumeType: EbsDeviceVolumeType.IO1, + iops: 5000, + }), }, { deviceName: 'ebs-snapshot', mappingEnabled: false, @@ -286,7 +296,7 @@ describe('LaunchTemplate', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { BlockDeviceMappings: [ { @@ -299,6 +309,22 @@ describe('LaunchTemplate', () => { VolumeType: 'io1', }, }, + { + DeviceName: 'ebs-cmk', + Ebs: { + DeleteOnTermination: true, + Encrypted: true, + KmsKeyId: { + 'Fn::GetAtt': [ + 'EbsKeyD3FEE551', + 'Arn', + ], + }, + Iops: 5000, + VolumeSize: 15, + VolumeType: 'io1', + }, + }, { DeviceName: 'ebs-snapshot', Ebs: { @@ -328,7 +354,7 @@ describe('LaunchTemplate', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { CreditSpecification: { CpuCredits: expected, @@ -347,7 +373,7 @@ describe('LaunchTemplate', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { DisableApiTermination: expected, }, @@ -364,7 +390,7 @@ describe('LaunchTemplate', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { EbsOptimized: expected, }, @@ -381,7 +407,7 @@ describe('LaunchTemplate', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { EnclaveOptions: { Enabled: expected, @@ -400,7 +426,7 @@ describe('LaunchTemplate', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { InstanceInitiatedShutdownBehavior: expected, }, @@ -414,7 +440,7 @@ describe('LaunchTemplate', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { KeyName: 'TestKeyname', }, @@ -431,7 +457,7 @@ describe('LaunchTemplate', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { Monitoring: { Enabled: expected, @@ -451,7 +477,7 @@ describe('LaunchTemplate', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { SecurityGroupIds: [ { @@ -476,7 +502,7 @@ describe('LaunchTemplate', () => { Tags.of(template).add('TestKey', 'TestValue'); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { TagSpecifications: [ { @@ -517,7 +543,7 @@ describe('LaunchTemplate', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { MetadataOptions: { HttpTokens: 'required', @@ -543,7 +569,7 @@ describe('LaunchTemplate marketOptions', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { InstanceMarketOptions: { MarketType: 'spot', @@ -589,7 +615,7 @@ describe('LaunchTemplate marketOptions', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { InstanceMarketOptions: { MarketType: 'spot', @@ -614,7 +640,7 @@ describe('LaunchTemplate marketOptions', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { InstanceMarketOptions: { MarketType: 'spot', @@ -639,7 +665,7 @@ describe('LaunchTemplate marketOptions', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { InstanceMarketOptions: { MarketType: 'spot', @@ -663,7 +689,7 @@ describe('LaunchTemplate marketOptions', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { InstanceMarketOptions: { MarketType: 'spot', @@ -684,7 +710,7 @@ describe('LaunchTemplate marketOptions', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { LaunchTemplateData: { InstanceMarketOptions: { MarketType: 'spot', diff --git a/packages/@aws-cdk/aws-ec2/test/machine-image.test.ts b/packages/@aws-cdk/aws-ec2/test/machine-image.test.ts index a25acdfbb4cfa..bb6b86921101a 100644 --- a/packages/@aws-cdk/aws-ec2/test/machine-image.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/machine-image.test.ts @@ -1,4 +1,4 @@ -import { expect as cdkExpect, matchTemplate, MatchStyle } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import { App, Stack } from '@aws-cdk/core'; import * as ec2 from '../lib'; @@ -44,7 +44,7 @@ test('can make and use a Linux image in agnostic stack', () => { }, }; - cdkExpect(stack).to(matchTemplate(expected, MatchStyle.EXACT)); + Template.fromStack(stack).templateMatches(expected); expect(stack.resolve(details.imageId)).toEqual({ 'Fn::FindInMap': ['AmiMap', { Ref: 'AWS::Region' }, 'ami'] }); expect(details.osType).toEqual(ec2.OperatingSystemType.LINUX); }); @@ -81,7 +81,7 @@ test('can make and use a Windows image in agnostic stack', () => { }, }; - cdkExpect(stack).to(matchTemplate(expected, MatchStyle.EXACT)); + Template.fromStack(stack).templateMatches(expected); expect(stack.resolve(details.imageId)).toEqual({ 'Fn::FindInMap': ['AmiMap', { Ref: 'AWS::Region' }, 'ami'] }); expect(details.osType).toEqual(ec2.OperatingSystemType.WINDOWS); }); @@ -177,10 +177,97 @@ test('cached lookups of Amazon Linux', () => { ]); }); +test('cached lookups of Amazon Linux 2', () => { + // WHEN + const ami = ec2.MachineImage.latestAmazonLinux({ + cachedInContext: true, + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, + }).getImage(stack).imageId; + + // THEN + expect(ami).toEqual('dummy-value-for-/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2'); + expect(app.synth().manifest.missing).toEqual([ + { + key: 'ssm:account=1234:parameterName=/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2:region=testregion', + props: { + account: '1234', + region: 'testregion', + parameterName: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2', + }, + provider: 'ssm', + }, + ]); +}); + +test('cached lookups of Amazon Linux 2 with kernel 5.x', () => { + // WHEN + const ami = ec2.MachineImage.latestAmazonLinux({ + cachedInContext: true, + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, + kernel: ec2.AmazonLinuxKernel.KERNEL5_X, + }).getImage(stack).imageId; + + // THEN + expect(ami).toEqual('dummy-value-for-/aws/service/ami-amazon-linux-latest/amzn2-ami-kernel-5.10-hvm-x86_64-gp2'); + expect(app.synth().manifest.missing).toEqual([ + { + key: 'ssm:account=1234:parameterName=/aws/service/ami-amazon-linux-latest/amzn2-ami-kernel-5.10-hvm-x86_64-gp2:region=testregion', + props: { + account: '1234', + region: 'testregion', + parameterName: '/aws/service/ami-amazon-linux-latest/amzn2-ami-kernel-5.10-hvm-x86_64-gp2', + }, + provider: 'ssm', + }, + ]); +}); + +test('throw error if storage param is set for Amazon Linux 2022', () => { + expect(() => { + ec2.MachineImage.latestAmazonLinux({ + cachedInContext: true, + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2022, + storage: ec2.AmazonLinuxStorage.GENERAL_PURPOSE, + }).getImage(stack).imageId; + }).toThrow(/Storage parameter does not exist in smm parameter name for Amazon Linux 2022./); +}); + +test('throw error if virtualization param is set for Amazon Linux 2022', () => { + expect(() => { + ec2.MachineImage.latestAmazonLinux({ + cachedInContext: true, + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2022, + virtualization: ec2.AmazonLinuxVirt.HVM, + }).getImage(stack).imageId; + }).toThrow(/Virtualization parameter does not exist in smm parameter name for Amazon Linux 2022./); +}); + +test('cached lookups of Amazon Linux 2022 with kernel 5.x', () => { + // WHEN + const ami = ec2.MachineImage.latestAmazonLinux({ + cachedInContext: true, + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2022, + }).getImage(stack).imageId; + + // THEN + expect(ami).toEqual('dummy-value-for-/aws/service/ami-amazon-linux-latest/al2022-ami-kernel-5.10-x86_64'); + expect(app.synth().manifest.missing).toEqual([ + { + key: 'ssm:account=1234:parameterName=/aws/service/ami-amazon-linux-latest/al2022-ami-kernel-5.10-x86_64:region=testregion', + props: { + account: '1234', + region: 'testregion', + parameterName: '/aws/service/ami-amazon-linux-latest/al2022-ami-kernel-5.10-x86_64', + }, + provider: 'ssm', + }, + ]); +}); + function isWindowsUserData(ud: ec2.UserData) { return ud.render().indexOf('powershell') > -1; } function isLinuxUserData(ud: ec2.UserData) { return ud.render().indexOf('bash') > -1; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ec2/test/security-group.test.ts b/packages/@aws-cdk/aws-ec2/test/security-group.test.ts index c7c3662f8592f..4624d2a648b72 100644 --- a/packages/@aws-cdk/aws-ec2/test/security-group.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/security-group.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import { App, Intrinsic, Lazy, Stack, Token } from '@aws-cdk/core'; import { Peer, Port, SecurityGroup, SecurityGroupProps, Vpc } from '../lib'; @@ -15,7 +15,7 @@ describe('security group', () => { new SecurityGroup(stack, 'SG1', { vpc, allowAllOutbound: true }); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { SecurityGroupEgress: [ { CidrIp: '0.0.0.0/0', @@ -38,7 +38,7 @@ describe('security group', () => { sg.addEgressRule(Peer.anyIpv4(), Port.tcp(86), 'This does not show up'); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { SecurityGroupEgress: [ { CidrIp: '0.0.0.0/0', @@ -60,7 +60,7 @@ describe('security group', () => { new SecurityGroup(stack, 'SG1', { vpc, allowAllOutbound: false }); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { SecurityGroupEgress: [ { CidrIp: '255.255.255.255/32', @@ -85,7 +85,7 @@ describe('security group', () => { sg.addEgressRule(Peer.anyIpv4(), Port.tcp(86), 'This replaces the other one'); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { SecurityGroupEgress: [ { CidrIp: '0.0.0.0/0', @@ -123,7 +123,7 @@ describe('security group', () => { sg.addEgressRule(Peer.anyIpv4(), Port.tcp(86), 'This rule was not added'); sg.addIngressRule(Peer.anyIpv4(), Port.tcp(86), 'This rule was not added'); - expect(stack).not.toHaveResource('AWS::EC2::SecurityGroup', { + const openEgressRules = Template.fromStack(stack).findResources('AWS::EC2::SecurityGroup', { SecurityGroupEgress: [ { CidrIp: '0.0.0.0/0', @@ -134,8 +134,9 @@ describe('security group', () => { }, ], }); + expect(Object.keys(openEgressRules).length).toBe(0); - expect(stack).not.toHaveResource('AWS::EC2::SecurityGroup', { + const openIngressRules = Template.fromStack(stack).findResources('AWS::EC2::SecurityGroup', { SecurityGroupIngress: [ { CidrIp: '0.0.0.0/0', @@ -146,8 +147,7 @@ describe('security group', () => { }, ], }); - - + expect(Object.keys(openIngressRules).length).toBe(0); }); describe('Inline Rule Control', () => { @@ -178,6 +178,7 @@ describe('security group', () => { Peer.anyIpv4(), Peer.anyIpv6(), Peer.prefixList('pl-012345'), + Peer.securityGroupId('sg-012345678'), ]; const ports = [ @@ -225,10 +226,10 @@ describe('security group', () => { sg.addIngressRule(peer2, Port.tcp(5432), 'Rule 2'); // THEN -- no crash - expect(stack).toHaveResourceLike('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { Description: 'Rule 1', }); - expect(stack).toHaveResourceLike('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { Description: 'Rule 2', }); }); @@ -337,6 +338,125 @@ describe('security group', () => { }); }); + describe('Peer security group ID validation', () => { + test('passes with valid security group ID', () => { + //GIVEN + const securityGroupIds = ['sg-12345678', 'sg-0123456789abcdefg']; + + // THEN + for (const securityGroupId of securityGroupIds) { + expect(Peer.securityGroupId(securityGroupId).uniqueId).toEqual(securityGroupId); + } + }); + + test('passes with valid security group ID and source owner id', () => { + //GIVEN + const securityGroupIds = ['sg-12345678', 'sg-0123456789abcdefg']; + const ownerIds = ['000000000000', '000000000001']; + + // THEN + for (const securityGroupId of securityGroupIds) { + for (const ownerId of ownerIds) { + expect(Peer.securityGroupId(securityGroupId, ownerId).uniqueId).toEqual(securityGroupId); + } + } + }); + + test('passes with unresolved security group id token or owner id token', () => { + // GIVEN + Token.asString('securityGroupId'); + + const securityGroupId = Lazy.string({ produce: () => 'sg-01234567' }); + const ownerId = Lazy.string({ produce: () => '000000000000' }); + Peer.securityGroupId(securityGroupId); + Peer.securityGroupId(securityGroupId, ownerId); + + // THEN: don't throw + }); + + test('throws if invalid security group ID', () => { + // THEN + expect(() => { + Peer.securityGroupId('invalid'); + }).toThrow(/Invalid security group ID/); + + + }); + + test('throws if invalid source security group id', () => { + // THEN + expect(() => { + Peer.securityGroupId('sg-12345678', 'invalid'); + }).toThrow(/Invalid security group owner ID/); + }); + }); + + describe('SourceSecurityGroupOwnerId property validation', () => { + test('SourceSecurityGroupOwnerId property is not present when value is not provided to ingress rule', () => { + // GIVEN + const stack = new Stack(undefined, 'TestStack'); + const vpc = new Vpc(stack, 'VPC'); + const sg = new SecurityGroup(stack, 'SG', { vpc }); + + //WHEN + sg.addIngressRule(Peer.securityGroupId('sg-123456789'), Port.allTcp(), 'no owner id property'); + + //THEN + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { + SecurityGroupIngress: [{ + SourceSecurityGroupId: 'sg-123456789', + Description: 'no owner id property', + FromPort: 0, + ToPort: 65535, + IpProtocol: 'tcp', + }], + }); + }); + + test('SourceSecurityGroupOwnerId property is present when value is provided to ingress rule', () => { + // GIVEN + const stack = new Stack(undefined, 'TestStack'); + const vpc = new Vpc(stack, 'VPC'); + const sg = new SecurityGroup(stack, 'SG', { vpc }); + + //WHEN + sg.addIngressRule(Peer.securityGroupId('sg-123456789', '000000000000'), Port.allTcp(), 'contains owner id property'); + + //THEN + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { + SecurityGroupIngress: [{ + SourceSecurityGroupId: 'sg-123456789', + SourceSecurityGroupOwnerId: '000000000000', + Description: 'contains owner id property', + FromPort: 0, + ToPort: 65535, + IpProtocol: 'tcp', + }], + }); + }); + + test('SourceSecurityGroupOwnerId property is not present when value is provided to egress rule', () => { + // GIVEN + const stack = new Stack(undefined, 'TestStack'); + const vpc = new Vpc(stack, 'VPC'); + const sg = new SecurityGroup(stack, 'SG', { vpc, allowAllOutbound: false }); + + //WHEN + sg.addEgressRule(Peer.securityGroupId('sg-123456789', '000000000000'), Port.allTcp(), 'no owner id property'); + + //THEN + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { + SecurityGroupEgress: [{ + DestinationSecurityGroupId: 'sg-123456789', + Description: 'no owner id property', + FromPort: 0, + ToPort: 65535, + IpProtocol: 'tcp', + }], + }); + }); + }); + testDeprecated('can look up a security group', () => { const app = new App(); const stack = new Stack(app, 'stack', { @@ -491,7 +611,7 @@ function testRulesAreInlined(contextDisableInlineRules: boolean | undefined | nu // WHEN new SecurityGroup(stack, 'SG1', props); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/SG1', VpcId: stack.resolve(vpc.vpcId), SecurityGroupEgress: [ @@ -502,8 +622,8 @@ function testRulesAreInlined(contextDisableInlineRules: boolean | undefined | nu }, ], }); - expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroupEgress', {}); - expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroupIngress', {}); + Template.fromStack(stack).resourceCountIs('AWS::EC2::SecurityGroupEgress', 0); + Template.fromStack(stack).resourceCountIs('AWS::EC2::SecurityGroupIngress', 0); }); @@ -518,7 +638,7 @@ function testRulesAreInlined(contextDisableInlineRules: boolean | undefined | nu const sg = new SecurityGroup(stack, 'SG1', props); sg.addEgressRule(Peer.anyIpv4(), Port.tcp(86), 'An external Rule'); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/SG1', VpcId: stack.resolve(vpc.vpcId), SecurityGroupEgress: [ @@ -530,8 +650,8 @@ function testRulesAreInlined(contextDisableInlineRules: boolean | undefined | nu ], }); - expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroupEgress', {}); - expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroupIngress', {}); + Template.fromStack(stack).resourceCountIs('AWS::EC2::SecurityGroupEgress', 0); + Template.fromStack(stack).resourceCountIs('AWS::EC2::SecurityGroupIngress', 0); }); @@ -546,7 +666,7 @@ function testRulesAreInlined(contextDisableInlineRules: boolean | undefined | nu const sg = new SecurityGroup(stack, 'SG1', props); sg.addIngressRule(Peer.anyIpv4(), Port.tcp(86), 'An external Rule'); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/SG1', VpcId: stack.resolve(vpc.vpcId), SecurityGroupIngress: [ @@ -581,7 +701,7 @@ function testRulesAreInlined(contextDisableInlineRules: boolean | undefined | nu // WHEN new SecurityGroup(stack, 'SG1', props); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/SG1', VpcId: stack.resolve(vpc.vpcId), SecurityGroupEgress: [ @@ -594,8 +714,8 @@ function testRulesAreInlined(contextDisableInlineRules: boolean | undefined | nu }, ], }); - expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroupIngress', {}); - expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroupIngress', {}); + Template.fromStack(stack).resourceCountIs('AWS::EC2::SecurityGroupIngress', 0); + Template.fromStack(stack).resourceCountIs('AWS::EC2::SecurityGroupIngress', 0); }); @@ -610,7 +730,7 @@ function testRulesAreInlined(contextDisableInlineRules: boolean | undefined | nu const sg = new SecurityGroup(stack, 'SG1', props); sg.addEgressRule(Peer.anyIpv4(), Port.tcp(86), 'An inline Rule'); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/SG1', VpcId: stack.resolve(vpc.vpcId), SecurityGroupEgress: [ @@ -624,8 +744,8 @@ function testRulesAreInlined(contextDisableInlineRules: boolean | undefined | nu ], }); - expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroupEgress', {}); - expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroupIngress', {}); + Template.fromStack(stack).resourceCountIs('AWS::EC2::SecurityGroupEgress', 0); + Template.fromStack(stack).resourceCountIs('AWS::EC2::SecurityGroupIngress', 0); }); @@ -640,7 +760,7 @@ function testRulesAreInlined(contextDisableInlineRules: boolean | undefined | nu const sg = new SecurityGroup(stack, 'SG1', props); sg.addIngressRule(Peer.anyIpv4(), Port.tcp(86), 'An external Rule'); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/SG1', VpcId: stack.resolve(vpc.vpcId), SecurityGroupIngress: [ @@ -663,8 +783,8 @@ function testRulesAreInlined(contextDisableInlineRules: boolean | undefined | nu ], }); - expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroupEgress', {}); - expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroupIngress', {}); + Template.fromStack(stack).resourceCountIs('AWS::EC2::SecurityGroupEgress', 0); + Template.fromStack(stack).resourceCountIs('AWS::EC2::SecurityGroupIngress', 0); }); }); @@ -685,17 +805,17 @@ function testRulesAreNotInlined(contextDisableInlineRules: boolean | undefined | // WHEN const sg = new SecurityGroup(stack, 'SG1', props); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/SG1', VpcId: stack.resolve(vpc.vpcId), }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: stack.resolve(sg.securityGroupId), CidrIp: '0.0.0.0/0', Description: 'Allow all outbound traffic by default', IpProtocol: '-1', }); - expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroupIngress', {}); + Template.fromStack(stack).resourceCountIs('AWS::EC2::SecurityGroupIngress', 0); }); @@ -710,19 +830,19 @@ function testRulesAreNotInlined(contextDisableInlineRules: boolean | undefined | const sg = new SecurityGroup(stack, 'SG1', props); sg.addEgressRule(Peer.anyIpv4(), Port.tcp(86), 'An external Rule'); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/SG1', VpcId: stack.resolve(vpc.vpcId), }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: stack.resolve(sg.securityGroupId), CidrIp: '0.0.0.0/0', Description: 'Allow all outbound traffic by default', IpProtocol: '-1', }); - expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroupIngress', {}); + Template.fromStack(stack).resourceCountIs('AWS::EC2::SecurityGroupIngress', 0); }); @@ -737,18 +857,18 @@ function testRulesAreNotInlined(contextDisableInlineRules: boolean | undefined | const sg = new SecurityGroup(stack, 'SG1', props); sg.addEgressRule(Peer.anyIpv4(), Port.tcp(86), 'An external Rule'); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/SG1', VpcId: stack.resolve(vpc.vpcId), }); - expect(stack).not.toHaveResource('AWS::EC2::SecurityGroupEgress', { + const egressGroups = Template.fromStack(stack).findResources('AWS::EC2::SecurityGroupEgress', { GroupId: stack.resolve(sg.securityGroupId), Description: 'An external Rule', }); + expect(Object.keys(egressGroups).length).toBe(0); - expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroupIngress', {}); - + Template.fromStack(stack).resourceCountIs('AWS::EC2::SecurityGroupIngress', 0); }); test('addIngressRule rule will add a new external ingress rule even if it could have been inlined', () => { @@ -762,12 +882,12 @@ function testRulesAreNotInlined(contextDisableInlineRules: boolean | undefined | const sg = new SecurityGroup(stack, 'SG1', props); sg.addIngressRule(Peer.anyIpv4(), Port.tcp(86), 'An external Rule'); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/SG1', VpcId: stack.resolve(vpc.vpcId), }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { GroupId: stack.resolve(sg.securityGroupId), CidrIp: '0.0.0.0/0', Description: 'An external Rule', @@ -776,7 +896,7 @@ function testRulesAreNotInlined(contextDisableInlineRules: boolean | undefined | ToPort: 86, }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: stack.resolve(sg.securityGroupId), CidrIp: '0.0.0.0/0', Description: 'Allow all outbound traffic by default', @@ -797,12 +917,12 @@ function testRulesAreNotInlined(contextDisableInlineRules: boolean | undefined | // WHEN const sg = new SecurityGroup(stack, 'SG1', props); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/SG1', VpcId: stack.resolve(vpc.vpcId), }); - expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroupIngress', {}); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).resourceCountIs('AWS::EC2::SecurityGroupIngress', 0); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: stack.resolve(sg.securityGroupId), CidrIp: '255.255.255.255/32', Description: 'Disallow all traffic', @@ -824,16 +944,16 @@ function testRulesAreNotInlined(contextDisableInlineRules: boolean | undefined | const sg = new SecurityGroup(stack, 'SG1', props); sg.addEgressRule(Peer.anyIpv4(), Port.tcp(86), 'An external Rule'); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/SG1', VpcId: stack.resolve(vpc.vpcId), }); - expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroupIngress', {}); - expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).resourceCountIs('AWS::EC2::SecurityGroupIngress', 0); + const egressGroups = Template.fromStack(stack).findResources('AWS::EC2::SecurityGroupEgress', { GroupId: stack.resolve(sg.securityGroupId), CidrIp: '255.255.255.255/32', }); - + expect(Object.keys(egressGroups).length).toBe(0); }); test('addEgressRule rule will add a new external egress rule even if it could have been inlined', () => { @@ -847,12 +967,12 @@ function testRulesAreNotInlined(contextDisableInlineRules: boolean | undefined | const sg = new SecurityGroup(stack, 'SG1', props); sg.addEgressRule(Peer.anyIpv4(), Port.tcp(86), 'An external Rule'); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/SG1', VpcId: stack.resolve(vpc.vpcId), }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: stack.resolve(sg.securityGroupId), CidrIp: '0.0.0.0/0', Description: 'An external Rule', @@ -861,7 +981,7 @@ function testRulesAreNotInlined(contextDisableInlineRules: boolean | undefined | ToPort: 86, }); - expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroupIngress', {}); + Template.fromStack(stack).resourceCountIs('AWS::EC2::SecurityGroupIngress', 0); }); @@ -876,12 +996,12 @@ function testRulesAreNotInlined(contextDisableInlineRules: boolean | undefined | const sg = new SecurityGroup(stack, 'SG1', props); sg.addIngressRule(Peer.anyIpv4(), Port.tcp(86), 'An external Rule'); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/SG1', VpcId: stack.resolve(vpc.vpcId), }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { GroupId: stack.resolve(sg.securityGroupId), CidrIp: '0.0.0.0/0', Description: 'An external Rule', @@ -890,7 +1010,7 @@ function testRulesAreNotInlined(contextDisableInlineRules: boolean | undefined | ToPort: 86, }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: stack.resolve(sg.securityGroupId), CidrIp: '255.255.255.255/32', Description: 'Disallow all traffic', @@ -902,4 +1022,4 @@ function testRulesAreNotInlined(contextDisableInlineRules: boolean | undefined | }); }); -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ec2/test/userdata.test.ts b/packages/@aws-cdk/aws-ec2/test/userdata.test.ts index e2596d8699abd..c385ce83a7254 100644 --- a/packages/@aws-cdk/aws-ec2/test/userdata.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/userdata.test.ts @@ -1,5 +1,6 @@ import { Bucket } from '@aws-cdk/aws-s3'; -import { Aws, Stack } from '@aws-cdk/core'; +import { Template, Match } from '@aws-cdk/assertions'; +import { Aws, Stack, CfnResource } from '@aws-cdk/core'; import * as ec2 from '../lib'; describe('user data', () => { @@ -41,6 +42,7 @@ describe('user data', () => { const stack = new Stack(); const resource = new ec2.Vpc(stack, 'RESOURCE'); const userData = ec2.UserData.forWindows(); + const logicalId = (resource.node.defaultChild as CfnResource).logicalId; // WHEN userData.addSignalOnExitCommand( resource ); @@ -49,9 +51,10 @@ describe('user data', () => { // THEN const rendered = userData.render(); + expect(stack.resolve(logicalId)).toEqual('RESOURCE1989552F'); expect(rendered).toEqual('trap {\n' + '$success=($PSItem.Exception.Message -eq "Success")\n' + - `cfn-signal --stack Default --resource RESOURCE1989552F --region ${Aws.REGION} --success ($success.ToString().ToLower())\n` + + `cfn-signal --stack Default --resource ${logicalId} --region ${Aws.REGION} --success ($success.ToString().ToLower())\n` + 'break\n' + '}\n' + 'command1\n' + @@ -59,6 +62,44 @@ describe('user data', () => { ); }); + test('can create Windows with Signal Command and userDataCausesReplacement', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc'); + const userData = ec2.UserData.forWindows(); + const resource = new ec2.Instance(stack, 'RESOURCE', { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.LARGE), + machineImage: ec2.MachineImage.genericWindows({ ['us-east-1']: 'ami-12345678' }), + userDataCausesReplacement: true, + userData, + }); + + const logicalId = (resource.node.defaultChild as CfnResource).logicalId; + + // WHEN + userData.addSignalOnExitCommand( resource ); + userData.addCommands('command1'); + + // THEN + Template.fromStack(stack).templateMatches({ + Resources: Match.objectLike({ + RESOURCE1989552Fdfd505305f427919: { + Type: 'AWS::EC2::Instance', + }, + }), + }); + expect(stack.resolve(logicalId)).toEqual('RESOURCE1989552Fdfd505305f427919'); + const rendered = userData.render(); + expect(rendered).toEqual('trap {\n' + + '$success=($PSItem.Exception.Message -eq "Success")\n' + + `cfn-signal --stack Default --resource ${logicalId} --region ${Aws.REGION} --success ($success.ToString().ToLower())\n` + + 'break\n' + + '}\n' + + 'command1\n' + + 'throw "Success"', + ); + }); test('can windows userdata download S3 files', () => { // GIVEN const stack = new Stack(); @@ -174,6 +215,7 @@ describe('user data', () => { // GIVEN const stack = new Stack(); const resource = new ec2.Vpc(stack, 'RESOURCE'); + const logicalId = (resource.node.defaultChild as CfnResource).logicalId; // WHEN const userData = ec2.UserData.forLinux(); @@ -182,15 +224,53 @@ describe('user data', () => { // THEN const rendered = userData.render(); + expect(stack.resolve(logicalId)).toEqual('RESOURCE1989552F'); expect(rendered).toEqual('#!/bin/bash\n' + 'function exitTrap(){\n' + 'exitCode=$?\n' + - `/opt/aws/bin/cfn-signal --stack Default --resource RESOURCE1989552F --region ${Aws.REGION} -e $exitCode || echo \'Failed to send Cloudformation Signal\'\n` + + `/opt/aws/bin/cfn-signal --stack Default --resource ${logicalId} --region ${Aws.REGION} -e $exitCode || echo \'Failed to send Cloudformation Signal\'\n` + '}\n' + 'trap exitTrap EXIT\n' + 'command1'); }); + test('can create Linux with Signal Command and userDataCausesReplacement', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc'); + const userData = ec2.UserData.forLinux(); + const resource = new ec2.Instance(stack, 'RESOURCE', { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.LARGE), + machineImage: ec2.MachineImage.genericLinux({ ['us-east-1']: 'ami-12345678' }), + userDataCausesReplacement: true, + userData, + }); + + const logicalId = (resource.node.defaultChild as CfnResource).logicalId; + + // WHEN + userData.addSignalOnExitCommand( resource ); + userData.addCommands('command1'); + + // THEN + Template.fromStack(stack).templateMatches({ + Resources: Match.objectLike({ + RESOURCE1989552F74a24ef4fbc89422: { + Type: 'AWS::EC2::Instance', + }, + }), + }); + expect(stack.resolve(logicalId)).toEqual('RESOURCE1989552F74a24ef4fbc89422'); + const rendered = userData.render(); + expect(rendered).toEqual('#!/bin/bash\n' + + 'function exitTrap(){\n' + + 'exitCode=$?\n' + + `/opt/aws/bin/cfn-signal --stack Default --resource ${logicalId} --region ${Aws.REGION} -e $exitCode || echo \'Failed to send Cloudformation Signal\'\n` + + '}\n' + + 'trap exitTrap EXIT\n' + + 'command1'); + }); test('can linux userdata download S3 files', () => { // GIVEN const stack = new Stack(); diff --git a/packages/@aws-cdk/aws-ec2/test/util.ts b/packages/@aws-cdk/aws-ec2/test/util.ts new file mode 100644 index 0000000000000..9eabad6658a2a --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/util.ts @@ -0,0 +1,20 @@ +import { Matcher, MatchResult } from '@aws-cdk/assertions'; + +export function stringLike(pattern: string | RegExp): Matcher { + return new RegexMatcher(new RegExp(pattern)); +} + +export class RegexMatcher extends Matcher { + constructor(private readonly pattern: RegExp, public readonly name: string = 'RegexMatch') { super(); } + public test(actual: any): MatchResult { + const result = new MatchResult(actual); + if (!this.pattern.test(actual)) { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected ${actual} to match ${this.pattern}`, + }); + } + return result; + } +} diff --git a/packages/@aws-cdk/aws-ec2/test/volume.test.ts b/packages/@aws-cdk/aws-ec2/test/volume.test.ts index 4ac7874c701d2..5e7d3591cd877 100644 --- a/packages/@aws-cdk/aws-ec2/test/volume.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/volume.test.ts @@ -1,16 +1,12 @@ -import '@aws-cdk/assert-internal/jest'; -import { - arrayWith, - ResourcePart, -} from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import { AccountRootPrincipal, Role, } from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; +import { testFutureBehavior, testLegacyBehavior } from '@aws-cdk/cdk-build-tools/lib/feature-flag'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import { testFutureBehavior, testLegacyBehavior } from '@aws-cdk/cdk-build-tools/lib/feature-flag'; import { AmazonLinuxGeneration, EbsDeviceVolumeType, @@ -34,7 +30,7 @@ describe('volume', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::Volume', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Volume', { AvailabilityZone: 'us-east-1a', MultiAttachEnabled: false, Size: 8, @@ -45,11 +41,11 @@ describe('volume', () => { Value: 'MyVolume', }, ], - }, ResourcePart.Properties); + }); - expect(stack).toHaveResource('AWS::EC2::Volume', { + Template.fromStack(stack).hasResource('AWS::EC2::Volume', { DeletionPolicy: 'Retain', - }, ResourcePart.CompleteDefinition); + }); }); test('fromVolumeAttributes', () => { @@ -85,7 +81,7 @@ describe('volume', () => { cdk.Tags.of(volume).add('TagKey', 'TagValue'); // THEN - expect(stack).toHaveResource('AWS::EC2::Volume', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Volume', { AvailabilityZone: 'us-east-1a', MultiAttachEnabled: false, Size: 8, @@ -94,7 +90,7 @@ describe('volume', () => { Key: 'TagKey', Value: 'TagValue', }], - }, ResourcePart.Properties); + }); }); @@ -111,9 +107,9 @@ describe('volume', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::Volume', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Volume', { AutoEnableIO: true, - }, ResourcePart.Properties); + }); }); @@ -130,9 +126,9 @@ describe('volume', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::Volume', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Volume', { Encrypted: true, - }, ResourcePart.Properties); + }); }); @@ -151,7 +147,7 @@ describe('volume', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::Volume', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Volume', { Encrypted: true, KmsKeyId: { 'Fn::GetAtt': [ @@ -159,57 +155,54 @@ describe('volume', () => { 'Arn', ], }, - }, ResourcePart.Properties); - expect(stack).toHaveResourceLike('AWS::KMS::Key', { + }); + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { - Statement: [ - {}, - { - Effect: 'Allow', - Principal: { - AWS: { + Statement: Match.arrayWith([{ + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + Resource: '*', + Action: [ + 'kms:DescribeKey', + 'kms:GenerateDataKeyWithoutPlainText', + ], + Condition: { + StringEquals: { + 'kms:ViaService': { 'Fn::Join': [ '', [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':iam::', + 'ec2.', { - Ref: 'AWS::AccountId', + Ref: 'AWS::Region', }, - ':root', + '.amazonaws.com', ], ], }, - }, - Resource: '*', - Action: [ - 'kms:DescribeKey', - 'kms:GenerateDataKeyWithoutPlainText', - ], - Condition: { - StringEquals: { - 'kms:ViaService': { - 'Fn::Join': [ - '', - [ - 'ec2.', - { - Ref: 'AWS::Region', - }, - '.amazonaws.com', - ], - ], - }, - 'kms:CallerAccount': { - Ref: 'AWS::AccountId', - }, + 'kms:CallerAccount': { + Ref: 'AWS::AccountId', }, }, }, - ], + }]), }, }); @@ -233,18 +226,15 @@ describe('volume', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { - Statement: [ - {}, - { - Action: [ - 'kms:DescribeKey', - 'kms:GenerateDataKeyWithoutPlainText', - 'kms:ReEncrypt*', - ], - }, - ], + Statement: Match.arrayWith([Match.objectLike({ + Action: [ + 'kms:DescribeKey', + 'kms:GenerateDataKeyWithoutPlainText', + 'kms:ReEncrypt*', + ], + })]), }, }); @@ -264,10 +254,10 @@ describe('volume', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::Volume', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Volume', { Iops: 500, VolumeType: 'io1', - }, ResourcePart.Properties); + }); }); @@ -286,9 +276,9 @@ describe('volume', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::Volume', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Volume', { MultiAttachEnabled: true, - }, ResourcePart.Properties); + }); }); @@ -304,9 +294,9 @@ describe('volume', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::Volume', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Volume', { SnapshotId: 'snap-00000000', - }, ResourcePart.Properties); + }); }); @@ -323,9 +313,9 @@ describe('volume', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::Volume', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Volume', { VolumeType: 'standard', - }, ResourcePart.Properties); + }); }); @@ -343,9 +333,9 @@ describe('volume', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::Volume', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Volume', { VolumeType: 'io1', - }, ResourcePart.Properties); + }); }); @@ -363,9 +353,9 @@ describe('volume', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::Volume', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Volume', { VolumeType: 'io2', - }, ResourcePart.Properties); + }); }); @@ -382,9 +372,9 @@ describe('volume', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::Volume', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Volume', { VolumeType: 'gp2', - }, ResourcePart.Properties); + }); }); @@ -401,9 +391,9 @@ describe('volume', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::Volume', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Volume', { VolumeType: 'gp3', - }, ResourcePart.Properties); + }); }); @@ -420,9 +410,9 @@ describe('volume', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::Volume', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Volume', { VolumeType: 'st1', - }, ResourcePart.Properties); + }); }); @@ -439,9 +429,9 @@ describe('volume', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::Volume', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Volume', { VolumeType: 'sc1', - }, ResourcePart.Properties); + }); }); @@ -459,7 +449,7 @@ describe('volume', () => { volume.grantAttachVolume(role); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [{ @@ -536,45 +526,41 @@ describe('volume', () => { volume.grantAttachVolume(role); // THEN - expect(stack).toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { - Statement: [ - {}, - {}, - { - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::GetAtt': [ - 'Role1ABCC5F0', - 'Arn', - ], - }, + Statement: Match.arrayWith([{ + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::GetAtt': [ + 'Role1ABCC5F0', + 'Arn', + ], }, - Action: 'kms:CreateGrant', - Condition: { - Bool: { - 'kms:GrantIsForAWSResource': true, - }, - StringEquals: { - 'kms:ViaService': { - 'Fn::Join': [ - '', - [ - 'ec2.', - { - Ref: 'AWS::Region', - }, - '.amazonaws.com', - ], + }, + Action: 'kms:CreateGrant', + Condition: { + Bool: { + 'kms:GrantIsForAWSResource': true, + }, + StringEquals: { + 'kms:ViaService': { + 'Fn::Join': [ + '', + [ + 'ec2.', + { + Ref: 'AWS::Region', + }, + '.amazonaws.com', ], - }, - 'kms:GrantConstraintType': 'EncryptionContextSubset', + ], }, + 'kms:GrantConstraintType': 'EncryptionContextSubset', }, - Resource: '*', }, - ], + Resource: '*', + }]), }, }); @@ -597,9 +583,9 @@ describe('volume', () => { volume.grantAttachVolume(role); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Effect: 'Allow', Action: 'kms:CreateGrant', Condition: { @@ -628,7 +614,7 @@ describe('volume', () => { 'Arn', ], }, - }), + }]), }, }); @@ -670,42 +656,39 @@ describe('volume', () => { volume.grantAttachVolume(role); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', - Statement: [ - {}, - { - Effect: 'Allow', - Action: 'kms:CreateGrant', - Resource: { - 'Fn::GetAtt': [ - 'Key961B73FD', - 'Arn', - ], + Statement: Match.arrayWith([{ + Effect: 'Allow', + Action: 'kms:CreateGrant', + Resource: { + 'Fn::GetAtt': [ + 'Key961B73FD', + 'Arn', + ], + }, + Condition: { + Bool: { + 'kms:GrantIsForAWSResource': true, }, - Condition: { - Bool: { - 'kms:GrantIsForAWSResource': true, - }, - StringEquals: { - 'kms:ViaService': { - 'Fn::Join': [ - '', - [ - 'ec2.', - { - Ref: 'AWS::Region', - }, - '.amazonaws.com', - ], + StringEquals: { + 'kms:ViaService': { + 'Fn::Join': [ + '', + [ + 'ec2.', + { + Ref: 'AWS::Region', + }, + '.amazonaws.com', ], - }, - 'kms:GrantConstraintType': 'EncryptionContextSubset', + ], }, + 'kms:GrantConstraintType': 'EncryptionContextSubset', }, }, - ], + }]), }, }); @@ -738,61 +721,58 @@ describe('volume', () => { volume.grantAttachVolume(role, [instance1, instance2]); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [{ Action: 'ec2:AttachVolume', Effect: 'Allow', - Resource: [ - {}, - { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':ec2:', - { - Ref: 'AWS::Region', - }, - ':', - { - Ref: 'AWS::AccountId', - }, - ':instance/', - { - Ref: 'Instance14BC3991D', - }, - ], + Resource: Match.arrayWith([{ + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ec2:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':instance/', + { + Ref: 'Instance14BC3991D', + }, ], - }, - { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':ec2:', - { - Ref: 'AWS::Region', - }, - ':', - { - Ref: 'AWS::AccountId', - }, - ':instance/', - { - Ref: 'Instance255F35265', - }, - ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ec2:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':instance/', + { + Ref: 'Instance255F35265', + }, ], - }, - ], + ], + }]), }], }, }); @@ -819,35 +799,32 @@ describe('volume', () => { volume.grantAttachVolumeByResourceTag(instance.grantPrincipal, [instance]); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [{ Action: 'ec2:AttachVolume', Effect: 'Allow', - Resource: [ - {}, - { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':ec2:', - { - Ref: 'AWS::Region', - }, - ':', - { - Ref: 'AWS::AccountId', - }, - ':instance/*', - ], + Resource: Match.arrayWith([{ + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ec2:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':instance/*', ], - }, - ], + ], + }]), Condition: { 'ForAnyValue:StringEquals': { 'ec2:ResourceTag/VolumeGrantAttach-B2376B2BDA': 'b2376b2bda65cb40f83c290dd844c4aa', @@ -856,25 +833,20 @@ describe('volume', () => { }], }, }); - expect(stack).toHaveResourceLike('AWS::EC2::Volume', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Volume', { Tags: [ { Key: 'VolumeGrantAttach-B2376B2BDA', Value: 'b2376b2bda65cb40f83c290dd844c4aa', }, ], - }, ResourcePart.Properties); - expect(stack).toHaveResourceLike('AWS::EC2::Instance', { - Tags: [ - {}, - { - Key: 'VolumeGrantAttach-B2376B2BDA', - Value: 'b2376b2bda65cb40f83c290dd844c4aa', - }, - ], - }, ResourcePart.Properties); - - + }); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { + Tags: Match.arrayWith([{ + Key: 'VolumeGrantAttach-B2376B2BDA', + Value: 'b2376b2bda65cb40f83c290dd844c4aa', + }]), + }); }); test('grantAttachVolume to instance self with suffix', () => { @@ -896,35 +868,32 @@ describe('volume', () => { volume.grantAttachVolumeByResourceTag(instance.grantPrincipal, [instance], 'TestSuffix'); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [{ Action: 'ec2:AttachVolume', Effect: 'Allow', - Resource: [ - {}, - { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':ec2:', - { - Ref: 'AWS::Region', - }, - ':', - { - Ref: 'AWS::AccountId', - }, - ':instance/*', - ], + Resource: Match.arrayWith([{ + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ec2:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':instance/*', ], - }, - ], + ], + }]), Condition: { 'ForAnyValue:StringEquals': { 'ec2:ResourceTag/VolumeGrantAttach-TestSuffix': 'b2376b2bda65cb40f83c290dd844c4aa', @@ -933,23 +902,20 @@ describe('volume', () => { }], }, }); - expect(stack).toHaveResourceLike('AWS::EC2::Volume', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Volume', { Tags: [ { Key: 'VolumeGrantAttach-TestSuffix', Value: 'b2376b2bda65cb40f83c290dd844c4aa', }, ], - }, ResourcePart.Properties); - expect(stack).toHaveResourceLike('AWS::EC2::Instance', { - Tags: [ - {}, - { - Key: 'VolumeGrantAttach-TestSuffix', - Value: 'b2376b2bda65cb40f83c290dd844c4aa', - }, - ], - }, ResourcePart.Properties); + }); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { + Tags: Match.arrayWith([{ + Key: 'VolumeGrantAttach-TestSuffix', + Value: 'b2376b2bda65cb40f83c290dd844c4aa', + }]), + }); }); @@ -966,7 +932,7 @@ describe('volume', () => { volume.grantDetachVolume(role); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [{ @@ -1049,61 +1015,58 @@ describe('volume', () => { volume.grantDetachVolume(role, [instance1, instance2]); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [{ Action: 'ec2:DetachVolume', Effect: 'Allow', - Resource: [ - {}, - { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':ec2:', - { - Ref: 'AWS::Region', - }, - ':', - { - Ref: 'AWS::AccountId', - }, - ':instance/', - { - Ref: 'Instance14BC3991D', - }, - ], + Resource: Match.arrayWith([{ + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ec2:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':instance/', + { + Ref: 'Instance14BC3991D', + }, ], - }, - { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':ec2:', - { - Ref: 'AWS::Region', - }, - ':', - { - Ref: 'AWS::AccountId', - }, - ':instance/', - { - Ref: 'Instance255F35265', - }, - ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ec2:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':instance/', + { + Ref: 'Instance255F35265', + }, ], - }, - ], + ], + }]), }], }, }); @@ -1130,35 +1093,32 @@ describe('volume', () => { volume.grantDetachVolumeByResourceTag(instance.grantPrincipal, [instance]); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [{ Action: 'ec2:DetachVolume', Effect: 'Allow', - Resource: [ - {}, - { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':ec2:', - { - Ref: 'AWS::Region', - }, - ':', - { - Ref: 'AWS::AccountId', - }, - ':instance/*', - ], + Resource: Match.arrayWith([{ + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ec2:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':instance/*', ], - }, - ], + ], + }]), Condition: { 'ForAnyValue:StringEquals': { 'ec2:ResourceTag/VolumeGrantDetach-B2376B2BDA': 'b2376b2bda65cb40f83c290dd844c4aa', @@ -1167,25 +1127,20 @@ describe('volume', () => { }], }, }); - expect(stack).toHaveResourceLike('AWS::EC2::Volume', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Volume', { Tags: [ { Key: 'VolumeGrantDetach-B2376B2BDA', Value: 'b2376b2bda65cb40f83c290dd844c4aa', }, ], - }, ResourcePart.Properties); - expect(stack).toHaveResourceLike('AWS::EC2::Instance', { - Tags: [ - {}, - { - Key: 'VolumeGrantDetach-B2376B2BDA', - Value: 'b2376b2bda65cb40f83c290dd844c4aa', - }, - ], - }, ResourcePart.Properties); - - + }); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { + Tags: Match.arrayWith([{ + Key: 'VolumeGrantDetach-B2376B2BDA', + Value: 'b2376b2bda65cb40f83c290dd844c4aa', + }]), + }); }); test('grantDetachVolume from instance self with suffix', () => { @@ -1207,14 +1162,13 @@ describe('volume', () => { volume.grantDetachVolumeByResourceTag(instance.grantPrincipal, [instance], 'TestSuffix'); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [{ Action: 'ec2:DetachVolume', Effect: 'Allow', - Resource: [ - {}, + Resource: Match.arrayWith([ { 'Fn::Join': [ '', @@ -1235,7 +1189,7 @@ describe('volume', () => { ], ], }, - ], + ]), Condition: { 'ForAnyValue:StringEquals': { 'ec2:ResourceTag/VolumeGrantDetach-TestSuffix': 'b2376b2bda65cb40f83c290dd844c4aa', @@ -1244,23 +1198,20 @@ describe('volume', () => { }], }, }); - expect(stack).toHaveResourceLike('AWS::EC2::Volume', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Volume', { Tags: [ { Key: 'VolumeGrantDetach-TestSuffix', Value: 'b2376b2bda65cb40f83c290dd844c4aa', }, ], - }, ResourcePart.Properties); - expect(stack).toHaveResourceLike('AWS::EC2::Instance', { - Tags: [ - {}, - { - Key: 'VolumeGrantDetach-TestSuffix', - Value: 'b2376b2bda65cb40f83c290dd844c4aa', - }, - ], - }, ResourcePart.Properties); + }); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { + Tags: Match.arrayWith([{ + Key: 'VolumeGrantDetach-TestSuffix', + Value: 'b2376b2bda65cb40f83c290dd844c4aa', + }]), + }); }); diff --git a/packages/@aws-cdk/aws-ec2/test/vpc-endpoint-service.test.ts b/packages/@aws-cdk/aws-ec2/test/vpc-endpoint-service.test.ts index 2b00460ec793b..3a1eb7edee100 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc-endpoint-service.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc-endpoint-service.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { ArnPrincipal } from '@aws-cdk/aws-iam'; import { Stack } from '@aws-cdk/core'; @@ -33,18 +33,18 @@ describe('vpc endpoint service', () => { allowedPrincipals: [new ArnPrincipal('arn:aws:iam::123456789012:root')], }); // THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpointService', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpointService', { NetworkLoadBalancerArns: ['arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/Test/9bn6qkf4e9jrw77a'], AcceptanceRequired: false, }); - expect(stack).not.toHaveResource('AWS::EC2::VPCEndpointServicePermissions', { + const servicePermissions = Template.fromStack(stack).findResources('AWS::EC2::VPCEndpointServicePermissions', { ServiceId: { Ref: 'EndpointServiceED36BE1F', }, AllowedPrincipals: [], }); - + expect(Object.keys(servicePermissions).length).toBe(0); }); test('create endpoint service with a principal', () => { @@ -60,12 +60,12 @@ describe('vpc endpoint service', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpointService', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpointService', { NetworkLoadBalancerArns: ['arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/Test/9bn6qkf4e9jrw77a'], AcceptanceRequired: false, }); - expect(stack).toHaveResource('AWS::EC2::VPCEndpointServicePermissions', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpointServicePermissions', { ServiceId: { Ref: 'EndpointServiceED36BE1F', }, @@ -88,12 +88,12 @@ describe('vpc endpoint service', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpointService', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpointService', { NetworkLoadBalancerArns: ['arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/Test/9bn6qkf4e9jrw77a'], AcceptanceRequired: true, }); - expect(stack).toHaveResource('AWS::EC2::VPCEndpointServicePermissions', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpointServicePermissions', { ServiceId: { Ref: 'EndpointServiceED36BE1F', }, diff --git a/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts b/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts index c5724e91c2bcc..9baa7c9e7e437 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts @@ -1,9 +1,9 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import { AnyPrincipal, PolicyStatement } from '@aws-cdk/aws-iam'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { ContextProvider, Fn, Stack } from '@aws-cdk/core'; // eslint-disable-next-line max-len -import { GatewayVpcEndpoint, GatewayVpcEndpointAwsService, InterfaceVpcEndpoint, InterfaceVpcEndpointAwsService, InterfaceVpcEndpointService, SecurityGroup, SubnetType, Vpc } from '../lib'; +import { GatewayVpcEndpoint, GatewayVpcEndpointAwsService, InterfaceVpcEndpoint, InterfaceVpcEndpointAwsService, InterfaceVpcEndpointService, SecurityGroup, SubnetFilter, SubnetType, Vpc } from '../lib'; describe('vpc endpoint', () => { describe('gateway endpoint', () => { @@ -21,7 +21,7 @@ describe('vpc endpoint', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { ServiceName: { 'Fn::Join': [ '', @@ -71,7 +71,7 @@ describe('vpc endpoint', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { ServiceName: { 'Fn::Join': [ '', @@ -123,7 +123,7 @@ describe('vpc endpoint', () => { })); // THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { PolicyDocument: { Statement: [ { @@ -185,7 +185,7 @@ describe('vpc endpoint', () => { // THEN vpc.addGatewayEndpoint('Gateway', { service: GatewayVpcEndpointAwsService.S3 }); - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { ServiceName: { 'Fn::Join': ['', ['com.amazonaws.', { Ref: 'AWS::Region' }, '.s3']] }, VpcId: 'id', RouteTableIds: ['rt1', 'rt2', 'rt3'], @@ -222,7 +222,7 @@ describe('vpc endpoint', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { ServiceName: { 'Fn::Join': [ '', @@ -258,7 +258,7 @@ describe('vpc endpoint', () => { VpcEndpointType: 'Interface', }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/VpcNetwork/EcrDocker/SecurityGroup', VpcId: { Ref: 'VpcNetworkB258E83A', @@ -268,6 +268,25 @@ describe('vpc endpoint', () => { }); + describe('add interface endpoint to looked-up VPC', () => { + test('initial run', () => { + // GIVEN + const stack = new Stack(undefined, undefined, { env: { account: '1234', region: 'us-east-1' } }); + const vpc = Vpc.fromLookup(stack, 'Vpc', { + vpcId: 'doesnt-matter', + }); + + // THEN: doesn't throw + vpc.addInterfaceEndpoint('SecretsManagerEndpoint', { + service: InterfaceVpcEndpointAwsService.SECRETS_MANAGER, + subnets: { + subnetFilters: [SubnetFilter.byIds(['1234'])], + }, + }); + }); + }); + + test('import/export', () => { // GIVEN const stack2 = new Stack(); @@ -281,7 +300,7 @@ describe('vpc endpoint', () => { importedEndpoint.connections.allowDefaultPortFromAnyIpv4(); // THEN - expect(stack2).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack2).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { GroupId: 'security-group-id', }); expect(importedEndpoint.vpcEndpointId).toEqual('vpc-endpoint-id'); @@ -319,7 +338,7 @@ describe('vpc endpoint', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { SecurityGroupIds: ['existing-id'], }); @@ -337,7 +356,7 @@ describe('vpc endpoint', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { SecurityGroupIds: ['existing-id'], }); @@ -354,14 +373,14 @@ describe('vpc endpoint', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { SecurityGroupIngress: [ - { + Match.objectLike({ CidrIp: { 'Fn::GetAtt': ['VpcNetworkB258E83A', 'CidrBlock'] }, FromPort: 443, IpProtocol: 'tcp', ToPort: 443, - }, + }), ], }); @@ -378,7 +397,7 @@ describe('vpc endpoint', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { ServiceName: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', PrivateDnsEnabled: false, }); @@ -400,7 +419,7 @@ describe('vpc endpoint', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { ServiceName: 'com.amazonaws.vpce.us-east-1.vpce-svc-mktplacesvcwprdns', PrivateDnsEnabled: true, }); @@ -439,7 +458,7 @@ describe('vpc endpoint', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { ServiceName: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', SubnetIds: [ { @@ -476,7 +495,7 @@ describe('vpc endpoint', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { ServiceName: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', SubnetIds: [ { @@ -522,7 +541,7 @@ describe('vpc endpoint', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { ServiceName: 'com.amazonaws.us-east-1.execute-api', SubnetIds: [ { @@ -617,7 +636,7 @@ describe('vpc endpoint', () => { }); //THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { ServiceName: 'cn.com.amazonaws.cn-north-1.ecr.api', }); @@ -634,7 +653,7 @@ describe('vpc endpoint', () => { }); //THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { ServiceName: 'cn.com.amazonaws.cn-northwest-1.lambda', }); @@ -651,7 +670,7 @@ describe('vpc endpoint', () => { }); //THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { ServiceName: 'com.amazonaws.cn-north-1.ecs', }); @@ -668,7 +687,7 @@ describe('vpc endpoint', () => { }); //THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { ServiceName: 'com.amazonaws.cn-northwest-1.glue', }); @@ -685,7 +704,7 @@ describe('vpc endpoint', () => { }); //THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { ServiceName: 'com.amazonaws.us-east-1.transcribe', }); @@ -702,7 +721,7 @@ describe('vpc endpoint', () => { }); //THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { ServiceName: 'cn.com.amazonaws.cn-north-1.transcribe.cn', }); @@ -719,7 +738,7 @@ describe('vpc endpoint', () => { }); //THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { ServiceName: 'cn.com.amazonaws.cn-northwest-1.transcribe.cn', }); diff --git a/packages/@aws-cdk/aws-ec2/test/vpc-flow-logs.test.ts b/packages/@aws-cdk/aws-ec2/test/vpc-flow-logs.test.ts index 956632bf1a0a5..88e9cc8705f8e 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc-flow-logs.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc-flow-logs.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; @@ -13,23 +13,21 @@ describe('vpc flow logs', () => { resourceType: FlowLogResourceType.fromNetworkInterfaceId('eni-123455'), }); - expect(stack). - toHaveResource('AWS::EC2::FlowLog', { - ResourceType: 'NetworkInterface', - TrafficType: 'ALL', - ResourceId: 'eni-123455', - DeliverLogsPermissionArn: { - 'Fn::GetAtt': ['FlowLogsIAMRoleF18F4209', 'Arn'], - }, - LogGroupName: { - Ref: 'FlowLogsLogGroup9853A85F', - }, + Template.fromStack(stack).hasResourceProperties('AWS::EC2::FlowLog', { + ResourceType: 'NetworkInterface', + TrafficType: 'ALL', + ResourceId: 'eni-123455', + DeliverLogsPermissionArn: { + 'Fn::GetAtt': ['FlowLogsIAMRoleF18F4209', 'Arn'], }, - ); + LogGroupName: { + Ref: 'FlowLogsLogGroup9853A85F', + }, + }); - expect(stack).toCountResources('AWS::Logs::LogGroup', 1); - expect(stack).toCountResources('AWS::IAM::Role', 1); - expect(stack).not.toHaveResource('AWS::S3::Bucket'); + Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 1); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 0); }); test('with cloudwatch logs as the destination, allows use of existing resources', () => { @@ -48,13 +46,13 @@ describe('vpc flow logs', () => { ), }); - expect(stack).toHaveResource('AWS::Logs::LogGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogGroup', { RetentionInDays: 5, }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { RoleName: 'TestName', }); - expect(stack).not.toHaveResource('AWS::S3::Bucket'); + Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 0); }); test('with s3 as the destination, allows use of existing resources', () => { @@ -69,9 +67,9 @@ describe('vpc flow logs', () => { ), }); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); - expect(stack).not.toHaveResource('AWS::IAM::Role'); - expect(stack).toHaveResource('AWS::S3::Bucket', { + Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 0); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 0); + Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { BucketName: 'testbucket', }); @@ -89,9 +87,9 @@ describe('vpc flow logs', () => { ), }); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); - expect(stack).not.toHaveResource('AWS::IAM::Role'); - expect(stack).toHaveResource('AWS::S3::Bucket', { + Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 0); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 0); + Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { BucketName: 'testbucket', }); @@ -104,19 +102,17 @@ describe('vpc flow logs', () => { destination: FlowLogDestination.toS3(), }); - expect(stack). - toHaveResource('AWS::EC2::FlowLog', { - ResourceType: 'NetworkInterface', - TrafficType: 'ALL', - ResourceId: 'eni-123456', - LogDestination: { - 'Fn::GetAtt': ['FlowLogsBucket87F67F60', 'Arn'], - }, + Template.fromStack(stack).hasResourceProperties('AWS::EC2::FlowLog', { + ResourceType: 'NetworkInterface', + TrafficType: 'ALL', + ResourceId: 'eni-123456', + LogDestination: { + 'Fn::GetAtt': ['FlowLogsBucket87F67F60', 'Arn'], }, - ); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); - expect(stack).not.toHaveResource('AWS::IAM::Role'); - expect(stack).toCountResources('AWS::S3::Bucket', 1); + }); + Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 0); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 0); + Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 1); }); test('create with vpc', () => { @@ -128,22 +124,20 @@ describe('vpc flow logs', () => { }, }); - expect(stack).toHaveResource('AWS::EC2::VPC'); - expect(stack). - toHaveResource('AWS::EC2::FlowLog', { - ResourceType: 'VPC', - TrafficType: 'ALL', - ResourceId: { - Ref: 'VPCB9E5F0B4', - }, - DeliverLogsPermissionArn: { - 'Fn::GetAtt': ['VPCflowLogsIAMRole9D21E1A6', 'Arn'], - }, - LogGroupName: { - Ref: 'VPCflowLogsLogGroupE900F980', - }, + Template.fromStack(stack).resourceCountIs('AWS::EC2::VPC', 1); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::FlowLog', { + ResourceType: 'VPC', + TrafficType: 'ALL', + ResourceId: { + Ref: 'VPCB9E5F0B4', + }, + DeliverLogsPermissionArn: { + 'Fn::GetAtt': ['VPCflowLogsIAMRole9D21E1A6', 'Arn'], + }, + LogGroupName: { + Ref: 'VPCflowLogsLogGroupE900F980', }, - ); + }); }); test('add to vpc', () => { @@ -152,23 +146,20 @@ describe('vpc flow logs', () => { const vpc = new Vpc(stack, 'VPC'); vpc.addFlowLog('FlowLogs'); - expect(stack).toHaveResource('AWS::EC2::VPC'); - expect(stack). - toHaveResource('AWS::EC2::FlowLog', { - ResourceType: 'VPC', - TrafficType: 'ALL', - ResourceId: { - Ref: 'VPCB9E5F0B4', - }, - DeliverLogsPermissionArn: { - 'Fn::GetAtt': ['VPCFlowLogsIAMRole55343234', 'Arn'], - }, - LogGroupName: { - Ref: 'VPCFlowLogsLogGroupF48E1B0A', - }, + Template.fromStack(stack).resourceCountIs('AWS::EC2::VPC', 1); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::FlowLog', { + ResourceType: 'VPC', + TrafficType: 'ALL', + ResourceId: { + Ref: 'VPCB9E5F0B4', }, - ); - + DeliverLogsPermissionArn: { + 'Fn::GetAtt': ['VPCFlowLogsIAMRole55343234', 'Arn'], + }, + LogGroupName: { + Ref: 'VPCFlowLogsLogGroupF48E1B0A', + }, + }); }); }); diff --git a/packages/@aws-cdk/aws-ec2/test/vpc.test.ts b/packages/@aws-cdk/aws-ec2/test/vpc.test.ts index 1ef3e8094a5e7..3c9d54d25bf4f 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { isSuperObject, MatchStyle, SynthUtils } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import { CfnOutput, CfnResource, Fn, Lazy, Stack, Tags } from '@aws-cdk/core'; import { @@ -48,7 +47,7 @@ describe('vpc', () => { test('it uses the correct network range', () => { const stack = getTestStack(); new Vpc(stack, 'TheVPC'); - expect(stack).toHaveResource('AWS::EC2::VPC', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPC', { CidrBlock: Vpc.DEFAULT_CIDR_RANGE, EnableDnsHostnames: true, EnableDnsSupport: true, @@ -59,15 +58,12 @@ describe('vpc', () => { test('the Name tag is defaulted to path', () => { const stack = getTestStack(); new Vpc(stack, 'TheVPC'); - expect(stack). - toHaveResource('AWS::EC2::VPC', - hasTags([{ Key: 'Name', Value: 'TestStack/TheVPC' }]), - ); - expect(stack). - toHaveResource('AWS::EC2::InternetGateway', - hasTags([{ Key: 'Name', Value: 'TestStack/TheVPC' }]), - ); - + Template.fromStack(stack).hasResource('AWS::EC2::VPC', + hasTags([{ Key: 'Name', Value: 'TestStack/TheVPC' }]), + ); + Template.fromStack(stack).hasResource('AWS::EC2::InternetGateway', + hasTags([{ Key: 'Name', Value: 'TestStack/TheVPC' }]), + ); }); }); @@ -81,7 +77,7 @@ describe('vpc', () => { defaultInstanceTenancy: DefaultInstanceTenancy.DEDICATED, }); - expect(stack).toHaveResource('AWS::EC2::VPC', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPC', { CidrBlock: '192.168.0.0/16', EnableDnsHostnames: false, EnableDnsSupport: false, @@ -112,7 +108,7 @@ describe('vpc', () => { defaultInstanceTenancy: DefaultInstanceTenancy.DEDICATED, }); - expect(stack).toHaveResource('AWS::EC2::VPC', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPC', { CidrBlock: '192.168.0.0/16', EnableDnsHostnames: input.dnsHostnames, EnableDnsSupport: input.dnsSupport, @@ -156,9 +152,9 @@ describe('vpc', () => { }, ], }); - expect(stack).not.toHaveResource('AWS::EC2::InternetGateway'); - expect(stack).not.toHaveResource('AWS::EC2::NatGateway'); - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).resourceCountIs('AWS::EC2::InternetGateway', 0); + Template.fromStack(stack).resourceCountIs('AWS::EC2::NatGateway', 0); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { MapPublicIpOnLaunch: false, }); @@ -178,8 +174,8 @@ describe('vpc', () => { }, ], }); - expect(stack).toCountResources('AWS::EC2::InternetGateway', 1); - expect(stack).not.toHaveResource('AWS::EC2::NatGateway'); + Template.fromStack(stack).resourceCountIs('AWS::EC2::InternetGateway', 1); + Template.fromStack(stack).resourceCountIs('AWS::EC2::NatGateway', 0); }); test('with private subnets and custom networkAcl.', () => { @@ -218,9 +214,9 @@ describe('vpc', () => { cidr: AclCidr.anyIpv4(), }); - expect(stack).toCountResources('AWS::EC2::NetworkAcl', 1); - expect(stack).toCountResources('AWS::EC2::NetworkAclEntry', 2); - expect(stack).toCountResources('AWS::EC2::SubnetNetworkAclAssociation', 3); + Template.fromStack(stack).resourceCountIs('AWS::EC2::NetworkAcl', 1); + Template.fromStack(stack).resourceCountIs('AWS::EC2::NetworkAclEntry', 2); + Template.fromStack(stack).resourceCountIs('AWS::EC2::SubnetNetworkAclAssociation', 3); }); @@ -228,8 +224,8 @@ describe('vpc', () => { const stack = getTestStack(); const zones = stack.availabilityZones.length; new Vpc(stack, 'TheVPC', {}); - expect(stack).toCountResources('AWS::EC2::InternetGateway', 1); - expect(stack).toCountResources('AWS::EC2::NatGateway', zones); + Template.fromStack(stack).resourceCountIs('AWS::EC2::InternetGateway', 1); + Template.fromStack(stack).resourceCountIs('AWS::EC2::NatGateway', zones); }); @@ -252,8 +248,8 @@ describe('vpc', () => { routerType: RouterType.GATEWAY, destinationCidrBlock: '8.8.8.8/32', }); - expect(stack).toHaveResource('AWS::EC2::InternetGateway'); - expect(stack).toHaveResourceLike('AWS::EC2::Route', { + Template.fromStack(stack).resourceCountIs('AWS::EC2::InternetGateway', 1); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Route', { DestinationCidrBlock: '8.8.8.8/32', GatewayId: {}, }); @@ -284,7 +280,7 @@ describe('vpc', () => { ], maxAzs: 3, }); - expect(stack).toCountResources('AWS::EC2::Subnet', 6); + Template.fromStack(stack).resourceCountIs('AWS::EC2::Subnet', 6); }); test('with reserved subnets, any other subnets should not have cidrBlock from within reserved space', () => { @@ -312,17 +308,18 @@ describe('vpc', () => { maxAzs: 3, }); for (let i = 0; i < 3; i++) { - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { CidrBlock: `10.0.${i}.0/24`, }); } for (let i = 3; i < 6; i++) { - expect(stack).not.toHaveResource('AWS::EC2::Subnet', { + const matchingSubnets = Template.fromStack(stack).findResources('AWS::EC2::Subnet', { CidrBlock: `10.0.${i}.0/24`, }); + expect(Object.keys(matchingSubnets).length).toBe(0); } for (let i = 6; i < 9; i++) { - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { CidrBlock: `10.0.${i}.0/24`, }); } @@ -352,16 +349,16 @@ describe('vpc', () => { ], maxAzs: 3, }); - expect(stack).toCountResources('AWS::EC2::InternetGateway', 1); - expect(stack).toCountResources('AWS::EC2::NatGateway', zones); - expect(stack).toCountResources('AWS::EC2::Subnet', 9); + Template.fromStack(stack).resourceCountIs('AWS::EC2::InternetGateway', 1); + Template.fromStack(stack).resourceCountIs('AWS::EC2::NatGateway', zones); + Template.fromStack(stack).resourceCountIs('AWS::EC2::Subnet', 9); for (let i = 0; i < 6; i++) { - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { CidrBlock: `10.0.${i}.0/24`, }); } for (let i = 0; i < 3; i++) { - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { CidrBlock: `10.0.6.${i * 16}/28`, }); } @@ -391,16 +388,16 @@ describe('vpc', () => { ], maxAzs: 3, }); - expect(stack).toCountResources('AWS::EC2::InternetGateway', 1); - expect(stack).toCountResources('AWS::EC2::NatGateway', 2); - expect(stack).toCountResources('AWS::EC2::Subnet', 9); + Template.fromStack(stack).resourceCountIs('AWS::EC2::InternetGateway', 1); + Template.fromStack(stack).resourceCountIs('AWS::EC2::NatGateway', 2); + Template.fromStack(stack).resourceCountIs('AWS::EC2::Subnet', 9); for (let i = 0; i < 6; i++) { - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { CidrBlock: `10.0.${i}.0/24`, }); } for (let i = 0; i < 3; i++) { - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { CidrBlock: `10.0.6.${i * 16}/28`, }); } @@ -426,9 +423,9 @@ describe('vpc', () => { }, ], }); - expect(stack).toCountResources('AWS::EC2::Subnet', 1); - expect(stack).not.toHaveResource('AWS::EC2::NatGateway'); - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).resourceCountIs('AWS::EC2::Subnet', 1); + Template.fromStack(stack).resourceCountIs('AWS::EC2::NatGateway', 0); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { MapPublicIpOnLaunch: true, }); @@ -447,9 +444,9 @@ describe('vpc', () => { }, ], }); - expect(stack).toCountResources('AWS::EC2::Subnet', 1); - expect(stack).not.toHaveResource('AWS::EC2::NatGateway'); - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).resourceCountIs('AWS::EC2::Subnet', 1); + Template.fromStack(stack).resourceCountIs('AWS::EC2::NatGateway', 0); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { MapPublicIpOnLaunch: true, }); }); @@ -466,9 +463,9 @@ describe('vpc', () => { }, ], }); - expect(stack).toCountResources('AWS::EC2::Subnet', 1); - expect(stack).not.toHaveResource('AWS::EC2::NatGateway'); - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).resourceCountIs('AWS::EC2::Subnet', 1); + Template.fromStack(stack).resourceCountIs('AWS::EC2::NatGateway', 0); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { MapPublicIpOnLaunch: false, }); }); @@ -510,6 +507,7 @@ describe('vpc', () => { }); }).toThrow(/subnet cannot include mapPublicIpOnLaunch parameter/); }); + test('verify the Default VPC name', () => { const stack = getTestStack(); const tagName = { Key: 'Name', Value: `${stack.node.path}/VPC` }; @@ -526,16 +524,16 @@ describe('vpc', () => { }, ], }); - expect(stack).toCountResources('AWS::EC2::Subnet', 2); - expect(stack).toHaveResource('AWS::EC2::NatGateway'); - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).resourceCountIs('AWS::EC2::Subnet', 2); + Template.fromStack(stack).hasResource('AWS::EC2::NatGateway', Match.anyValue()); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { MapPublicIpOnLaunch: true, }); - expect(stack).toHaveResource('AWS::EC2::VPC', hasTags([tagName])); + Template.fromStack(stack).hasResource('AWS::EC2::VPC', hasTags([tagName])); }); + test('verify the assigned VPC name passing the "vpcName" prop', () => { const stack = getTestStack(); - const tagNameDefault = { Key: 'Name', Value: `${stack.node.path}/VPC` }; const tagName = { Key: 'Name', Value: 'CustomVPCName' }; new Vpc(stack, 'VPC', { maxAzs: 1, @@ -551,25 +549,24 @@ describe('vpc', () => { ], vpcName: 'CustomVPCName', }); - expect(stack).toCountResources('AWS::EC2::Subnet', 2); - expect(stack).toHaveResource('AWS::EC2::NatGateway'); - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).resourceCountIs('AWS::EC2::Subnet', 2); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::NatGateway', Match.anyValue()); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { MapPublicIpOnLaunch: true, }); - expect(stack).not.toHaveResource('AWS::EC2::VPC', hasTags([tagNameDefault])); - expect(stack).toHaveResource('AWS::EC2::VPC', hasTags([tagName])); + Template.fromStack(stack).hasResource('AWS::EC2::VPC', hasTags([tagName])); }); test('maxAZs defaults to 3 if unset', () => { const stack = getTestStack(); new Vpc(stack, 'VPC'); - expect(stack).toCountResources('AWS::EC2::Subnet', 6); - expect(stack).toCountResources('AWS::EC2::Route', 6); + Template.fromStack(stack).resourceCountIs('AWS::EC2::Subnet', 6); + Template.fromStack(stack).resourceCountIs('AWS::EC2::Route', 6); for (let i = 0; i < 6; i++) { - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { CidrBlock: `10.0.${i * 32}.0/19`, }); } - expect(stack).toHaveResourceLike('AWS::EC2::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Route', { DestinationCidrBlock: '0.0.0.0/0', NatGatewayId: {}, }); @@ -578,14 +575,14 @@ describe('vpc', () => { test('with maxAZs set to 2', () => { const stack = getTestStack(); new Vpc(stack, 'VPC', { maxAzs: 2 }); - expect(stack).toCountResources('AWS::EC2::Subnet', 4); - expect(stack).toCountResources('AWS::EC2::Route', 4); + Template.fromStack(stack).resourceCountIs('AWS::EC2::Subnet', 4); + Template.fromStack(stack).resourceCountIs('AWS::EC2::Route', 4); for (let i = 0; i < 4; i++) { - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { CidrBlock: `10.0.${i * 64}.0/18`, }); } - expect(stack).toHaveResourceLike('AWS::EC2::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Route', { DestinationCidrBlock: '0.0.0.0/0', NatGatewayId: {}, }); @@ -596,10 +593,10 @@ describe('vpc', () => { new Vpc(stack, 'VPC', { natGateways: 1, }); - expect(stack).toCountResources('AWS::EC2::Subnet', 6); - expect(stack).toCountResources('AWS::EC2::Route', 6); - expect(stack).toCountResources('AWS::EC2::NatGateway', 1); - expect(stack).toHaveResourceLike('AWS::EC2::Route', { + Template.fromStack(stack).resourceCountIs('AWS::EC2::Subnet', 6); + Template.fromStack(stack).resourceCountIs('AWS::EC2::Route', 6); + Template.fromStack(stack).resourceCountIs('AWS::EC2::NatGateway', 1); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Route', { DestinationCidrBlock: '0.0.0.0/0', NatGatewayId: {}, }); @@ -629,14 +626,14 @@ describe('vpc', () => { subnetGroupName: 'egress', }, }); - expect(stack).toCountResources('AWS::EC2::NatGateway', 3); + Template.fromStack(stack).resourceCountIs('AWS::EC2::NatGateway', 3); for (let i = 1; i < 4; i++) { - expect(stack).toHaveResource('AWS::EC2::Subnet', hasTags([{ - Key: 'Name', - Value: `TestStack/VPC/egressSubnet${i}`, - }, { + Template.fromStack(stack).hasResource('AWS::EC2::Subnet', hasTags([{ Key: 'aws-cdk:subnet-name', Value: 'egress', + }, { + Key: 'Name', + Value: `TestStack/VPC/egressSubnet${i}`, }])); } @@ -668,7 +665,7 @@ describe('vpc', () => { new Vpc(stack, 'VPC', { natGateways: 0, }); - expect(stack).toHaveResource('AWS::EC2::Subnet', hasTags([{ + Template.fromStack(stack).hasResource('AWS::EC2::Subnet', hasTags([{ Key: 'aws-cdk:subnet-type', Value: 'Isolated', }])); @@ -678,7 +675,7 @@ describe('vpc', () => { test('unspecified natGateways constructs with PRIVATE subnet', () => { const stack = getTestStack(); new Vpc(stack, 'VPC'); - expect(stack).toHaveResource('AWS::EC2::Subnet', hasTags([{ + Template.fromStack(stack).hasResource('AWS::EC2::Subnet', hasTags([{ Key: 'aws-cdk:subnet-type', Value: 'Private', }])); @@ -702,7 +699,7 @@ describe('vpc', () => { ], natGateways: 0, }); - expect(stack).toHaveResource('AWS::EC2::Subnet', hasTags([{ + Template.fromStack(stack).hasResource('AWS::EC2::Subnet', hasTags([{ Key: 'aws-cdk:subnet-name', Value: 'ingress', }])); @@ -728,8 +725,8 @@ describe('vpc', () => { natGatewayProvider: NatProvider.gateway({ eipAllocationIds: ['b'] }), natGateways: 1, }); - expect(stack).toCountResources('AWS::EC2::EIP', 0); - expect(stack).toHaveResource('AWS::EC2::NatGateway', { + Template.fromStack(stack).resourceCountIs('AWS::EC2::EIP', 0); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::NatGateway', { AllocationId: 'b', }); }); @@ -762,12 +759,12 @@ describe('vpc', () => { vpnGatewayAsn: 65000, }); - expect(stack).toHaveResource('AWS::EC2::VPNGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPNGateway', { AmazonSideAsn: 65000, Type: 'ipsec.1', }); - expect(stack).toHaveResource('AWS::EC2::VPCGatewayAttachment', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCGatewayAttachment', { VpcId: { Ref: 'VPCB9E5F0B4', }, @@ -776,7 +773,7 @@ describe('vpc', () => { }, }); - expect(stack).toHaveResource('AWS::EC2::VPNGatewayRoutePropagation', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPNGatewayRoutePropagation', { RouteTableIds: [ { Ref: 'VPCPrivateSubnet1RouteTableBE8A6027', @@ -810,7 +807,7 @@ describe('vpc', () => { ], }); - expect(stack).toHaveResource('AWS::EC2::VPNGatewayRoutePropagation', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPNGatewayRoutePropagation', { RouteTableIds: [ { Ref: 'VPCIsolatedSubnet1RouteTableEB156210', @@ -848,7 +845,7 @@ describe('vpc', () => { ], }); - expect(stack).toHaveResource('AWS::EC2::VPNGatewayRoutePropagation', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPNGatewayRoutePropagation', { RouteTableIds: [ { Ref: 'VPCPrivateSubnet1RouteTableBE8A6027', @@ -886,7 +883,7 @@ describe('vpc', () => { vpnGateway: true, }); - expect(stack).toHaveResource('AWS::EC2::VPNGatewayRoutePropagation', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPNGatewayRoutePropagation', { RouteTableIds: [ { Ref: 'VPCIsolatedSubnet1RouteTableEB156210', @@ -914,7 +911,7 @@ describe('vpc', () => { vpnGateway: true, }); - expect(stack).toHaveResource('AWS::EC2::VPNGatewayRoutePropagation', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPNGatewayRoutePropagation', { RouteTableIds: [ { Ref: 'VPCPublicSubnet1RouteTableFEE4B781', @@ -968,8 +965,6 @@ describe('vpc', () => { const vpc = new Vpc(stack, 'VpcNetwork'); expect(vpc.publicSubnets[0].node.defaultChild instanceof CfnSubnet).toEqual(true); - - }); test('CIDR cannot be a Token', () => { @@ -979,8 +974,6 @@ describe('vpc', () => { cidr: Lazy.string({ produce: () => 'abc' }), }); }).toThrow(/property must be a concrete CIDR string/); - - }); test('Default NAT gateway provider', () => { @@ -989,8 +982,6 @@ describe('vpc', () => { new Vpc(stack, 'VpcNetwork', { natGatewayProvider }); expect(natGatewayProvider.configuredGateways.length).toBeGreaterThan(0); - - }); test('NAT gateway provider with EIP allocations', () => { @@ -1000,14 +991,12 @@ describe('vpc', () => { }); new Vpc(stack, 'VpcNetwork', { natGatewayProvider }); - expect(stack).toHaveResource('AWS::EC2::NatGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::NatGateway', { AllocationId: 'a', }); - expect(stack).toHaveResource('AWS::EC2::NatGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::NatGateway', { AllocationId: 'b', }); - - }); test('NAT gateway provider with insufficient EIP allocations', () => { @@ -1015,8 +1004,6 @@ describe('vpc', () => { const natGatewayProvider = NatProvider.gateway({ eipAllocationIds: ['a'] }); expect(() => new Vpc(stack, 'VpcNetwork', { natGatewayProvider })) .toThrow(/Not enough NAT gateway EIP allocation IDs \(1 provided\) for the requested subnet count \(\d+ needed\)/); - - }); test('NAT gateway provider with token EIP allocations', () => { @@ -1025,14 +1012,12 @@ describe('vpc', () => { const natGatewayProvider = NatProvider.gateway({ eipAllocationIds }); new Vpc(stack, 'VpcNetwork', { natGatewayProvider }); - expect(stack).toHaveResource('AWS::EC2::NatGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::NatGateway', { AllocationId: stack.resolve(Fn.select(0, eipAllocationIds)), }); - expect(stack).toHaveResource('AWS::EC2::NatGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::NatGateway', { AllocationId: stack.resolve(Fn.select(1, eipAllocationIds)), }); - - }); test('Can add an IPv6 route', () => { @@ -1049,7 +1034,7 @@ describe('vpc', () => { // THEN - expect(stack).toHaveResourceLike('AWS::EC2::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Route', { DestinationIpv6CidrBlock: '2001:4860:4860::8888/32', NetworkInterfaceId: 'router-1', }); @@ -1070,7 +1055,7 @@ describe('vpc', () => { // THEN - expect(stack).toHaveResourceLike('AWS::EC2::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Route', { DestinationCidrBlock: '0.0.0.0/0', NetworkInterfaceId: 'router-1', }); @@ -1094,18 +1079,18 @@ describe('vpc', () => { new Vpc(stack, 'TheVPC', { natGatewayProvider }); // THEN - expect(stack).toCountResources('AWS::EC2::Instance', 3); - expect(stack).toHaveResource('AWS::EC2::Instance', { + Template.fromStack(stack).resourceCountIs('AWS::EC2::Instance', 3); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { ImageId: 'ami-1', InstanceType: 'q86.mega', SourceDestCheck: false, }); - expect(stack).toHaveResource('AWS::EC2::Route', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Route', { RouteTableId: { Ref: 'TheVPCPrivateSubnet1RouteTableF6513BC2' }, DestinationCidrBlock: '0.0.0.0/0', InstanceId: { Ref: 'TheVPCPublicSubnet1NatInstanceCC514192' }, }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { SecurityGroupEgress: [ { CidrIp: '0.0.0.0/0', @@ -1141,9 +1126,7 @@ describe('vpc', () => { }); // THEN - expect(stack).toCountResources('AWS::EC2::Instance', 1); - - + Template.fromStack(stack).resourceCountIs('AWS::EC2::Instance', 1); }); testDeprecated('can configure Security Groups of NAT instances with allowAllTraffic false', () => { @@ -1164,7 +1147,7 @@ describe('vpc', () => { provider.connections.allowFrom(Peer.ipv4('1.2.3.4/32'), Port.tcp(86)); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { SecurityGroupEgress: [ { CidrIp: '0.0.0.0/0', @@ -1203,7 +1186,7 @@ describe('vpc', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { SecurityGroupEgress: [ { CidrIp: '0.0.0.0/0', @@ -1240,7 +1223,7 @@ describe('vpc', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { SecurityGroupEgress: [ { CidrIp: '0.0.0.0/0', @@ -1270,7 +1253,7 @@ describe('vpc', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { SecurityGroupEgress: [ { CidrIp: '255.255.255.255/32', @@ -1298,15 +1281,13 @@ describe('vpc', () => { value: (vpc.publicSubnets[0] as Subnet).subnetNetworkAclAssociationId, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Outputs: { Output: { Value: { 'Fn::GetAtt': ['TheVPCPublicSubnet1Subnet770D4FF2', 'NetworkAclAssociationId'] }, }, }, - }, MatchStyle.SUPERSET); - - + }); }); test('if ACL is replaced new ACL reference is returned', () => { @@ -1323,15 +1304,13 @@ describe('vpc', () => { subnetSelection: { subnetType: SubnetType.PUBLIC }, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Outputs: { Output: { Value: { Ref: 'ACLDBD1BB49' }, }, }, - }, MatchStyle.SUPERSET); - - + }); }); }); @@ -1339,10 +1318,9 @@ describe('vpc', () => { test('vpc.vpcCidrBlock is the correct network range', () => { const stack = getTestStack(); new Vpc(stack, 'TheVPC', { cidr: '192.168.0.0/16' }); - expect(stack).toHaveResource('AWS::EC2::VPC', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPC', { CidrBlock: '192.168.0.0/16', }); - }); }); describe('When tagging', () => { @@ -1354,19 +1332,22 @@ describe('vpc', () => { const noPropTags = { BusinessUnit: 'Marketing', }; - const allTags = { ...tags, ...noPropTags }; + const allTags = { ...noPropTags, ...tags }; const vpc = new Vpc(stack, 'TheVPC'); // overwrite to set propagate Tags.of(vpc).add('BusinessUnit', 'Marketing', { includeResourceTypes: [CfnVPC.CFN_RESOURCE_TYPE_NAME] }); Tags.of(vpc).add('VpcType', 'Good'); - expect(stack).toHaveResource('AWS::EC2::VPC', hasTags(toCfnTags(allTags))); + Template.fromStack(stack).hasResource('AWS::EC2::VPC', hasTags(toCfnTags(allTags))); const taggables = ['Subnet', 'InternetGateway', 'NatGateway', 'RouteTable']; const propTags = toCfnTags(tags); const noProp = toCfnTags(noPropTags); for (const resource of taggables) { - expect(stack).toHaveResource(`AWS::EC2::${resource}`, hasTags(propTags)); - expect(stack).not.toHaveResource(`AWS::EC2::${resource}`, hasTags(noProp)); + Template.fromStack(stack).hasResourceProperties(`AWS::EC2::${resource}`, { + Tags: Match.arrayWith(propTags), + }); + const matchingResources = Template.fromStack(stack).findResources(`AWS::EC2::${resource}`, hasTags(noProp)); + expect(Object.keys(matchingResources).length).toBe(0); } }); @@ -1375,12 +1356,12 @@ describe('vpc', () => { const vpc = new Vpc(stack, 'TheVPC'); for (const subnet of vpc.publicSubnets) { const tag = { Key: 'Name', Value: subnet.node.path }; - expect(stack).toHaveResource('AWS::EC2::NatGateway', hasTags([tag])); - expect(stack).toHaveResource('AWS::EC2::RouteTable', hasTags([tag])); + Template.fromStack(stack).hasResource('AWS::EC2::NatGateway', hasTags([tag])); + Template.fromStack(stack).hasResource('AWS::EC2::RouteTable', hasTags([tag])); } for (const subnet of vpc.privateSubnets) { const tag = { Key: 'Name', Value: subnet.node.path }; - expect(stack).toHaveResource('AWS::EC2::RouteTable', hasTags([tag])); + Template.fromStack(stack).hasResource('AWS::EC2::RouteTable', hasTags([tag])); } }); @@ -1389,9 +1370,8 @@ describe('vpc', () => { const vpc = new Vpc(stack, 'TheVPC'); const tag = { Key: 'Late', Value: 'Adder' }; - expect(stack).not.toHaveResource('AWS::EC2::VPC', hasTags([tag])); Tags.of(vpc).add(tag.Key, tag.Value); - expect(stack).toHaveResource('AWS::EC2::VPC', hasTags([tag])); + Template.fromStack(stack).hasResource('AWS::EC2::VPC', hasTags([tag])); }); }); @@ -1573,24 +1553,15 @@ describe('vpc', () => { }); // THEN - No exception - expect(stack).toHaveResource('Some::Resource', { + Template.fromStack(stack).hasResourceProperties('Some::Resource', { subnetIds: { 'Fn::Split': [',', { 'Fn::ImportValue': 'myPublicSubnetIds' }] }, }); - // THEN - Warnings have been added to the stack metadata - const asm = SynthUtils.synthesize(stack); - expect(asm.messages).toEqual(expect.arrayContaining([ - expect.objectContaining( - { - entry: { - type: 'aws:cdk:warning', - data: "fromVpcAttributes: 'availabilityZones' is a list token: the imported VPC will not work with constructs that require a list of subnets at synthesis time. Use 'Vpc.fromLookup()' or 'Fn.importListValue' instead.", - }, - }, - ), - ])); - - + expect(vpc.node.metadataEntry).toContainEqual({ + data: expect.stringContaining("fromVpcAttributes: 'availabilityZones' is a list token: the imported VPC will not work with constructs that require a list of subnets at synthesis time. Use 'Vpc.fromLookup()' or 'Fn.importListValue' instead."), + type: 'aws:cdk:warning', + trace: undefined, + }); }); test('fromVpcAttributes using fixed-length list tokens', () => { @@ -1618,7 +1589,7 @@ describe('vpc', () => { // THEN - No exception const publicSubnetList = { 'Fn::Split': [',', { 'Fn::ImportValue': 'myPublicSubnetIds' }] }; - expect(stack).toHaveResource('Some::Resource', { + Template.fromStack(stack).hasResourceProperties('Some::Resource', { subnetIds: [ { 'Fn::Select': [0, publicSubnetList] }, { 'Fn::Select': [1, publicSubnetList] }, @@ -1723,7 +1694,7 @@ describe('vpc', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { ServiceName: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', SubnetIds: [ { @@ -1754,7 +1725,7 @@ describe('vpc', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { ServiceName: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', SubnetIds: [ { @@ -1806,7 +1777,7 @@ describe('vpc', () => { // THEN // 10.0.160.0/19 is the third subnet, sequentially, if you split // 10.0.0.0/16 into 6 pieces - expect(stack).toHaveResource('AWS::EC2::Instance', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { SubnetId: { Ref: 'VPCPrivateSubnet3Subnet3EDCD457', }, @@ -1837,7 +1808,7 @@ describe('vpc', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { ServiceName: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', SubnetIds: [ { @@ -1872,7 +1843,7 @@ describe('vpc', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::VPCEndpoint', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPCEndpoint', { ServiceName: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', SubnetIds: ['priv-1', 'priv-2'], }); @@ -1915,29 +1886,10 @@ function toCfnTags(tags: any): Array<{Key: string, Value: string}> { }); } -function hasTags(expectedTags: Array<{Key: string, Value: string}>): (props: any) => boolean { - return (props: any) => { - try { - const tags = props.Tags; - const actualTags = tags.filter( (tag: {Key: string, Value: string}) => { - for (const expectedTag of expectedTags) { - if (isSuperObject(expectedTag, tag)) { - return true; - } else { - continue; - } - } - // no values in array so expecting empty - return false; - }); - return actualTags.length === expectedTags.length; - } catch (e) { - /* eslint-disable no-console */ - console.error('Tags are incorrect'); - console.error('found tags ', props.Tags); - console.error('expected tags ', expectedTags); - /* eslint-enable no-console */ - throw e; - } +function hasTags(expectedTags: Array<{Key: string, Value: string}>) { + return { + Properties: { + Tags: Match.arrayWith(expectedTags), + }, }; } diff --git a/packages/@aws-cdk/aws-ec2/test/vpn.test.ts b/packages/@aws-cdk/aws-ec2/test/vpn.test.ts index 8a4c99159e5ec..1152581defc8f 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpn.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpn.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Duration, Stack, Token } from '@aws-cdk/core'; import { PublicSubnet, Vpc, VpnConnection } from '../lib'; @@ -18,13 +18,13 @@ describe('vpn', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::CustomerGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::CustomerGateway', { BgpAsn: 65001, IpAddress: '192.0.2.1', Type: 'ipsec.1', }); - expect(stack).toHaveResource('AWS::EC2::VPNConnection', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPNConnection', { CustomerGatewayId: { Ref: 'VpcNetworkVpnConnectionCustomerGateway8B56D9AF', }, @@ -56,7 +56,7 @@ describe('vpn', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::VPNConnection', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPNConnection', { CustomerGatewayId: { Ref: 'VpcNetworkstaticCustomerGatewayAF2651CC', }, @@ -67,14 +67,14 @@ describe('vpn', () => { StaticRoutesOnly: true, }); - expect(stack).toHaveResource('AWS::EC2::VPNConnectionRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPNConnectionRoute', { DestinationCidrBlock: '192.168.10.0/24', VpnConnectionId: { Ref: 'VpcNetworkstaticE33EA98C', }, }); - expect(stack).toHaveResource('AWS::EC2::VPNConnectionRoute', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPNConnectionRoute', { DestinationCidrBlock: '192.168.20.0/24', VpnConnectionId: { Ref: 'VpcNetworkstaticE33EA98C', @@ -102,7 +102,7 @@ describe('vpn', () => { }, }); - expect(stack).toHaveResource('AWS::EC2::VPNConnection', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPNConnection', { CustomerGatewayId: { Ref: 'VpcNetworkVpnConnectionCustomerGateway8B56D9AF', }, @@ -316,7 +316,7 @@ describe('vpn', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::CustomerGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::CustomerGateway', { Type: 'ipsec.1', }); @@ -336,7 +336,7 @@ describe('vpn', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::CustomerGateway', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::CustomerGateway', { IpAddress: '192.0.2.1', }); diff --git a/packages/@aws-cdk/aws-ecr-assets/NOTICE b/packages/@aws-cdk/aws-ecr-assets/NOTICE index b5fabb830ac84..1b7adbb891265 100644 --- a/packages/@aws-cdk/aws-ecr-assets/NOTICE +++ b/packages/@aws-cdk/aws-ecr-assets/NOTICE @@ -1,91 +1,2 @@ AWS Cloud Development Kit (AWS CDK) Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. - -------------------------------------------------------------------------------- - -The AWS CDK includes the following third-party software/licensing: - -** minimatch - https://www.npmjs.com/package/minimatch -Copyright (c) Isaac Z. Schlueter and Contributors - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - ----------------- - -** brace-expansion - https://www.npmjs.com/package/brace-expansion -Copyright (c) 2013 Julian Gruber - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ----------------- - -** balanced-match - https://www.npmjs.com/package/balanced-match -Copyright (c) 2013 Julian Gruber - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ----------------- - -** concat-map - https://www.npmjs.com/package/concat-map - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ----------------- \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr-assets/README.md b/packages/@aws-cdk/aws-ecr-assets/README.md index b106c952007b8..9be425daadd06 100644 --- a/packages/@aws-cdk/aws-ecr-assets/README.md +++ b/packages/@aws-cdk/aws-ecr-assets/README.md @@ -21,7 +21,7 @@ and/or your app's CI/CD pipeline, and can be naturally referenced in your CDK ap import { DockerImageAsset } from '@aws-cdk/aws-ecr-assets'; const asset = new DockerImageAsset(this, 'MyBuildImage', { - directory: path.join(__dirname, 'my-image') + directory: path.join(__dirname, 'my-image'), }); ``` @@ -55,14 +55,16 @@ values that can change between different machines to maintain a consistent asset hash. ```ts +import { DockerImageAsset } from '@aws-cdk/aws-ecr-assets'; + const asset = new DockerImageAsset(this, 'MyBuildImage', { directory: path.join(__dirname, 'my-image'), buildArgs: { - HTTP_PROXY: 'http://10.20.30.2:1234' + HTTP_PROXY: 'http://10.20.30.2:1234', }, invalidation: { - buildArgs: false - } + buildArgs: false, + }, }); ``` @@ -70,9 +72,23 @@ You can optionally pass a target to the `docker build` command by specifying the `target` property: ```ts +import { DockerImageAsset } from '@aws-cdk/aws-ecr-assets'; + const asset = new DockerImageAsset(this, 'MyBuildImage', { directory: path.join(__dirname, 'my-image'), - target: 'a-target' + target: 'a-target', +}); +``` + +You can optionally pass networking mode to the `docker build` command by specifying +the `networkMode` property: + +```ts +import { DockerImageAsset, NetworkMode } from '@aws-cdk/aws-ecr-assets'; + +const asset = new DockerImageAsset(this, 'MyBuildImage', { + directory: path.join(__dirname, 'my-image'), + networkMode: NetworkMode.HOST, }) ``` @@ -85,7 +101,7 @@ naturally referenced in your CDK app. import { TarballImageAsset } from '@aws-cdk/aws-ecr-assets'; const asset = new TarballImageAsset(this, 'MyBuildImage', { - tarballFile: 'local-image.tar' + tarballFile: 'local-image.tar', }); ``` @@ -106,7 +122,10 @@ your choice. Here an example from the [cdklabs/cdk-ecr-deployment] project: -```ts +```text +// This example available in TypeScript only + +import { DockerImageAsset } from '@aws-cdk/aws-ecr-assets'; import * as ecrdeploy from 'cdk-ecr-deployment'; const image = new DockerImageAsset(this, 'CDKDockerImage', { diff --git a/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts b/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts index 78e706587dd16..665b67e73105b 100644 --- a/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts +++ b/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts @@ -12,6 +12,50 @@ import { FingerprintOptions, FollowMode, IAsset } from '@aws-cdk/assets'; // eslint-disable-next-line no-duplicate-imports, import/order import { Construct as CoreConstruct } from '@aws-cdk/core'; +/** + * networking mode on build time supported by docker + */ +export class NetworkMode { + /** + * The default networking mode if omitted, create a network stack on the default Docker bridge + */ + public static readonly DEFAULT = new NetworkMode('default'); + + /** + * Use the Docker host network stack + */ + public static readonly HOST = new NetworkMode('host'); + + /** + * Disable the network stack, only the loopback device will be created + */ + public static readonly NONE = new NetworkMode('none'); + + /** + * Reuse another container's network stack + * + * @param containerId The target container's id or name + */ + public static fromContainer(containerId: string) { + return new NetworkMode(`container:${containerId}`); + } + + /** + * Used to specify a custom networking mode + * Use this if the networking mode name is not yet supported by the CDK. + * + * @param mode The networking mode to use for docker build + */ + public static custom(mode: string) { + return new NetworkMode(mode); + } + + /** + * @param mode The networking mode to use for docker build + */ + private constructor(public readonly mode: string) {} +} + /** * Options to control invalidation of `DockerImageAsset` asset hashes */ @@ -50,6 +94,13 @@ export interface DockerImageAssetInvalidationOptions { * @default true */ readonly repositoryName?: boolean; + + /** + * Use `networkMode` while calculating the asset hash + * + * @default true + */ + readonly networkMode?: boolean; } /** @@ -95,6 +146,13 @@ export interface DockerImageAssetOptions extends FingerprintOptions, FileFingerp */ readonly file?: string; + /** + * Networking mode for the RUN commands during build. Support docker API 1.25+. + * + * @default - no networking mode specified (the default networking mode `NetworkMode.DEFAULT` will be used) + */ + readonly networkMode?: NetworkMode; + /** * Options to control which parameters are used to invalidate the asset hash. * @@ -227,6 +285,7 @@ export class DockerImageAsset extends CoreConstruct implements IAsset { if (props.invalidation?.target !== false && props.target) { extraHash.target = props.target; } if (props.invalidation?.file !== false && props.file) { extraHash.file = props.file; } if (props.invalidation?.repositoryName !== false && props.repositoryName) { extraHash.repositoryName = props.repositoryName; } + if (props.invalidation?.networkMode !== false && props.networkMode) { extraHash.networkMode = props.networkMode; } // add "salt" to the hash in order to invalidate the image in the upgrade to // 1.21.0 which removes the AdoptedRepository resource (and will cause the @@ -258,6 +317,7 @@ export class DockerImageAsset extends CoreConstruct implements IAsset { dockerBuildTarget: this.dockerBuildTarget, dockerFile: props.file, sourceHash: staging.assetHash, + networkMode: props.networkMode?.mode, }); this.repository = ecr.Repository.fromRepositoryName(this, 'Repository', location.repositoryName); diff --git a/packages/@aws-cdk/aws-ecr-assets/package.json b/packages/@aws-cdk/aws-ecr-assets/package.json index 2b801a1787aa8..75fb178adb95a 100644 --- a/packages/@aws-cdk/aws-ecr-assets/package.json +++ b/packages/@aws-cdk/aws-ecr-assets/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -65,15 +72,15 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "@types/proxyquire": "^1.3.28", "aws-cdk": "0.0.0", - "jest": "^27.4.5", + "jest": "^27.5.1", "proxyquire": "^2.1.3" }, "dependencies": { @@ -83,8 +90,7 @@ "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cx-api": "0.0.0", - "constructs": "^3.3.69", - "minimatch": "^3.0.4" + "constructs": "^3.3.69" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { @@ -102,9 +108,6 @@ "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, - "bundledDependencies": [ - "minimatch" - ], "stability": "stable", "maturity": "stable", "awscdkio": { diff --git a/packages/@aws-cdk/aws-ecr-assets/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-ecr-assets/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..0770e89346bb9 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr-assets/rosetta/default.ts-fixture @@ -0,0 +1,11 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as path from 'path'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts b/packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts index a42fd2451d8e3..d931401484ba2 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts +++ b/packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts @@ -1,12 +1,12 @@ import * as fs from 'fs'; import * as path from 'path'; -import { expect as ourExpect, haveResource } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import { describeDeprecated, testDeprecated, testFutureBehavior } from '@aws-cdk/cdk-build-tools'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { App, DefaultStackSynthesizer, IgnoreMode, Lazy, LegacyStackSynthesizer, Stack, Stage } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import { DockerImageAsset } from '../lib'; +import { DockerImageAsset, NetworkMode } from '../lib'; /* eslint-disable quote-props */ @@ -147,6 +147,20 @@ describe('image asset', () => { }); + testFutureBehavior('with networkMode', flags, App, (app) => { + // GIVEN + const stack = new Stack(app); + // WHEN + new DockerImageAsset(stack, 'Image', { + directory: path.join(__dirname, 'demo-image'), + networkMode: NetworkMode.DEFAULT, + }); + + // THEN + const assetMetadata = stack.node.metadataEntry.find(({ type }) => type === cxschema.ArtifactMetadataEntryType.ASSET); + expect(assetMetadata && (assetMetadata.data as cxschema.ContainerImageAssetMetadataEntry).networkMode).toEqual('default'); + }); + testFutureBehavior('asset.repository.grantPull can be used to grant a principal permissions to use the image', flags, App, (app) => { // GIVEN const stack = new Stack(app); @@ -159,7 +173,7 @@ describe('image asset', () => { asset.repository.grantPull(user); // THEN - ourExpect(stack).to(haveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { 'Statement': [ { @@ -204,9 +218,7 @@ describe('image asset', () => { 'Ref': 'MyUserDC45028B', }, ], - })); - - + }); }); test('fails if the directory does not exist', () => { @@ -291,8 +303,6 @@ describe('image asset', () => { expect(!fs.existsSync(path.join(session.directory, `asset.${image.assetHash}`, 'node_modules', 'one'))).toBeDefined(); expect(!fs.existsSync(path.join(session.directory, `asset.${image.assetHash}`, 'node_modules', 'some_dep'))).toBeDefined(); expect(!fs.existsSync(path.join(session.directory, `asset.${image.assetHash}`, 'node_modules', 'some_dep', 'file'))).toBeDefined(); - - }); testFutureBehavior('docker directory is staged without files specified in exclude option', flags, App, (app) => { @@ -319,8 +329,6 @@ describe('image asset', () => { directory: path.join(__dirname, 'demo-image'), buildArgs: { key: token }, })).toThrow(expected); - - }); testDeprecated('fails if using token as repositoryName', () => { @@ -333,8 +341,6 @@ describe('image asset', () => { directory: path.join(__dirname, 'demo-image'), repositoryName: token, })).toThrow(/Cannot use Token as value of 'repositoryName'/); - - }); testFutureBehavior('docker build options are included in the asset id', flags, App, (app) => { @@ -386,8 +392,6 @@ function testDockerDirectoryIsStagedWithoutFilesSpecifiedInDockerignore(app: App expect(!fs.existsSync(path.join(session.directory, `asset.${image.assetHash}`, 'foobar.txt'))).toBeDefined(); expect(fs.existsSync(path.join(session.directory, `asset.${image.assetHash}`, 'subdirectory'))).toBeDefined(); expect(fs.existsSync(path.join(session.directory, `asset.${image.assetHash}`, 'subdirectory', 'baz.txt'))).toBeDefined(); - - } function testDockerDirectoryIsStagedWithoutFilesSpecifiedInExcludeOption(app: App, ignoreMode?: IgnoreMode) { @@ -406,8 +410,6 @@ function testDockerDirectoryIsStagedWithoutFilesSpecifiedInExcludeOption(app: Ap expect(!fs.existsSync(path.join(session.directory, `asset.${image.assetHash}`, 'foobar.txt'))).toBeDefined(); expect(!fs.existsSync(path.join(session.directory, `asset.${image.assetHash}`, 'subdirectory'))).toBeDefined(); expect(!fs.existsSync(path.join(session.directory, `asset.${image.assetHash}`, 'subdirectory', 'baz.txt'))).toBeDefined(); - - } testFutureBehavior('nested assemblies share assets: legacy synth edition', flags, App, (app) => { diff --git a/packages/@aws-cdk/aws-ecr-assets/test/tarball-asset.test.ts b/packages/@aws-cdk/aws-ecr-assets/test/tarball-asset.test.ts index 77e22f20dca9d..dec618738fc5a 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/tarball-asset.test.ts +++ b/packages/@aws-cdk/aws-ecr-assets/test/tarball-asset.test.ts @@ -1,11 +1,11 @@ import * as fs from 'fs'; import * as path from 'path'; -import { expect as ourExpect, haveResource } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; +import { testFutureBehavior } from '@aws-cdk/cdk-build-tools/lib/feature-flag'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { App, Stack } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import { testFutureBehavior } from '@aws-cdk/cdk-build-tools/lib/feature-flag'; import { TarballImageAsset } from '../lib'; /* eslint-disable quote-props */ @@ -64,7 +64,7 @@ describe('image asset', () => { asset.repository.grantPull(user); // THEN - ourExpect(stack).to(haveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { 'Statement': [ { @@ -112,7 +112,7 @@ describe('image asset', () => { 'Ref': 'MyUserDC45028B', }, ], - })); + }); }); testFutureBehavior('docker directory is staged if asset staging is enabled', flags, App, (app) => { diff --git a/packages/@aws-cdk/aws-ecr/README.md b/packages/@aws-cdk/aws-ecr/README.md index 193d5de2f0645..6e67349024891 100644 --- a/packages/@aws-cdk/aws-ecr/README.md +++ b/packages/@aws-cdk/aws-ecr/README.md @@ -79,6 +79,32 @@ You can set tag immutability on images in our repository using the `imageTagMuta new ecr.Repository(this, 'Repo', { imageTagMutability: ecr.TagMutability.IMMUTABLE }); ``` +### Encryption + +By default, Amazon ECR uses server-side encryption with Amazon S3-managed encryption keys which encrypts your data at rest using an AES-256 encryption algorithm. For more control over the encryption for your Amazon ECR repositories, you can use server-side encryption with KMS keys stored in AWS Key Management Service (AWS KMS). Read more about this feature in the [ECR Developer Guide](https://docs.aws.amazon.com/AmazonECR/latest/userguide/encryption-at-rest.html). + +When you use AWS KMS to encrypt your data, you can either use the default AWS managed key, which is managed by Amazon ECR, by specifying `RepositoryEncryption.KMS` in the `encryption` property. Or specify your own customer managed KMS key, by specifying the `encryptionKey` property. + +When `encryptionKey` is set, the `encryption` property must be `KMS` or empty. + +In the case `encryption` is set to `KMS` but no `encryptionKey` is set, an AWS managed KMS key is used. + +```ts +new ecr.Repository(this, 'Repo', { + encryption: ecr.RepositoryEncryption.KMS +}); +``` + +Otherwise, a customer-managed KMS key is used if `encryptionKey` was set and `encryption` was optionally set to `KMS`. + +```ts +import * as kms from '@aws-cdk/aws-kms'; + +new ecr.Repository(this, 'Repo', { + encryptionKey: new kms.Key(this, 'Key'), +}); +``` + ## Automatically clean up repositories You can set life cycle rules to automatically clean up old images from your diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index 7c25767cb7962..f73c8990dd95f 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -1,6 +1,7 @@ import { EOL } from 'os'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; import { ArnFormat, IResource, Lazy, RemovalPolicy, Resource, Stack, Token } from '@aws-cdk/core'; import { IConstruct, Construct } from 'constructs'; import { CfnRepository } from './ecr.generated'; @@ -327,6 +328,27 @@ export interface RepositoryProps { */ readonly repositoryName?: string; + /** + * The kind of server-side encryption to apply to this repository. + * + * If you choose KMS, you can specify a KMS key via `encryptionKey`. If + * encryptionKey is not specified, an AWS managed KMS key is used. + * + * @default - `KMS` if `encryptionKey` is specified, or `AES256` otherwise. + */ + readonly encryption?: RepositoryEncryption; + + /** + * External KMS key to use for repository encryption. + * + * The 'encryption' property must be either not specified or set to "KMS". + * An error will be emitted if encryption is set to "AES256". + * + * @default - If encryption is set to `KMS` and this property is undefined, + * an AWS managed KMS key is used. + */ + readonly encryptionKey?: kms.IKey; + /** * Life cycle rules to apply to this registry * @@ -488,6 +510,7 @@ export class Repository extends RepositoryBase { lifecyclePolicy: Lazy.any({ produce: () => this.renderLifecyclePolicy() }), imageScanningConfiguration: props.imageScanOnPush ? { scanOnPush: true } : { scanOnPush: false }, imageTagMutability: props.imageTagMutability || undefined, + encryptionConfiguration: this.parseEncryption(props), }); resource.applyRemovalPolicy(props.removalPolicy); @@ -600,6 +623,34 @@ export class Repository extends RepositoryBase { validateAnyRuleLast(ret); return ret; } + + /** + * Set up key properties and return the Repository encryption property from the + * user's configuration. + */ + private parseEncryption(props: RepositoryProps): CfnRepository.EncryptionConfigurationProperty | undefined { + + // default based on whether encryptionKey is specified + const encryptionType = props.encryption ?? (props.encryptionKey ? RepositoryEncryption.KMS : RepositoryEncryption.AES_256); + + // if encryption key is set, encryption must be set to KMS. + if (encryptionType !== RepositoryEncryption.KMS && props.encryptionKey) { + throw new Error(`encryptionKey is specified, so 'encryption' must be set to KMS (value: ${encryptionType.value})`); + } + + if (encryptionType === RepositoryEncryption.AES_256) { + return undefined; + } + + if (encryptionType === RepositoryEncryption.KMS) { + return { + encryptionType: 'KMS', + kmsKey: props.encryptionKey?.keyArn, + }; + } + + throw new Error(`Unexpected 'encryptionType': ${encryptionType}`); + } } function validateAnyRuleLast(rules: LifecycleRule[]) { @@ -662,3 +713,24 @@ export enum TagMutability { IMMUTABLE = 'IMMUTABLE', } + +/** + * Indicates whether server-side encryption is enabled for the object, and whether that encryption is + * from the AWS Key Management Service (AWS KMS) or from Amazon S3 managed encryption (SSE-S3). + * @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#SysMetadata + */ +export class RepositoryEncryption { + /** + * 'AES256' + */ + public static readonly AES_256 = new RepositoryEncryption('AES256'); + /** + * 'KMS' + */ + public static readonly KMS = new RepositoryEncryption('KMS'); + + /** + * @param value the string value of the encryption + */ + protected constructor(public readonly value: string) { } +} diff --git a/packages/@aws-cdk/aws-ecr/package.json b/packages/@aws-cdk/aws-ecr/package.json index bc4e518ac3666..ae13302f8642e 100644 --- a/packages/@aws-cdk/aws-ecr/package.json +++ b/packages/@aws-cdk/aws-ecr/package.json @@ -83,16 +83,17 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, @@ -100,6 +101,7 @@ "peerDependencies": { "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, diff --git a/packages/@aws-cdk/aws-ecr/test/auth-token.test.ts b/packages/@aws-cdk/aws-ecr/test/auth-token.test.ts index f9be93b1e15d0..421ba7c2f6645 100644 --- a/packages/@aws-cdk/aws-ecr/test/auth-token.test.ts +++ b/packages/@aws-cdk/aws-ecr/test/auth-token.test.ts @@ -1,4 +1,4 @@ -import { expect as expectCDK, haveResourceLike } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import { Stack } from '@aws-cdk/core'; import { AuthorizationToken, PublicGalleryAuthorizationToken } from '../lib'; @@ -13,7 +13,7 @@ describe('auth-token', () => { AuthorizationToken.grantRead(user); // THEN - expectCDK(stack).to(haveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -23,7 +23,7 @@ describe('auth-token', () => { }, ], }, - })); + }); }); test('PublicGalleryAuthorizationToken.grantRead()', () => { @@ -35,7 +35,7 @@ describe('auth-token', () => { PublicGalleryAuthorizationToken.grantRead(user); // THEN - expectCDK(stack).to(haveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -48,6 +48,6 @@ describe('auth-token', () => { }, ], }, - })); + }); }); }); diff --git a/packages/@aws-cdk/aws-ecr/test/repository.test.ts b/packages/@aws-cdk/aws-ecr/test/repository.test.ts index 4e3aff2e4db8d..bfe570deab493 100644 --- a/packages/@aws-cdk/aws-ecr/test/repository.test.ts +++ b/packages/@aws-cdk/aws-ecr/test/repository.test.ts @@ -1,6 +1,7 @@ import { EOL } from 'os'; -import { expect as expectCDK, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; import * as cdk from '@aws-cdk/core'; import * as ecr from '../lib'; @@ -15,7 +16,7 @@ describe('repository', () => { new ecr.Repository(stack, 'Repo'); // THEN - expectCDK(stack).toMatch({ + Template.fromStack(stack).templateMatches({ Resources: { Repo02AC86CF: { Type: 'AWS::ECR::Repository', @@ -39,11 +40,11 @@ describe('repository', () => { new ecr.Repository(stack, 'Repo', { imageScanOnPush: true }); // THEN - expectCDK(stack).to(haveResource('AWS::ECR::Repository', { + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', { ImageScanningConfiguration: { ScanOnPush: true, }, - })); + }); }); test('tag-based lifecycle policy', () => { @@ -55,12 +56,12 @@ describe('repository', () => { repo.addLifecycleRule({ tagPrefixList: ['abc'], maxImageCount: 1 }); // THEN - expectCDK(stack).to(haveResource('AWS::ECR::Repository', { + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', { LifecyclePolicy: { // eslint-disable-next-line max-len LifecyclePolicyText: '{"rules":[{"rulePriority":1,"selection":{"tagStatus":"tagged","tagPrefixList":["abc"],"countType":"imageCountMoreThan","countNumber":1},"action":{"type":"expire"}}]}', }, - })); + }); }); test('image tag mutability can be set', () => { @@ -69,9 +70,9 @@ describe('repository', () => { new ecr.Repository(stack, 'Repo', { imageTagMutability: ecr.TagMutability.IMMUTABLE }); // THEN - expectCDK(stack).to(haveResource('AWS::ECR::Repository', { + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', { ImageTagMutability: 'IMMUTABLE', - })); + }); }); test('add day-based lifecycle policy', () => { @@ -85,12 +86,12 @@ describe('repository', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::ECR::Repository', { + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', { LifecyclePolicy: { // eslint-disable-next-line max-len LifecyclePolicyText: '{"rules":[{"rulePriority":1,"selection":{"tagStatus":"any","countType":"sinceImagePushed","countNumber":5,"countUnit":"days"},"action":{"type":"expire"}}]}', }, - })); + }); }); test('add count-based lifecycle policy', () => { @@ -104,12 +105,12 @@ describe('repository', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::ECR::Repository', { + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', { LifecyclePolicy: { // eslint-disable-next-line max-len LifecyclePolicyText: '{"rules":[{"rulePriority":1,"selection":{"tagStatus":"any","countType":"imageCountMoreThan","countNumber":5},"action":{"type":"expire"}}]}', }, - })); + }); }); test('mixing numbered and unnumbered rules', () => { @@ -122,12 +123,12 @@ describe('repository', () => { repo.addLifecycleRule({ rulePriority: 10, tagStatus: ecr.TagStatus.TAGGED, tagPrefixList: ['b'], maxImageCount: 5 }); // THEN - expectCDK(stack).to(haveResource('AWS::ECR::Repository', { + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', { LifecyclePolicy: { // eslint-disable-next-line max-len LifecyclePolicyText: '{"rules":[{"rulePriority":10,"selection":{"tagStatus":"tagged","tagPrefixList":["b"],"countType":"imageCountMoreThan","countNumber":5},"action":{"type":"expire"}},{"rulePriority":11,"selection":{"tagStatus":"tagged","tagPrefixList":["a"],"countType":"imageCountMoreThan","countNumber":5},"action":{"type":"expire"}}]}', }, - })); + }); }); test('tagstatus Any is automatically sorted to the back', () => { @@ -140,12 +141,12 @@ describe('repository', () => { repo.addLifecycleRule({ tagStatus: ecr.TagStatus.TAGGED, tagPrefixList: ['important'], maxImageCount: 999 }); // THEN - expectCDK(stack).to(haveResource('AWS::ECR::Repository', { + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', { LifecyclePolicy: { // eslint-disable-next-line max-len LifecyclePolicyText: '{"rules":[{"rulePriority":1,"selection":{"tagStatus":"tagged","tagPrefixList":["important"],"countType":"imageCountMoreThan","countNumber":999},"action":{"type":"expire"}},{"rulePriority":2,"selection":{"tagStatus":"any","countType":"imageCountMoreThan","countNumber":5},"action":{"type":"expire"}}]}', }, - })); + }); }); test('lifecycle rules can be added upon initialization', () => { @@ -160,12 +161,12 @@ describe('repository', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::ECR::Repository', { + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', { 'LifecyclePolicy': { // eslint-disable-next-line max-len 'LifecyclePolicyText': '{"rules":[{"rulePriority":1,"selection":{"tagStatus":"any","countType":"imageCountMoreThan","countNumber":3},"action":{"type":"expire"}}]}', }, - })); + }); }); test('calculate repository URI', () => { @@ -173,21 +174,24 @@ describe('repository', () => { const stack = new cdk.Stack(); const repo = new ecr.Repository(stack, 'Repo'); - // WHEN - const uri = repo.repositoryUri; + new cdk.CfnOutput(stack, 'RepoUri', { + value: repo.repositoryUri, + }); // THEN const arnSplit = { 'Fn::Split': [':', { 'Fn::GetAtt': ['Repo02AC86CF', 'Arn'] }] }; - expectCDK(stack.resolve(uri)).toMatch({ - 'Fn::Join': ['', [ - { 'Fn::Select': [4, arnSplit] }, - '.dkr.ecr.', - { 'Fn::Select': [3, arnSplit] }, - '.', - { Ref: 'AWS::URLSuffix' }, - '/', - { Ref: 'Repo02AC86CF' }, - ]], + Template.fromStack(stack).hasOutput('*', { + 'Value': { + 'Fn::Join': ['', [ + { 'Fn::Select': [4, arnSplit] }, + '.dkr.ecr.', + { 'Fn::Select': [3, arnSplit] }, + '.', + { Ref: 'AWS::URLSuffix' }, + '/', + { Ref: 'Repo02AC86CF' }, + ]], + }, }); }); @@ -222,10 +226,20 @@ describe('repository', () => { repositoryArn: cdk.Fn.getAtt('Boom', 'Arn').toString(), repositoryName: cdk.Fn.getAtt('Boom', 'Name').toString(), }); + new cdk.CfnOutput(stack, 'RepoArn', { + value: repo.repositoryArn, + }); + new cdk.CfnOutput(stack, 'RepoName', { + value: repo.repositoryName, + }); // THEN - expectCDK(stack.resolve(repo.repositoryArn)).toMatch({ 'Fn::GetAtt': ['Boom', 'Arn'] }); - expectCDK(stack.resolve(repo.repositoryName)).toMatch({ 'Fn::GetAtt': ['Boom', 'Name'] }); + Template.fromStack(stack).hasOutput('*', { + Value: { 'Fn::GetAtt': ['Boom', 'Arn'] }, + }); + Template.fromStack(stack).hasOutput('*', { + Value: { 'Fn::GetAtt': ['Boom', 'Name'] }, + }); }); test('import only with a repository name (arn is deduced)', () => { @@ -234,20 +248,30 @@ describe('repository', () => { // WHEN const repo = ecr.Repository.fromRepositoryName(stack, 'just-name', 'my-repo'); + new cdk.CfnOutput(stack, 'RepoArn', { + value: repo.repositoryArn, + }); + new cdk.CfnOutput(stack, 'RepoName', { + value: repo.repositoryName, + }); // THEN - expectCDK(stack.resolve(repo.repositoryArn)).toMatch({ - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':ecr:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':repository/my-repo', - ]], - }); - expect(stack.resolve(repo.repositoryName)).toBe('my-repo'); + Template.fromStack(stack).hasOutput('*', { + Value: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ecr:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':repository/my-repo', + ]], + }, + }); + Template.fromStack(stack).hasOutput('*', { + Value: 'my-repo', + }); }); test('arnForLocalRepository can be used to render an ARN for a local repository', () => { @@ -260,20 +284,30 @@ describe('repository', () => { repositoryArn: ecr.Repository.arnForLocalRepository(repoName, stack), repositoryName: repoName, }); + new cdk.CfnOutput(stack, 'RepoArn', { + value: repo.repositoryArn, + }); + new cdk.CfnOutput(stack, 'RepoName', { + value: repo.repositoryName, + }); // THEN - expectCDK(stack.resolve(repo.repositoryName)).toMatch({ 'Fn::GetAtt': ['Boom', 'Name'] }); - expectCDK(stack.resolve(repo.repositoryArn)).toMatch({ - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':ecr:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':repository/', - { 'Fn::GetAtt': ['Boom', 'Name'] }, - ]], + Template.fromStack(stack).hasOutput('*', { + Value: { 'Fn::GetAtt': ['Boom', 'Name'] }, + }); + Template.fromStack(stack).hasOutput('*', { + Value: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ecr:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':repository/', + { 'Fn::GetAtt': ['Boom', 'Name'] }, + ]], + }, }); }); @@ -289,7 +323,7 @@ describe('repository', () => { })); // THEN - expectCDK(stack).to(haveResource('AWS::ECR::Repository', { + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', { RepositoryPolicyText: { Statement: [ { @@ -300,7 +334,7 @@ describe('repository', () => { ], Version: '2012-10-17', }, - })); + }); }); test('fails if repository policy has no actions', () => { @@ -335,6 +369,79 @@ describe('repository', () => { expect(() => app.synth()).toThrow(/A PolicyStatement used in a resource-based policy must specify at least one IAM principal/); }); + test('default encryption configuration', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'my-stack'); + + // WHEN + new ecr.Repository(stack, 'Repo', { encryption: ecr.RepositoryEncryption.AES_256 }); + + // THEN + Template.fromStack(stack).templateMatches({ + Resources: { + Repo02AC86CF: { + Type: 'AWS::ECR::Repository', + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, + }, + }); + }); + + test('kms encryption configuration', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'my-stack'); + + // WHEN + new ecr.Repository(stack, 'Repo', { encryption: ecr.RepositoryEncryption.KMS }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', + { + EncryptionConfiguration: { + EncryptionType: 'KMS', + }, + }); + }); + + test('kms encryption with custom kms configuration', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'my-stack'); + + // WHEN + const custom_key = new kms.Key(stack, 'Key'); + new ecr.Repository(stack, 'Repo', { encryptionKey: custom_key }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', + { + EncryptionConfiguration: { + EncryptionType: 'KMS', + KmsKey: { + 'Fn::GetAtt': [ + 'Key961B73FD', + 'Arn', + ], + }, + }, + }); + }); + + test('fails if with custom kms key and AES256 as encryption', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'my-stack'); + const custom_key = new kms.Key(stack, 'Key'); + + // THEN + expect(() => { + new ecr.Repository(stack, 'Repo', { encryption: ecr.RepositoryEncryption.AES_256, encryptionKey: custom_key }); + }).toThrow('encryptionKey is specified, so \'encryption\' must be set to KMS (value: AES256)'); + }); + describe('events', () => { test('onImagePushed without imageTag creates the correct event', () => { const stack = new cdk.Stack(); @@ -346,7 +453,7 @@ describe('repository', () => { }, }); - expectCDK(stack).to(haveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.ecr', @@ -365,7 +472,7 @@ describe('repository', () => { }, }, 'State': 'ENABLED', - })); + }); }); test('onImageScanCompleted without imageTags creates the correct event', () => { @@ -378,7 +485,7 @@ describe('repository', () => { }, }); - expectCDK(stack).to(haveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.ecr', @@ -395,7 +502,7 @@ describe('repository', () => { }, }, 'State': 'ENABLED', - })); + }); }); test('onImageScanCompleted with one imageTag creates the correct event', () => { @@ -409,7 +516,7 @@ describe('repository', () => { }, }); - expectCDK(stack).to(haveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.ecr', @@ -429,7 +536,7 @@ describe('repository', () => { }, }, 'State': 'ENABLED', - })); + }); }); test('onImageScanCompleted with multiple imageTags creates the correct event', () => { @@ -443,7 +550,7 @@ describe('repository', () => { }, }); - expectCDK(stack).to(haveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.ecr', @@ -465,7 +572,7 @@ describe('repository', () => { }, }, 'State': 'ENABLED', - })); + }); }); test('removal policy is "Retain" by default', () => { @@ -476,10 +583,10 @@ describe('repository', () => { new ecr.Repository(stack, 'Repo'); // THEN - expectCDK(stack).to(haveResource('AWS::ECR::Repository', { + Template.fromStack(stack).hasResource('AWS::ECR::Repository', { 'Type': 'AWS::ECR::Repository', 'DeletionPolicy': 'Retain', - }, ResourcePart.CompleteDefinition)); + }); }); test('"Delete" removal policy can be set explicitly', () => { @@ -492,10 +599,10 @@ describe('repository', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::ECR::Repository', { + Template.fromStack(stack).hasResource('AWS::ECR::Repository', { 'Type': 'AWS::ECR::Repository', 'DeletionPolicy': 'Delete', - }, ResourcePart.CompleteDefinition)); + }); }); test('grant adds appropriate resource-*', () => { @@ -507,7 +614,7 @@ describe('repository', () => { repo.grantPull(new iam.AnyPrincipal()); // THEN - expectCDK(stack).to(haveResource('AWS::ECR::Repository', { + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', { 'RepositoryPolicyText': { 'Statement': [ { @@ -517,12 +624,12 @@ describe('repository', () => { 'ecr:BatchGetImage', ], 'Effect': 'Allow', - 'Principal': { AWS: '*' }, + 'Principal': { 'AWS': '*' }, }, ], 'Version': '2012-10-17', }, - })); + }); }); }); diff --git a/packages/@aws-cdk/aws-ecs-patterns/README.md b/packages/@aws-cdk/aws-ecs-patterns/README.md index 386682e0b75d9..593421abeaa1a 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/README.md +++ b/packages/@aws-cdk/aws-ecs-patterns/README.md @@ -544,6 +544,26 @@ const queueProcessingFargateService = new ecsPatterns.QueueProcessingFargateServ }); ``` +### Set a custom container-level Healthcheck for QueueProcessingFargateService + +```ts +declare const vpc: ec2.Vpc; +declare const securityGroup: ec2.SecurityGroup; +const queueProcessingFargateService = new ecsPatterns.QueueProcessingFargateService(this, 'Service', { + vpc, + memoryLimitMiB: 512, + image: ecs.ContainerImage.fromRegistry('test'), + healthCheck: { + command: [ "CMD-SHELL", "curl -f http://localhost/ || exit 1" ], + // the properties below are optional + interval: Duration.minutes(30), + retries: 123, + startPeriod: Duration.minutes(30), + timeout: Duration.minutes(30), + }, +}); +``` + ### Set capacityProviderStrategies for QueueProcessingEc2Service ```ts diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts index 0857b99ee8cfd..942f13e3439aa 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts @@ -347,7 +347,7 @@ export abstract class NetworkLoadBalancedServiceBase extends CoreConstruct { const loadBalancer = props.loadBalancer ?? new NetworkLoadBalancer(this, 'LB', lbProps); const listenerPort = props.listenerPort ?? 80; const targetProps = { - port: 80, + port: props.taskImageOptions?.containerPort ?? 80, }; this.listener = loadBalancer.addListener('PublicListener', { port: listenerPort }); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts index 8eb751b07d13e..677caf8c2df9f 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts @@ -374,7 +374,7 @@ export abstract class NetworkMultipleTargetGroupsServiceBase extends CoreConstru protected registerECSTargets(service: BaseService, container: ContainerDefinition, targets: NetworkTargetProps[]): NetworkTargetGroup { for (const targetProps of targets) { const targetGroup = this.findListener(targetProps.listener).addTargets(`ECSTargetGroup${container.containerName}${targetProps.containerPort}`, { - port: 80, + port: targetProps.containerPort ?? 80, targets: [ service.loadBalancerTarget({ containerName: container.containerName, 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 033d19dc39612..ff63c4a3502d8 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 @@ -1,5 +1,5 @@ import * as ec2 from '@aws-cdk/aws-ec2'; -import { FargatePlatformVersion, FargateService, FargateTaskDefinition } from '@aws-cdk/aws-ecs'; +import { FargatePlatformVersion, FargateService, FargateTaskDefinition, HealthCheck } from '@aws-cdk/aws-ecs'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { QueueProcessingServiceBase, QueueProcessingServiceBaseProps } from '../base/queue-processing-service-base'; @@ -69,6 +69,13 @@ export interface QueueProcessingFargateServiceProps extends QueueProcessingServi */ readonly containerName?: string; + /** + * The health check command and associated configuration parameters for the container. + * + * @default - Health check configuration from container. + */ + readonly healthCheck?: HealthCheck; + /** * The subnets to associate with the service. * @@ -127,6 +134,7 @@ export class QueueProcessingFargateService extends QueueProcessingServiceBase { environment: this.environment, secrets: this.secrets, logging: this.logDriver, + healthCheck: props.healthCheck, }); // The desiredCount should be removed from the fargate service when the feature flag is removed. diff --git a/packages/@aws-cdk/aws-ecs-patterns/package.json b/packages/@aws-cdk/aws-ecs-patterns/package.json index ed419dd5739d4..72d98e55f86d1 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/package.json +++ b/packages/@aws-cdk/aws-ecs-patterns/package.json @@ -72,13 +72,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-applicationautoscaling": "0.0.0", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/l3s-v2.test.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/l3s-v2.test.ts index 767c7a13f0df6..322e503a44aaf 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/l3s-v2.test.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/l3s-v2.test.ts @@ -1,5 +1,4 @@ -import { SynthUtils } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import { AutoScalingGroup } from '@aws-cdk/aws-autoscaling'; import { Certificate } from '@aws-cdk/aws-certificatemanager'; import { MachineImage, Vpc } from '@aws-cdk/aws-ec2'; @@ -44,16 +43,16 @@ describe('When Application Load Balancer', () => { }); // THEN - stack contains a load balancer, a service, and a target group. - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer'); + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::LoadBalancer', 1); - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 1, LaunchType: 'EC2', }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Image: 'test', LogConfiguration: { LogDriver: 'awslogs', @@ -76,7 +75,7 @@ describe('When Application Load Balancer', () => { Protocol: 'tcp', }, ], - }, + }), ], NetworkMode: 'bridge', RequiresCompatibilities: [ @@ -167,7 +166,7 @@ describe('When Application Load Balancer', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 3, LaunchType: 'EC2', EnableECSManagedTags: true, @@ -192,7 +191,7 @@ describe('When Application Load Balancer', () => { ServiceName: 'myService', }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Cpu: 256, @@ -260,7 +259,7 @@ describe('When Application Load Balancer', () => { }, }); - expect(stack).toHaveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { Port: 443, Protocol: 'HTTPS', Certificates: [{ @@ -270,26 +269,6 @@ describe('When Application Load Balancer', () => { }); }); - test('set vpc instead of cluster', () => { - // GIVEN - const stack = new Stack(); - const vpc = new Vpc(stack, 'VPC'); - - // WHEN - new ApplicationMultipleTargetGroupsEc2Service(stack, 'Service', { - vpc, - memoryLimitMiB: 1024, - taskImageOptions: { - image: ContainerImage.fromRegistry('test'), - }, - }); - - // THEN - stack does not contain a LaunchConfiguration - const template = SynthUtils.synthesize(stack, { skipValidation: true }); - expect(template).not.toHaveResource('AWS::AutoScaling::LaunchConfiguration'); - expect(() => SynthUtils.synthesize(stack)).toThrow(); - }); - test('able to pass pre-defined task definition', () => { // GIVEN const stack = new Stack(); @@ -319,7 +298,7 @@ describe('When Application Load Balancer', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Essential: true, @@ -408,8 +387,8 @@ describe('When Application Load Balancer', () => { }); // THEN - const template = SynthUtils.synthesize(stack).template.Outputs; - expect(template).toEqual({ + const outputs = Template.fromStack(stack).findOutputs('*'); + expect(outputs).toEqual({ ServiceLoadBalancerDNSlb175E78BFE: { Value: { 'Fn::GetAtt': [ @@ -596,7 +575,7 @@ describe('When Application Load Balancer', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { ServiceRegistries: [ { ContainerName: 'web', @@ -611,7 +590,7 @@ describe('When Application Load Balancer', () => { ], }); - expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -926,14 +905,14 @@ describe('When Network Load Balancer', () => { }); // THEN - stack contains a load balancer and a service - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer'); + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::LoadBalancer', 1); - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 1, LaunchType: 'EC2', }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Essential: true, @@ -1066,7 +1045,7 @@ describe('When Network Load Balancer', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 3, EnableECSManagedTags: true, HealthCheckGracePeriodSeconds: 2, @@ -1092,7 +1071,7 @@ describe('When Network Load Balancer', () => { ServiceName: 'myService', }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Cpu: 256, @@ -1161,26 +1140,6 @@ describe('When Network Load Balancer', () => { }); }); - test('set vpc instead of cluster', () => { - // GIVEN - const stack = new Stack(); - const vpc = new Vpc(stack, 'VPC'); - - // WHEN - new NetworkMultipleTargetGroupsEc2Service(stack, 'Service', { - vpc, - memoryLimitMiB: 256, - taskImageOptions: { - image: ContainerImage.fromRegistry('test'), - }, - }); - - // THEN - stack does not contain a LaunchConfiguration - const template = SynthUtils.synthesize(stack, { skipValidation: true }); - expect(template).not.toHaveResource('AWS::AutoScaling::LaunchConfiguration'); - expect(() => SynthUtils.synthesize(stack)).toThrow(); - }); - test('able to pass pre-defined task definition', () => { // GIVEN const stack = new Stack(); @@ -1210,7 +1169,7 @@ describe('When Network Load Balancer', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Essential: true, @@ -1357,7 +1316,7 @@ describe('When Network Load Balancer', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { ServiceRegistries: [ { ContainerName: 'web', @@ -1372,7 +1331,7 @@ describe('When Network Load Balancer', () => { ], }); - expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/l3s.test.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/l3s.test.ts index c8f29cfbc695c..db4764c1e4d6d 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/l3s.test.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/l3s.test.ts @@ -1,5 +1,4 @@ -import { ABSENT, arrayWith, objectLike, SynthUtils } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import { AutoScalingGroup } from '@aws-cdk/aws-autoscaling'; import { Certificate } from '@aws-cdk/aws-certificatemanager'; import * as ec2 from '@aws-cdk/aws-ec2'; @@ -42,16 +41,16 @@ test('test ECS loadbalanced construct', () => { }); // THEN - stack contains a load balancer and a service - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer'); + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::LoadBalancer', 1); - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 2, LaunchType: 'EC2', }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Environment: [ { Name: 'TEST_ENVIRONMENT_VARIABLE1', @@ -67,7 +66,7 @@ test('test ECS loadbalanced construct', () => { label1: 'labelValue1', label2: 'labelValue2', }, - }, + }), ], }); }); @@ -96,8 +95,8 @@ test('ApplicationLoadBalancedEc2Service desiredCount can be undefined when featu }, }); - expect(stack).toHaveResource('AWS::ECS::Service', { - DesiredCount: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + DesiredCount: Match.absent(), }); }); @@ -117,8 +116,8 @@ test('ApplicationLoadBalancedFargateService desiredCount can be undefined when f }, }); - expect(stack).toHaveResource('AWS::ECS::Service', { - DesiredCount: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + DesiredCount: Match.absent(), }); }); @@ -146,8 +145,8 @@ test('NetworkLoadBalancedEc2Service desiredCount can be undefined when feature f }, }); - expect(stack).toHaveResource('AWS::ECS::Service', { - DesiredCount: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + DesiredCount: Match.absent(), }); }); @@ -167,36 +166,11 @@ test('NetworkLoadBalancedFargateService desiredCount can be undefined when featu }, }); - expect(stack).toHaveResource('AWS::ECS::Service', { - DesiredCount: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + DesiredCount: Match.absent(), }); }); -test('set vpc instead of cluster', () => { - // GIVEN - const stack = new cdk.Stack(); - const vpc = new ec2.Vpc(stack, 'VPC'); - - // WHEN - new ecsPatterns.ApplicationLoadBalancedEc2Service(stack, 'Service', { - vpc, - memoryLimitMiB: 1024, - taskImageOptions: { - image: ecs.ContainerImage.fromRegistry('test'), - environment: { - TEST_ENVIRONMENT_VARIABLE1: 'test environment variable 1 value', - TEST_ENVIRONMENT_VARIABLE2: 'test environment variable 2 value', - }, - }, - desiredCount: 2, - }); - - // THEN - stack does not contain a LaunchConfiguration\ - const template = SynthUtils.synthesize(stack, { skipValidation: true }); - expect(template).not.toHaveResource('AWS::AutoScaling::LaunchConfiguration'); - expect(() => SynthUtils.synthesize(stack)).toThrow(); -}); - test('setting vpc and cluster throws error', () => { // GIVEN const stack = new cdk.Stack(); @@ -236,13 +210,13 @@ test('test ECS loadbalanced construct with memoryReservationMiB', () => { }); // THEN - stack contains a load balancer and a service - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer'); + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::LoadBalancer', 1); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ MemoryReservation: 1024, - }, + }), ], }); }); @@ -279,7 +253,7 @@ test('creates AWS Cloud Map service for Private DNS namespace with application l }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { ServiceRegistries: [ { ContainerName: 'web', @@ -294,7 +268,7 @@ test('creates AWS Cloud Map service for Private DNS namespace with application l ], }); - expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -355,7 +329,7 @@ test('creates AWS Cloud Map service for Private DNS namespace with network load }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { ServiceRegistries: [ { RegistryArn: { @@ -368,7 +342,7 @@ test('creates AWS Cloud Map service for Private DNS namespace with network load ], }); - expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -418,10 +392,10 @@ test('test Fargate loadbalanced construct', () => { }); // THEN - stack contains a load balancer and a service - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer'); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::LoadBalancer', 1); + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Environment: [ { Name: 'TEST_ENVIRONMENT_VARIABLE1', @@ -444,16 +418,16 @@ test('test Fargate loadbalanced construct', () => { label1: 'labelValue1', label2: 'labelValue2', }, - }, + }), ], }); - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 2, LaunchType: 'FARGATE', }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { Port: 80, Protocol: 'HTTP', }); @@ -480,9 +454,9 @@ test('test Fargate loadbalanced construct opting out of log driver creation', () }); // THEN - stack contains a load balancer and a service - expect(stack).not.toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Environment: [ { Name: 'TEST_ENVIRONMENT_VARIABLE1', @@ -493,15 +467,8 @@ test('test Fargate loadbalanced construct opting out of log driver creation', () Value: 'test environment variable 2 value', }, ], - LogConfiguration: { - LogDriver: 'awslogs', - Options: { - 'awslogs-group': { Ref: 'ServiceTaskDefwebLogGroup2A898F61' }, - 'awslogs-stream-prefix': 'Service', - 'awslogs-region': { Ref: 'AWS::Region' }, - }, - }, - }, + LogConfiguration: Match.absent(), + }), ], }); }); @@ -526,9 +493,9 @@ test('test Fargate loadbalanced construct with TLS', () => { }); // THEN - stack contains a load balancer and a service - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer'); + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::LoadBalancer', 1); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { Port: 443, Protocol: 'HTTPS', Certificates: [{ @@ -537,7 +504,7 @@ test('test Fargate loadbalanced construct with TLS', () => { SslPolicy: SslPolicy.TLS12_EXT, }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 80, Protocol: 'HTTP', TargetType: 'ip', @@ -546,12 +513,12 @@ test('test Fargate loadbalanced construct with TLS', () => { }, }); - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 1, LaunchType: 'FARGATE', }); - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'api.example.com.', HostedZoneId: { Ref: 'HostedZoneDB99F866', @@ -583,7 +550,7 @@ test('test Fargateloadbalanced construct with TLS and default certificate', () = }); // THEN - stack contains a load balancer, a service, and a certificate - expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { DomainName: 'api.example.com', DomainValidationOptions: [ { @@ -596,9 +563,9 @@ test('test Fargateloadbalanced construct with TLS and default certificate', () = ValidationMethod: 'DNS', }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer'); + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::LoadBalancer', 1); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { Port: 443, Protocol: 'HTTPS', Certificates: [{ @@ -608,12 +575,12 @@ test('test Fargateloadbalanced construct with TLS and default certificate', () = }], }); - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 1, LaunchType: 'FARGATE', }); - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'api.example.com.', HostedZoneId: { Ref: 'HostedZoneDB99F866', @@ -744,9 +711,9 @@ test('test Fargate loadbalanced construct with optional log driver input', () => }); // THEN - stack contains a load balancer and a service - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Environment: [ { Name: 'TEST_ENVIRONMENT_VARIABLE1', @@ -765,7 +732,7 @@ test('test Fargate loadbalanced construct with optional log driver input', () => 'awslogs-region': { Ref: 'AWS::Region' }, }, }, - }, + }), ], }); }); @@ -791,9 +758,9 @@ test('test Fargate loadbalanced construct with logging enabled', () => { }); // THEN - stack contains a load balancer and a service - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Environment: [ { Name: 'TEST_ENVIRONMENT_VARIABLE1', @@ -812,7 +779,7 @@ test('test Fargate loadbalanced construct with logging enabled', () => { 'awslogs-region': { Ref: 'AWS::Region' }, }, }, - }, + }), ], }); }); @@ -868,9 +835,9 @@ test('test Fargate application loadbalanced construct with taskDefinition provid memoryLimitMiB: 1024, }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Image: 'amazon/amazon-ecs-sample', Memory: 512, Name: 'passedTaskDef', @@ -880,7 +847,7 @@ test('test Fargate application loadbalanced construct with taskDefinition provid Protocol: 'tcp', }, ], - }, + }), ], }); }); @@ -954,7 +921,7 @@ test('ALBFargate - having *HealthyPercent properties', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentConfiguration: { MinimumHealthyPercent: 100, MaximumPercent: 200, @@ -981,7 +948,7 @@ test('NLBFargate - having *HealthyPercent properties', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentConfiguration: { MinimumHealthyPercent: 100, MaximumPercent: 200, @@ -1015,7 +982,7 @@ test('ALB - having *HealthyPercent properties', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentConfiguration: { MinimumHealthyPercent: 100, MaximumPercent: 200, @@ -1052,7 +1019,7 @@ test('ALB - includes provided protocol version properties', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { ProtocolVersion: 'GRPC', }); }); @@ -1083,7 +1050,7 @@ test('NLB - having *HealthyPercent properties', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentConfiguration: { MinimumHealthyPercent: 100, MaximumPercent: 200, @@ -1117,7 +1084,7 @@ test('ALB - having deployment controller', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentController: { Type: 'CODE_DEPLOY', }, @@ -1150,7 +1117,7 @@ test('NLB - having deployment controller', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentController: { Type: 'CODE_DEPLOY', }, @@ -1181,7 +1148,7 @@ test('ALB with circuit breaker', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentConfiguration: { DeploymentCircuitBreaker: { Enable: true, @@ -1218,7 +1185,7 @@ test('NLB with circuit breaker', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentConfiguration: { DeploymentCircuitBreaker: { Enable: true, @@ -1259,10 +1226,10 @@ test('NetworkLoadbalancedEC2Service accepts previously created load balancer', ( }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { LaunchType: 'EC2', }); - expect(stack).toHaveResourceLike('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { Type: 'network', }); }); @@ -1302,12 +1269,12 @@ test('NetworkLoadBalancedEC2Service accepts imported load balancer', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { LaunchType: 'EC2', - LoadBalancers: [{ ContainerName: 'Container', ContainerPort: 80 }], + LoadBalancers: [Match.objectLike({ ContainerName: 'Container', ContainerPort: 80 })], }); - expect(stack).toHaveResourceLike('AWS::ElasticLoadBalancingV2::TargetGroup'); - expect(stack).toHaveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::TargetGroup', 1); + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { LoadBalancerArn: nlb.loadBalancerArn, Port: 80, }); @@ -1345,10 +1312,10 @@ test('ApplicationLoadBalancedEC2Service accepts previously created load balancer }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { LaunchType: 'EC2', }); - expect(stack).toHaveResourceLike('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { Type: 'application', }); }); @@ -1388,12 +1355,12 @@ test('ApplicationLoadBalancedEC2Service accepts imported load balancer', () => { taskDefinition: taskDef, }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { LaunchType: 'EC2', - LoadBalancers: [{ ContainerName: 'Container', ContainerPort: 80 }], + LoadBalancers: [Match.objectLike({ ContainerName: 'Container', ContainerPort: 80 })], }); - expect(stack).toHaveResourceLike('AWS::ElasticLoadBalancingV2::TargetGroup'); - expect(stack).toHaveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::TargetGroup', 1); + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { LoadBalancerArn: alb.loadBalancerArn, Port: 80, }); @@ -1422,13 +1389,13 @@ test('test ECS loadbalanced construct default/open security group', () => { }); // THEN - Stack contains no ingress security group rules - expect(stack).toHaveResourceLike('AWS::EC2::SecurityGroup', { - SecurityGroupIngress: [{ + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { + SecurityGroupIngress: [Match.objectLike({ CidrIp: '0.0.0.0/0', FromPort: 80, IpProtocol: 'tcp', ToPort: 80, - }], + })], }); }); @@ -1461,7 +1428,7 @@ test('test ECS loadbalanced construct closed security group', () => { }); // THEN - Stack contains no ingress security group rules - expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroup', { - SecurityGroupIngress: arrayWith(objectLike({})), + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { + SecurityGroupIngress: Match.absent(), }); }); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/queue-processing-ecs-service.test.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/queue-processing-ecs-service.test.ts index 9a64cfd40428e..e5b68caa55761 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/queue-processing-ecs-service.test.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/queue-processing-ecs-service.test.ts @@ -1,5 +1,4 @@ -import { ABSENT } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import { AutoScalingGroup } from '@aws-cdk/aws-autoscaling'; import * as autoscaling from '@aws-cdk/aws-autoscaling'; import { MachineImage } from '@aws-cdk/aws-ec2'; @@ -33,12 +32,12 @@ test('test ECS queue worker service construct - with only required props', () => }); // THEN - QueueWorker is of EC2 launch type, an SQS queue is created and all default properties are set. - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 1, LaunchType: 'EC2', }); - expect(stack).toHaveResource('AWS::SQS::Queue', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::Queue', { RedrivePolicy: { deadLetterTargetArn: { 'Fn::GetAtt': [ @@ -50,13 +49,13 @@ test('test ECS queue worker service construct - with only required props', () => }, }); - expect(stack).toHaveResource('AWS::SQS::Queue', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::Queue', { MessageRetentionPeriod: 1209600, }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Environment: [ { Name: 'QUEUE_NAME', @@ -83,7 +82,7 @@ test('test ECS queue worker service construct - with only required props', () => Essential: true, Image: 'test', Memory: 512, - }, + }), ], Family: 'ServiceQueueProcessingTaskDef83DB34F1', }); @@ -112,8 +111,8 @@ test('test ECS queue worker service construct - with remove default desiredCount }); // THEN - QueueWorker is of EC2 launch type, and desiredCount is not defined on the Ec2Service. - expect(stack).toHaveResource('AWS::ECS::Service', { - DesiredCount: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + DesiredCount: Match.absent(), LaunchType: 'EC2', }); }); @@ -142,12 +141,12 @@ test('test ECS queue worker service construct - with optional props for queues', }); // THEN - QueueWorker is of EC2 launch type, an SQS queue is created and all default properties are set. - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 1, LaunchType: 'EC2', }); - expect(stack).toHaveResource('AWS::SQS::Queue', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::Queue', { RedrivePolicy: { deadLetterTargetArn: { 'Fn::GetAtt': [ @@ -160,13 +159,13 @@ test('test ECS queue worker service construct - with optional props for queues', VisibilityTimeout: 300, }); - expect(stack).toHaveResource('AWS::SQS::Queue', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::Queue', { MessageRetentionPeriod: 604800, }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Environment: [ { Name: 'QUEUE_NAME', @@ -193,7 +192,7 @@ test('test ECS queue worker service construct - with optional props for queues', Essential: true, Image: 'test', Memory: 512, - }, + }), ], Family: 'ServiceQueueProcessingTaskDef83DB34F1', }); @@ -238,7 +237,7 @@ testDeprecated('test ECS queue worker service construct - with optional props', }); // THEN - QueueWorker is of EC2 launch type, an SQS queue is created and all optional properties are set. - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 2, DeploymentConfiguration: { MinimumHealthyPercent: 60, @@ -255,13 +254,13 @@ testDeprecated('test ECS queue worker service construct - with optional props', }, }); - expect(stack).toHaveResource('AWS::SQS::Queue', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::Queue', { QueueName: 'ecs-test-sqs-queue', }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Command: [ '-c', '4', @@ -294,7 +293,7 @@ testDeprecated('test ECS queue worker service construct - with optional props', Value: '256', }, ], - }, + }), ], Family: 'ecs-task-family', }); @@ -323,7 +322,7 @@ testDeprecated('can set desiredTaskCount to 0', () => { }); // THEN - QueueWorker is of EC2 launch type, an SQS queue is created and all default properties are set. - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 0, LaunchType: 'EC2', }); @@ -375,11 +374,11 @@ test('can set custom containerName', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Name: 'my-container', - }, + }), ], }); }); @@ -412,8 +411,8 @@ test('can set capacity provider strategies', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { - LaunchType: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + LaunchType: Match.absent(), CapacityProviderStrategy: [ { CapacityProvider: { diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/scheduled-ecs-task.test.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/scheduled-ecs-task.test.ts index 9524d7a4d145c..71d48850d3b0d 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/scheduled-ecs-task.test.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/scheduled-ecs-task.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { AutoScalingGroup } from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import { MachineImage } from '@aws-cdk/aws-ec2'; @@ -32,7 +32,7 @@ test('Can create a scheduled Ec2 Task - with only required props', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { State: 'ENABLED', Targets: [ { @@ -48,7 +48,7 @@ test('Can create a scheduled Ec2 Task - with only required props', () => { ], }); - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Essential: true, @@ -101,7 +101,7 @@ test('Can create a scheduled Ec2 Task - with optional props', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Name: 'sample-scheduled-task-rule', State: 'DISABLED', Targets: [ @@ -118,7 +118,7 @@ test('Can create a scheduled Ec2 Task - with optional props', () => { ], }); - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Cpu: 2, @@ -169,7 +169,7 @@ test('Scheduled ECS Task - with securityGroups defined', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, @@ -225,7 +225,7 @@ test('Scheduled Ec2 Task - with MemoryReservation defined', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Essential: true, @@ -273,7 +273,7 @@ test('Scheduled Ec2 Task - with Command defined', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Command: [ diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.multiple-network-load-balanced-fargate-service.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.multiple-network-load-balanced-fargate-service.expected.json index 643ff38905d69..91413b0f0fd6f 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.multiple-network-load-balanced-fargate-service.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.multiple-network-load-balanced-fargate-service.expected.json @@ -458,7 +458,7 @@ "myServicelb2listener2ECSTargetGroupweb90Group6841F924": { "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", "Properties": { - "Port": 80, + "Port": 90, "Protocol": "TCP", "TargetType": "ip", "VpcId": { diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.expected.json index ae18adac13bbf..8af962023d1d2 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.expected.json @@ -604,6 +604,15 @@ } ], "Essential": true, + "HealthCheck": { + "Command": [ + "CMD-SHELL", + "curl -f http://localhost/ || exit 1" + ], + "Interval": 720, + "Retries": 34, + "Timeout": 5 + }, "Image": { "Fn::Join": [ "", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.ts index 5b0d74f9e3ceb..4877a7d211747 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; -import { App, Stack } from '@aws-cdk/core'; +import { App, Stack, Duration } from '@aws-cdk/core'; import { QueueProcessingFargateService } from '../../lib'; @@ -14,6 +14,11 @@ new QueueProcessingFargateService(stack, 'PublicQueueService', { memoryLimitMiB: 512, image: new ecs.AssetImage(path.join(__dirname, '..', 'sqs-reader')), assignPublicIp: true, + healthCheck: { + command: ['CMD-SHELL', 'curl -f http://localhost/ || exit 1'], + interval: Duration.minutes(12), + retries: 34, + }, }); app.synth(); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.special-listener.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.special-listener.expected.json index 91c1f4e575e23..1af60f7eaf55c 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.special-listener.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.special-listener.expected.json @@ -404,7 +404,7 @@ "FargateNlbServiceLBPublicListenerECSGroup7501571D": { "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", "Properties": { - "Port": 80, + "Port": 2015, "Protocol": "TCP", "TargetType": "ip", "VpcId": { diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/load-balanced-fargate-service-v2.test.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/load-balanced-fargate-service-v2.test.ts index 902d5412ee8bf..b196f4b0616b1 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/load-balanced-fargate-service-v2.test.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/load-balanced-fargate-service-v2.test.ts @@ -1,9 +1,10 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import { Vpc } from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; +import { ContainerImage } from '@aws-cdk/aws-ecs'; import { CompositePrincipal, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import { Duration, Stack } from '@aws-cdk/core'; -import { ApplicationMultipleTargetGroupsFargateService, NetworkMultipleTargetGroupsFargateService, ApplicationLoadBalancedFargateService } from '../../lib'; +import { ApplicationLoadBalancedFargateService, ApplicationMultipleTargetGroupsFargateService, NetworkLoadBalancedFargateService, NetworkMultipleTargetGroupsFargateService } from '../../lib'; describe('When Application Load Balancer', () => { test('test Fargate loadbalanced construct with default settings', () => { @@ -21,9 +22,9 @@ describe('When Application Load Balancer', () => { }); // THEN - stack contains a load balancer and a service - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer'); + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::LoadBalancer', 1); - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 1, LaunchType: 'FARGATE', LoadBalancers: [ @@ -37,9 +38,9 @@ describe('When Application Load Balancer', () => { ], }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Image: 'test', LogConfiguration: { LogDriver: 'awslogs', @@ -60,7 +61,7 @@ describe('When Application Load Balancer', () => { Protocol: 'tcp', }, ], - }, + }), ], Cpu: '256', ExecutionRoleArn: { @@ -135,7 +136,7 @@ describe('When Application Load Balancer', () => { }); // THEN - stack contains a load balancer and a service - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 3, EnableECSManagedTags: true, HealthCheckGracePeriodSeconds: 2, @@ -182,7 +183,7 @@ describe('When Application Load Balancer', () => { ServiceName: 'myService', }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Environment: [ @@ -314,11 +315,11 @@ describe('When Application Load Balancer', () => { }); // THEN - stack contains a load balancer and a service - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { Name: 'alb-test-load-balancer', }); - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 1, LaunchType: 'FARGATE', LoadBalancers: [ @@ -332,9 +333,9 @@ describe('When Application Load Balancer', () => { ], }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Image: 'test', LogConfiguration: { LogDriver: 'awslogs', @@ -355,7 +356,7 @@ describe('When Application Load Balancer', () => { Protocol: 'tcp', }, ], - }, + }), ], Cpu: '256', ExecutionRoleArn: { @@ -390,9 +391,9 @@ describe('When Network Load Balancer', () => { }); // THEN - stack contains a load balancer and a service - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer'); + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::LoadBalancer', 1); - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 1, LaunchType: 'FARGATE', LoadBalancers: [ @@ -406,9 +407,9 @@ describe('When Network Load Balancer', () => { ], }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Image: 'test', LogConfiguration: { LogDriver: 'awslogs', @@ -429,7 +430,7 @@ describe('When Network Load Balancer', () => { Protocol: 'tcp', }, ], - }, + }), ], Cpu: '256', ExecutionRoleArn: { @@ -500,7 +501,7 @@ describe('When Network Load Balancer', () => { }); // THEN - stack contains a load balancer and a service - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 3, EnableECSManagedTags: true, HealthCheckGracePeriodSeconds: 2, @@ -546,7 +547,7 @@ describe('When Network Load Balancer', () => { ServiceName: 'myService', }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Environment: [ @@ -661,4 +662,75 @@ describe('When Network Load Balancer', () => { }); }).toThrow(/You must specify one of: taskDefinition or image/); }); + + test('test Fargate networkloadbalanced construct with custom Port', () => { + // GIVEN + const stack = new Stack(); + const vpc = new Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + + new NetworkLoadBalancedFargateService(stack, 'NLBService', { + cluster: cluster, + memoryLimitMiB: 1024, + cpu: 512, + taskImageOptions: { + image: ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + containerPort: 81, + }, + listenerPort: 8181, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { + Port: 81, + Protocol: 'TCP', + TargetType: 'ip', + VpcId: { + Ref: 'VPCB9E5F0B4', + }, + }); + }); + + test('test Fargate multinetworkloadbalanced construct with custom Port', () => { + // GIVEN + const stack = new Stack(); + const vpc = new Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + + new NetworkMultipleTargetGroupsFargateService(stack, 'Service', { + cluster, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('test'), + }, + }); + + + new NetworkMultipleTargetGroupsFargateService(stack, 'NLBService', { + cluster: cluster, + memoryLimitMiB: 1024, + cpu: 512, + taskImageOptions: { + image: ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + }, + loadBalancers: [ + { + name: 'lb1', + listeners: [ + { name: 'listener1', port: 8181 }, + ], + }, + ], + targetGroups: [{ + containerPort: 81, + }], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { + Port: 81, + Protocol: 'TCP', + TargetType: 'ip', + VpcId: { + Ref: 'VPCB9E5F0B4', + }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/load-balanced-fargate-service.test.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/load-balanced-fargate-service.test.ts index 0f4c0ab29ba86..70763a1bc2277 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/load-balanced-fargate-service.test.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/load-balanced-fargate-service.test.ts @@ -1,5 +1,4 @@ -import { SynthUtils } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import { AutoScalingGroup } from '@aws-cdk/aws-autoscaling'; import { DnsValidatedCertificate } from '@aws-cdk/aws-certificatemanager'; import * as ec2 from '@aws-cdk/aws-ec2'; @@ -27,7 +26,7 @@ test('setting loadBalancerType to Network creates an NLB Public', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { Type: 'network', Scheme: 'internet-facing', }); @@ -49,7 +48,7 @@ test('setting loadBalancerType to Network and publicLoadBalancer to false create }); // THEN - expect(stack).toHaveResourceLike('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { Type: 'network', Scheme: 'internal', }); @@ -95,8 +94,9 @@ test('setting executionRole updated taskDefinition with given execution role', ( }); // THEN - const serviceTaskDefinition = SynthUtils.synthesize(stack).template.Resources.ServiceTaskDef1922A00F; - expect(serviceTaskDefinition.Properties.ExecutionRoleArn).toEqual({ 'Fn::GetAtt': ['ExecutionRole605A040B', 'Arn'] }); + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + ExecutionRoleArn: { 'Fn::GetAtt': ['ExecutionRole605A040B', 'Arn'] }, + }); }); test('setting taskRole updated taskDefinition with given task role', () => { @@ -122,8 +122,9 @@ test('setting taskRole updated taskDefinition with given task role', () => { }); // THEN - const serviceTaskDefinition = SynthUtils.synthesize(stack).template.Resources.ServiceTaskDef1922A00F; - expect(serviceTaskDefinition.Properties.TaskRoleArn).toEqual({ 'Fn::GetAtt': ['taskRoleTest9DA66B6E', 'Arn'] }); + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + TaskRoleArn: { 'Fn::GetAtt': ['taskRoleTest9DA66B6E', 'Arn'] }, + }); }); test('setting containerName updates container name with given name', () => { @@ -142,8 +143,13 @@ test('setting containerName updates container name with given name', () => { }); // THEN - const serviceTaskDefinition = SynthUtils.synthesize(stack).template.Resources.ServiceTaskDef1922A00F; - expect(serviceTaskDefinition.Properties.ContainerDefinitions[0].Name).toEqual('bob'); + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + Match.objectLike({ + Name: 'bob', + }), + ], + }); }); test('not setting containerName updates container name with default', () => { @@ -161,8 +167,13 @@ test('not setting containerName updates container name with default', () => { }); // THEN - const serviceTaskDefinition = SynthUtils.synthesize(stack).template.Resources.ServiceTaskDef1922A00F; - expect(serviceTaskDefinition.Properties.ContainerDefinitions[0].Name).toEqual('web'); + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + Match.objectLike({ + Name: 'web', + }), + ], + }); }); test('setting servicename updates service name with given name', () => { @@ -180,8 +191,9 @@ test('setting servicename updates service name with given name', () => { serviceName: 'bob', }); // THEN - const serviceTaskDefinition = SynthUtils.synthesize(stack).template.Resources.Service9571FDD8; - expect(serviceTaskDefinition.Properties.ServiceName).toEqual('bob'); + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + ServiceName: 'bob', + }); }); test('not setting servicename updates service name with default', () => { @@ -199,8 +211,9 @@ test('not setting servicename updates service name with default', () => { }); // THEN - const serviceTaskDefinition = SynthUtils.synthesize(stack).template.Resources.Service9571FDD8; - expect(serviceTaskDefinition.Properties.ServiceName).toBeUndefined(); + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + ServiceName: Match.absent(), + }); }); test('setting healthCheckGracePeriod works', () => { @@ -215,8 +228,9 @@ test('setting healthCheckGracePeriod works', () => { healthCheckGracePeriod: cdk.Duration.seconds(600), }); // THEN - const serviceTaskDefinition = SynthUtils.synthesize(stack).template.Resources.Service9571FDD8; - expect(serviceTaskDefinition.Properties.HealthCheckGracePeriodSeconds).toEqual(600); + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + HealthCheckGracePeriodSeconds: 600, + }); }); test('selecting correct vpcSubnets', () => { @@ -248,7 +262,7 @@ test('selecting correct vpcSubnets', () => { }, }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { NetworkConfiguration: { AwsvpcConfiguration: { Subnets: [ @@ -275,7 +289,7 @@ test('target group uses HTTP/80 as default', () => { }, }); // THEN - expect(stack).toHaveResourceLike('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 80, Protocol: 'HTTP', }); @@ -293,7 +307,7 @@ test('target group uses HTTPS/443 when configured', () => { targetProtocol: ApplicationProtocol.HTTPS, }); // THEN - expect(stack).toHaveResourceLike('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 443, Protocol: 'HTTPS', }); @@ -311,7 +325,7 @@ test('setting platform version', () => { platformVersion: ecs.FargatePlatformVersion.VERSION1_4, }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { PlatformVersion: ecs.FargatePlatformVersion.VERSION1_4, }); }); @@ -347,15 +361,15 @@ test('test load balanced service with family defined', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 2, LaunchType: 'FARGATE', ServiceName: 'fargate-test-service', }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Environment: [ { Name: 'TEST_ENVIRONMENT_VARIABLE1', @@ -367,7 +381,7 @@ test('test load balanced service with family defined', () => { }, ], Image: '/aws/aws-example-app', - }, + }), ], Family: 'fargate-task-family', }); @@ -388,7 +402,7 @@ test('setting ALB deployment controller', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentController: { Type: 'CODE_DEPLOY', }, @@ -410,7 +424,7 @@ test('setting NLB deployment controller', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentController: { Type: 'CODE_DEPLOY', }, @@ -430,7 +444,7 @@ test('setting ALB circuitBreaker works', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentConfiguration: { DeploymentCircuitBreaker: { Enable: true, @@ -456,7 +470,7 @@ test('setting NLB circuitBreaker works', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentConfiguration: { DeploymentCircuitBreaker: { Enable: true, @@ -486,11 +500,11 @@ test('setting NLB special listener port to create the listener', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ - { + Match.objectLike({ Type: 'forward', - }, + }), ], Port: 2015, Protocol: 'TCP', @@ -514,11 +528,11 @@ test('setting ALB special listener port to create the listener', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ - { + Match.objectLike({ Type: 'forward', - }, + }), ], Port: 2015, Protocol: 'HTTP', @@ -547,11 +561,11 @@ test('setting ALB HTTPS protocol to create the listener on 443', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ - { + Match.objectLike({ Type: 'forward', - }, + }), ], Port: 443, Protocol: 'HTTPS', @@ -580,7 +594,7 @@ test('setting ALB HTTPS correctly sets the recordset name', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'test.domain.com.', }); }); @@ -608,7 +622,7 @@ test('setting ALB cname option correctly sets the recordset type', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'test.domain.com.', Type: 'CNAME', }); @@ -637,7 +651,7 @@ test('setting ALB record type to NONE correctly omits the recordset', () => { }); // THEN - expect(stack).not.toHaveResource('AWS::Route53::RecordSet'); + Template.fromStack(stack).resourceCountIs('AWS::Route53::RecordSet', 0); }); @@ -663,7 +677,7 @@ test('setting NLB cname option correctly sets the recordset type', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'test.domain.com.', Type: 'CNAME', }); @@ -691,7 +705,7 @@ test('setting NLB record type to NONE correctly omits the recordset', () => { }); // THEN - expect(stack).not.toHaveResource('AWS::Route53::RecordSet'); + Template.fromStack(stack).resourceCountIs('AWS::Route53::RecordSet', 0); }); test('setting ALB HTTP protocol to create the listener on 80', () => { @@ -711,11 +725,11 @@ test('setting ALB HTTP protocol to create the listener on 80', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ - { + Match.objectLike({ Type: 'forward', - }, + }), ], Port: 80, Protocol: 'HTTP', @@ -738,11 +752,11 @@ test('setting ALB without any protocol or listenerPort to create the listener on }); // THEN - expect(stack).toHaveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ - { + Match.objectLike({ Type: 'forward', - }, + }), ], Port: 80, Protocol: 'HTTP', @@ -765,11 +779,11 @@ test('passing in existing network load balancer to NLB Fargate Service', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { LaunchType: 'FARGATE', }); - expect(stack).toHaveResourceLike('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { Type: 'network', }); }); @@ -813,14 +827,14 @@ test('passing in imported network load balancer and resources to NLB Fargate ser }); // THEN - expect(stack2).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack2).hasResourceProperties('AWS::ECS::Service', { LaunchType: 'FARGATE', - LoadBalancers: [{ ContainerName: 'myContainer', ContainerPort: 80 }], + LoadBalancers: [Match.objectLike({ ContainerName: 'myContainer', ContainerPort: 80 })], }); - expect(stack2).toHaveResourceLike('AWS::ElasticLoadBalancingV2::TargetGroup'); + Template.fromStack(stack2).resourceCountIs('AWS::ElasticLoadBalancingV2::TargetGroup', 1); - expect(stack2).toHaveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack2).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { LoadBalancerArn: nlb2.loadBalancerArn, Port: 80, }); @@ -845,11 +859,11 @@ test('passing in previously created application load balancer to ALB Fargate Ser }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { LaunchType: 'FARGATE', }); - expect(stack).toHaveResourceLike('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { Type: 'application', }); }); @@ -890,14 +904,14 @@ test('passing in imported application load balancer and resources to ALB Fargate }); // THEN - expect(stack1).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack1).hasResourceProperties('AWS::ECS::Service', { LaunchType: 'FARGATE', - LoadBalancers: [{ ContainerName: 'Container', ContainerPort: 80 }], + LoadBalancers: [Match.objectLike({ ContainerName: 'Container', ContainerPort: 80 })], }); - expect(stack1).toHaveResourceLike('AWS::ElasticLoadBalancingV2::TargetGroup'); + Template.fromStack(stack1).resourceCountIs('AWS::ElasticLoadBalancingV2::TargetGroup', 1); - expect(stack1).toHaveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack1).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { LoadBalancerArn: alb.loadBalancerArn, Port: 80, }); @@ -925,11 +939,11 @@ test('passing in previously created security groups to ALB Fargate Service', () }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { LaunchType: 'FARGATE', }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Example', GroupName: 'Rolly', SecurityGroupEgress: [ @@ -973,9 +987,7 @@ test('domainName and domainZone not required for HTTPS listener with provided ce }); // THEN - expect(stack).not.toHaveResourceLike('AWS::Route53::RecordSet', { - Name: 'test.domain.com.', - }); + Template.fromStack(stack).resourceCountIs('AWS::Route53::RecordSet', 0); }); test('test ALB load balanced service with docker labels defined', () => { @@ -994,15 +1006,15 @@ test('test ALB load balanced service with docker labels defined', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Image: '/aws/aws-example-app', DockerLabels: { label1: 'labelValue1', label2: 'labelValue2', }, - }, + }), ], }); }); @@ -1023,15 +1035,15 @@ test('test Network load balanced service with docker labels defined', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Image: '/aws/aws-example-app', DockerLabels: { label1: 'labelValue1', label2: 'labelValue2', }, - }, + }), ], }); }); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/queue-processing-fargate-service.test.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/queue-processing-fargate-service.test.ts index 6f91b633d1249..b6ca462f52a2b 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/queue-processing-fargate-service.test.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/queue-processing-fargate-service.test.ts @@ -1,5 +1,4 @@ -import { ABSENT } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import { AutoScalingGroup } from '@aws-cdk/aws-autoscaling'; import { MachineImage } from '@aws-cdk/aws-ec2'; import * as ec2 from '@aws-cdk/aws-ec2'; @@ -32,12 +31,12 @@ test('test fargate queue worker service construct - with only required props', ( }); // THEN - QueueWorker is of FARGATE launch type, an SQS queue is created and all default properties are set. - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 1, LaunchType: 'FARGATE', }); - expect(stack).toHaveResource('AWS::SQS::Queue', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::Queue', { RedrivePolicy: { deadLetterTargetArn: { 'Fn::GetAtt': [ @@ -49,11 +48,11 @@ test('test fargate queue worker service construct - with only required props', ( }, }); - expect(stack).toHaveResource('AWS::SQS::Queue', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::Queue', { MessageRetentionPeriod: 1209600, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -77,9 +76,9 @@ test('test fargate queue worker service construct - with only required props', ( }, }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Environment: [ { Name: 'QUEUE_NAME', @@ -104,7 +103,7 @@ test('test fargate queue worker service construct - with only required props', ( }, }, Image: 'test', - }, + }), ], Family: 'ServiceQueueProcessingTaskDef83DB34F1', }); @@ -126,8 +125,8 @@ test('test fargate queue worker service construct - with remove default desiredC }); // THEN - QueueWorker is of FARGATE launch type, and desiredCount is not defined on the FargateService. - expect(stack).toHaveResource('AWS::ECS::Service', { - DesiredCount: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + DesiredCount: Match.absent(), LaunchType: 'FARGATE', }); }); @@ -156,12 +155,12 @@ test('test fargate queue worker service construct - with optional props for queu }); // THEN - QueueWorker is of FARGATE launch type, an SQS queue is created and all default properties are set. - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 1, LaunchType: 'FARGATE', }); - expect(stack).toHaveResource('AWS::SQS::Queue', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::Queue', { RedrivePolicy: { deadLetterTargetArn: { 'Fn::GetAtt': [ @@ -174,11 +173,11 @@ test('test fargate queue worker service construct - with optional props for queu VisibilityTimeout: 300, }); - expect(stack).toHaveResource('AWS::SQS::Queue', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::Queue', { MessageRetentionPeriod: 604800, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -202,9 +201,9 @@ test('test fargate queue worker service construct - with optional props for queu }, }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Environment: [ { Name: 'QUEUE_NAME', @@ -229,7 +228,7 @@ test('test fargate queue worker service construct - with optional props for queu }, }, Image: 'test', - }, + }), ], Family: 'ServiceQueueProcessingTaskDef83DB34F1', }); @@ -276,7 +275,7 @@ test('test Fargate queue worker service construct - without desiredCount specifi }); // THEN - QueueWorker is of FARGATE launch type, an SQS queue is created and all optional properties are set. - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentConfiguration: { MinimumHealthyPercent: 60, MaximumPercent: 150, @@ -289,16 +288,16 @@ test('test Fargate queue worker service construct - without desiredCount specifi }, }); - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { MaxCapacity: 5, MinCapacity: 2, }); - expect(stack).toHaveResource('AWS::SQS::Queue', { QueueName: 'fargate-test-sqs-queue' }); + Template.fromStack(stack).hasResourceProperties('AWS::SQS::Queue', { QueueName: 'fargate-test-sqs-queue' }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Command: [ '-c', '4', @@ -324,7 +323,7 @@ test('test Fargate queue worker service construct - without desiredCount specifi }, ], Image: 'test', - }, + }), ], Family: 'fargate-task-family', }); @@ -369,7 +368,7 @@ testDeprecated('test Fargate queue worker service construct - with optional prop }); // THEN - QueueWorker is of FARGATE launch type, an SQS queue is created and all optional properties are set. - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DesiredCount: 2, DeploymentConfiguration: { MinimumHealthyPercent: 60, @@ -387,11 +386,11 @@ testDeprecated('test Fargate queue worker service construct - with optional prop }, }); - expect(stack).toHaveResource('AWS::SQS::Queue', { QueueName: 'fargate-test-sqs-queue' }); + Template.fromStack(stack).hasResourceProperties('AWS::SQS::Queue', { QueueName: 'fargate-test-sqs-queue' }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Command: [ '-c', '4', @@ -417,7 +416,7 @@ testDeprecated('test Fargate queue worker service construct - with optional prop }, ], Image: 'test', - }, + }), ], Family: 'fargate-task-family', }); @@ -443,11 +442,11 @@ test('can set custom containerName', () => { image: ecs.ContainerImage.fromRegistry('test'), }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Name: 'my-container', - }, + }), ], }); }); @@ -482,7 +481,7 @@ test('can set custom networking options', () => { }); // THEN - NetworkConfiguration is created with the specific security groups and selected subnets - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { LaunchType: 'FARGATE', NetworkConfiguration: { AwsvpcConfiguration: { @@ -521,7 +520,7 @@ test('can set use public IP', () => { }); // THEN - The Subnets defaults to Public and AssignPublicIp settings change to ENABLED - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { LaunchType: 'FARGATE', NetworkConfiguration: { AwsvpcConfiguration: { @@ -573,8 +572,8 @@ test('can set capacity provider strategies', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { - LaunchType: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + LaunchType: Match.absent(), CapacityProviderStrategy: [ { CapacityProvider: 'FARGATE_SPOT', diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/scheduled-fargate-task.test.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/scheduled-fargate-task.test.ts index 5d37bba01af2f..696a413ad2def 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/scheduled-fargate-task.test.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/scheduled-fargate-task.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; import * as events from '@aws-cdk/aws-events'; @@ -21,7 +21,7 @@ test('Can create a scheduled Fargate Task - with only required props', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { State: 'ENABLED', Targets: [ { @@ -56,7 +56,7 @@ test('Can create a scheduled Fargate Task - with only required props', () => { ], }); - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Essential: true, @@ -100,7 +100,7 @@ test('Can create a scheduled Fargate Task - with optional props', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Name: 'sample-scheduled-task-rule', State: 'DISABLED', Targets: [ @@ -136,7 +136,7 @@ test('Can create a scheduled Fargate Task - with optional props', () => { ], }); - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Environment: [ @@ -180,7 +180,7 @@ test('Scheduled Fargate Task - with MemoryReservation defined', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Essential: true, @@ -219,7 +219,7 @@ test('Scheduled Fargate Task - with Command defined', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Command: [ @@ -268,22 +268,22 @@ test('Scheduled Fargate Task - with subnetSelection defined', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ - { - EcsParameters: { + Match.objectLike({ + EcsParameters: Match.objectLike({ NetworkConfiguration: { - AwsVpcConfiguration: { + AwsVpcConfiguration: Match.objectLike({ AssignPublicIp: 'ENABLED', Subnets: [ { Ref: 'VpcPublicSubnet1Subnet5C2D37C4', }, ], - }, + }), }, - }, - }, + }), + }), ], }); }); @@ -305,7 +305,7 @@ test('Scheduled Fargate Task - with platformVersion defined', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, @@ -359,7 +359,7 @@ test('Scheduled Fargate Task - with securityGroups defined', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 602e046ba2d61..d0d091eda612e 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -96,6 +96,15 @@ const cluster = new ecs.Cluster(this, 'Cluster', { }); ``` +The following code imports an existing cluster using the ARN which can be used to +import an Amazon ECS service either EC2 or Fargate. + +```ts +const clusterArn = 'arn:aws:ecs:us-east-1:012345678910:cluster/clusterName'; + +const cluster = ecs.Cluster.fromClusterArn(this, 'Cluster', clusterArn); +``` + To use tasks with Amazon EC2 launch-type, you have to add capacity to the cluster in order for tasks to be scheduled on your instances. Typically, you add an AutoScalingGroup with instances running the latest @@ -418,6 +427,7 @@ const newContainer = taskDefinition.addContainer('container', { secrets: { // Retrieved from AWS Secrets Manager or AWS Systems Manager Parameter Store at container start-up. SECRET: ecs.Secret.fromSecretsManager(secret), DB_PASSWORD: ecs.Secret.fromSecretsManager(dbSecret, 'password'), // Reference a specific JSON field, (requires platform version 1.4.0 or later for Fargate tasks) + API_KEY: ecs.Secret.fromSecretsManagerVersion(secret, { versionId: '12345' }, 'apiKey'), // Reference a specific version of the secret by its version id or version stage (requires platform version 1.4.0 or later for Fargate tasks) PARAMETER: ecs.Secret.fromSsmParameter(parameter), }, }); @@ -447,6 +457,50 @@ taskDefinition.addContainer('container', { }); ``` +### Using Windows containers on Fargate + +AWS Fargate supports Amazon ECS Windows containers. For more details, please see this [blog post](https://aws.amazon.com/tw/blogs/containers/running-windows-containers-with-amazon-ecs-on-aws-fargate/) + +```ts +// Create a Task Definition for the Windows container to start +const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', { + runtimePlatform: { + operatingSystemFamily: ecs.OperatingSystemFamily.WINDOWS_SERVER_2019_CORE, + cpuArchitecture: ecs.CpuArchitecture.X86_64, + }, + cpu: 1024, + memoryLimitMiB: 2048, +}); + +taskDefinition.addContainer('windowsservercore', { + logging: ecs.LogDriver.awsLogs({ streamPrefix: 'win-iis-on-fargate' }), + portMappings: [{ containerPort: 80 }], + image: ecs.ContainerImage.fromRegistry('mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019'), +}); +``` + +### Using Graviton2 with Fargate + +AWS Graviton2 supports AWS Fargate. For more details, please see this [blog post](https://aws.amazon.com/blogs/aws/announcing-aws-graviton2-support-for-aws-fargate-get-up-to-40-better-price-performance-for-your-serverless-containers/) + +```ts +// Create a Task Definition for running container on Graviton Runtime. +const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', { + runtimePlatform: { + operatingSystemFamily: ecs.OperatingSystemFamily.LINUX, + cpuArchitecture: ecs.CpuArchitecture.ARM64, + }, + cpu: 1024, + memoryLimitMiB: 2048, +}); + +taskDefinition.addContainer('webarm64', { + logging: ecs.LogDriver.awsLogs({ streamPrefix: 'graviton2-on-fargate' }), + portMappings: [{ containerPort: 80 }], + image: ecs.ContainerImage.fromRegistry('public.ecr.aws/nginx/nginx:latest-arm64v8'), +}); +``` + ## Service A `Service` instantiates a `TaskDefinition` on a `Cluster` a given number of diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 1809000064f12..4e5de4c90eacf 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -5,10 +5,10 @@ import * as elb from '@aws-cdk/aws-elasticloadbalancing'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as iam from '@aws-cdk/aws-iam'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; -import { Annotations, Duration, IResolvable, IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; +import { Annotations, Duration, IResolvable, IResource, Lazy, Resource, Stack, ArnFormat } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { LoadBalancerTargetOptions, NetworkMode, TaskDefinition } from '../base/task-definition'; -import { ICluster, CapacityProviderStrategy, ExecuteCommandLogging } from '../cluster'; +import { ICluster, CapacityProviderStrategy, ExecuteCommandLogging, Cluster } from '../cluster'; import { ContainerDefinition, Protocol } from '../container-definition'; import { CfnService } from '../ecs.generated'; import { ScalableTaskCount } from './scalable-task-count'; @@ -315,6 +315,46 @@ export interface IBaseService extends IService { */ export abstract class BaseService extends Resource implements IBaseService, elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, elb.ILoadBalancerTarget { + /** + * Import an existing ECS/Fargate Service using the service cluster format. + * The format is the "new" format "arn:aws:ecs:region:aws_account_id:service/cluster-name/service-name". + * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html#ecs-resource-ids + */ + public static fromServiceArnWithCluster(scope: Construct, id: string, serviceArn: string): IBaseService { + const stack = Stack.of(scope); + const arn = stack.splitArn(serviceArn, ArnFormat.SLASH_RESOURCE_NAME); + const resourceName = arn.resourceName; + if (!resourceName) { + throw new Error('Missing resource Name from service ARN: ${serviceArn}'); + } + const resourceNameParts = resourceName.split('/'); + if (resourceNameParts.length !== 2) { + throw new Error(`resource name ${resourceName} from service ARN: ${serviceArn} is not using the ARN cluster format`); + } + const clusterName = resourceNameParts[0]; + const serviceName = resourceNameParts[1]; + + const clusterArn = Stack.of(scope).formatArn({ + partition: arn.partition, + region: arn.region, + account: arn.account, + service: 'ecs', + resource: 'cluster', + resourceName: clusterName, + }); + + const cluster = Cluster.fromClusterArn(scope, `${id}Cluster`, clusterArn); + + class Import extends Resource implements IBaseService { + public readonly serviceArn = serviceArn; + public readonly serviceName = serviceName; + public readonly cluster = cluster; + } + + return new Import(scope, id, { + environmentFromArn: serviceArn, + }); + } /** * The security groups which manage the allowed network traffic for the service. @@ -470,7 +510,7 @@ export abstract class BaseService extends Resource resources: ['*'], })); - const logGroupArn = logConfiguration?.cloudWatchLogGroup ? `arn:aws:logs:${this.stack.region}:${this.stack.account}:log-group:${logConfiguration.cloudWatchLogGroup.logGroupName}:*` : '*'; + const logGroupArn = logConfiguration?.cloudWatchLogGroup ? `arn:${this.stack.partition}:logs:${this.stack.region}:${this.stack.account}:log-group:${logConfiguration.cloudWatchLogGroup.logGroupName}:*` : '*'; this.taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({ actions: [ 'logs:CreateLogStream', @@ -491,14 +531,14 @@ export abstract class BaseService extends Resource actions: [ 's3:PutObject', ], - resources: [`arn:aws:s3:::${logConfiguration.s3Bucket.bucketName}/*`], + resources: [`arn:${this.stack.partition}:s3:::${logConfiguration.s3Bucket.bucketName}/*`], })); if (logConfiguration.s3EncryptionEnabled) { this.taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({ actions: [ 's3:GetEncryptionConfiguration', ], - resources: [`arn:aws:s3:::${logConfiguration.s3Bucket.bucketName}`], + resources: [`arn:${this.stack.partition}:s3:::${logConfiguration.s3Bucket.bucketName}`], })); } } @@ -518,7 +558,7 @@ export abstract class BaseService extends Resource 'kms:*', ], resources: ['*'], - principals: [new iam.ArnPrincipal(`arn:aws:iam::${this.stack.account}:root`)], + principals: [new iam.ArnPrincipal(`arn:${this.stack.partition}:iam::${this.stack.account}:root`)], })); if (logging === ExecuteCommandLogging.DEFAULT || this.cluster.executeCommandConfiguration?.logConfiguration?.cloudWatchEncryptionEnabled) { @@ -533,7 +573,7 @@ export abstract class BaseService extends Resource resources: ['*'], principals: [new iam.ServicePrincipal(`logs.${this.stack.region}.amazonaws.com`)], conditions: { - ArnLike: { 'kms:EncryptionContext:aws:logs:arn': `arn:aws:logs:${this.stack.region}:${this.stack.account}:*` }, + ArnLike: { 'kms:EncryptionContext:aws:logs:arn': `arn:${this.stack.partition}:logs:${this.stack.region}:${this.stack.account}:*` }, }, })); } 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 9521142e650c5..a42d98fb5c656 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts @@ -8,6 +8,7 @@ import { FirelensLogRouter, FirelensLogRouterDefinitionOptions, FirelensLogRoute import { AwsLogDriver } from '../log-drivers/aws-log-driver'; import { PlacementConstraint } from '../placement'; import { ProxyConfiguration } from '../proxy-configuration/proxy-configuration'; +import { RuntimePlatform } from '../runtime-platform'; import { ImportedTaskDefinition } from './_imported-task-definition'; /** @@ -208,6 +209,15 @@ export interface TaskDefinitionProps extends CommonTaskDefinitionProps { * @default - Undefined, in which case, the task will receive 20GiB ephemeral storage. */ readonly ephemeralStorageGiB?: number; + + /** + * The operating system that your task definitions are running on. + * A runtimePlatform is supported only for tasks using the Fargate launch type. + * + * + * @default - Undefined. + */ + readonly runtimePlatform?: RuntimePlatform; } /** @@ -369,6 +379,8 @@ export class TaskDefinition extends TaskDefinitionBase { private _referencesSecretJsonField?: boolean; + private runtimePlatform?: RuntimePlatform; + /** * Constructs a new instance of the TaskDefinition class. */ @@ -405,6 +417,10 @@ export class TaskDefinition extends TaskDefinitionBase { throw new Error(`External tasks can only have Bridge network mode, got: ${this.networkMode}`); } + if (!this.isFargateCompatible && props.runtimePlatform) { + throw new Error('Cannot specify runtimePlatform in non-Fargate compatible tasks'); + } + this._executionRole = props.executionRole; this.taskRole = props.taskRole || new iam.Role(this, 'TaskRole', { @@ -417,6 +433,15 @@ export class TaskDefinition extends TaskDefinitionBase { this.ephemeralStorageGiB = props.ephemeralStorageGiB; + // validate the cpu and memory size for the Windows operation system family. + if (props.runtimePlatform?.operatingSystemFamily?._operatingSystemFamily.includes('WINDOWS')) { + // We know that props.cpu and props.memoryMiB are defined because an error would have been thrown previously if they were not. + // But, typescript is not able to figure this out, so using the `!` operator here to let the type-checker know they are defined. + this.checkFargateWindowsBasedTasksSize(props.cpu!, props.memoryMiB!, props.runtimePlatform!); + } + + this.runtimePlatform = props.runtimePlatform; + const taskDef = new CfnTaskDefinition(this, 'Resource', { containerDefinitions: Lazy.any({ produce: () => this.renderContainers() }, { omitEmptyArray: true }), volumes: Lazy.any({ produce: () => this.renderVolumes() }, { omitEmptyArray: true }), @@ -445,6 +470,10 @@ export class TaskDefinition extends TaskDefinitionBase { ephemeralStorage: this.ephemeralStorageGiB ? { sizeInGiB: this.ephemeralStorageGiB, } : undefined, + runtimePlatform: this.isFargateCompatible && this.runtimePlatform ? { + cpuArchitecture: this.runtimePlatform?.cpuArchitecture?._cpuArchitecture, + operatingSystemFamily: this.runtimePlatform?.operatingSystemFamily?._operatingSystemFamily, + } : undefined, }); if (props.placementConstraints) { @@ -697,6 +726,24 @@ export class TaskDefinition extends TaskDefinitionBase { return this.containers.map(x => x.renderContainerDefinition()); } + + private checkFargateWindowsBasedTasksSize(cpu: string, memory: string, runtimePlatform: RuntimePlatform) { + if (Number(cpu) === 1024) { + if (Number(memory) < 1024 || Number(memory) > 8192 || (Number(memory)% 1024 !== 0)) { + throw new Error(`If provided cpu is ${cpu}, then memoryMiB must have a min of 1024 and a max of 8192, in 1024 increments. Provided memoryMiB was ${Number(memory)}.`); + } + } else if (Number(cpu) === 2048) { + if (Number(memory) < 4096 || Number(memory) > 16384 || (Number(memory) % 1024 !== 0)) { + throw new Error(`If provided cpu is ${cpu}, then memoryMiB must have a min of 4096 and max of 16384, in 1024 increments. Provided memoryMiB ${Number(memory)}.`); + } + } else if (Number(cpu) === 4096) { + if (Number(memory) < 8192 || Number(memory) > 30720 || (Number(memory) % 1024 !== 0)) { + throw new Error(`If provided cpu is ${ cpu }, then memoryMiB must have a min of 8192 and a max of 30720, in 1024 increments.Provided memoryMiB was ${ Number(memory) }.`); + } + } else { + throw new Error(`If operatingSystemFamily is ${runtimePlatform.operatingSystemFamily!._operatingSystemFamily}, then cpu must be in 1024 (1 vCPU), 2048 (2 vCPU), or 4096 (4 vCPU). Provided value was: ${cpu}`); + } + }; } /** diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 1dc50ac97ae49..8e48e2be59cec 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -6,7 +6,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 cloudmap from '@aws-cdk/aws-servicediscovery'; -import { Duration, Lazy, IResource, Resource, Stack, Aspects, IAspect, IConstruct } from '@aws-cdk/core'; +import { Duration, Lazy, IResource, Resource, Stack, Aspects, IAspect, IConstruct, ArnFormat } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { BottleRocketImage, EcsOptimizedAmi } from './amis'; import { InstanceDrainHook } from './drain-hook/instance-drain-hook'; @@ -105,6 +105,41 @@ export class Cluster extends Resource implements ICluster { return new ImportedCluster(scope, id, attrs); } + /** + * Import an existing cluster to the stack from the cluster ARN. + * This does not provide access to the vpc, hasEc2Capacity, or connections - + * use the `fromClusterAttributes` method to access those properties. + */ + public static fromClusterArn(scope: Construct, id: string, clusterArn: string): ICluster { + const stack = Stack.of(scope); + const arn = stack.splitArn(clusterArn, ArnFormat.SLASH_RESOURCE_NAME); + const clusterName = arn.resourceName; + + if (!clusterName) { + throw new Error(`Missing required Cluster Name from Cluster ARN: ${clusterArn}`); + } + + const errorSuffix = 'is not available for a Cluster imported using fromClusterArn(), please use fromClusterAttributes() instead.'; + + class Import extends Resource implements ICluster { + public readonly clusterArn = clusterArn; + public readonly clusterName = clusterName!; + get hasEc2Capacity(): boolean { + throw new Error(`hasEc2Capacity ${errorSuffix}`); + } + get connections(): ec2.Connections { + throw new Error(`connections ${errorSuffix}`); + } + get vpc(): ec2.IVpc { + throw new Error(`vpc ${errorSuffix}`); + } + } + + return new Import(scope, id, { + environmentFromArn: clusterArn, + }); + } + /** * Manage the allowed network connections for the cluster with Security Groups. */ diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 6dd3fd0dbbe40..0ee433ff951e7 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -14,6 +14,24 @@ import { LogDriver, LogDriverConfig } from './log-drivers/log-driver'; // eslint-disable-next-line no-duplicate-imports, import/order import { Construct as CoreConstruct } from '@aws-cdk/core'; +/** + * Specify the secret's version id or version stage + */ +export interface SecretVersionInfo { + /** + * version id of the secret + * + * @default - use default version id + */ + readonly versionId?: string; + /** + * version stage of the secret + * + * @default - use default version stage + */ + readonly versionStage?: string; +} + /** * A secret environment variable. */ @@ -47,6 +65,25 @@ export abstract class Secret { }; } + /** + * Creates a environment variable value from a secret stored in AWS Secrets + * Manager. + * + * @param secret the secret stored in AWS Secrets Manager + * @param versionInfo the version information to reference the secret + * @param field the name of the field with the value that you want to set as + * the environment variable value. Only values in JSON format are supported. + * If you do not specify a JSON field, then the full content of the secret is + * used. + */ + public static fromSecretsManagerVersion(secret: secretsmanager.ISecret, versionInfo: SecretVersionInfo, field?: string): Secret { + return { + arn: `${secret.secretArn}:${field ?? ''}:${versionInfo.versionStage ?? ''}:${versionInfo.versionId ?? ''}`, + hasField: !!field, + grantRead: grantee => secret.grantRead(grantee), + }; + } + /** * The ARN of the secret */ @@ -408,6 +445,11 @@ export class ContainerDefinition extends CoreConstruct { */ public readonly referencesSecretJsonField?: boolean; + /** + * The name of the image referenced by this container. + */ + public readonly imageName: string; + /** * The inference accelerators referenced by this container. */ @@ -441,6 +483,8 @@ export class ContainerDefinition extends CoreConstruct { this.containerName = props.containerName ?? this.node.id; this.imageConfig = props.image.bind(this, this); + this.imageName = this.imageConfig.imageName; + if (props.logging) { this.logDriverConfig = props.logging.bind(this, this); } @@ -685,7 +729,7 @@ export class ContainerDefinition extends CoreConstruct { workingDirectory: this.props.workingDirectory, logConfiguration: this.logDriverConfig, environment: this.environment && Object.keys(this.environment).length ? renderKV(this.environment, 'name', 'value') : undefined, - environmentFiles: this.environmentFiles && renderEnvironmentFiles(this.environmentFiles), + environmentFiles: this.environmentFiles && renderEnvironmentFiles(cdk.Stack.of(this).partition, this.environmentFiles), secrets: this.secrets, extraHosts: this.props.extraHosts && renderKV(this.props.extraHosts, 'hostname', 'ipAddress'), healthCheck: this.props.healthCheck && renderHealthCheck(this.props.healthCheck), @@ -757,7 +801,7 @@ function renderKV(env: { [key: string]: string }, keyName: string, valueName: st return ret; } -function renderEnvironmentFiles(environmentFiles: EnvironmentFileConfig[]): any[] { +function renderEnvironmentFiles(partition: string, environmentFiles: EnvironmentFileConfig[]): any[] { const ret = []; for (const environmentFile of environmentFiles) { const s3Location = environmentFile.s3Location; @@ -768,7 +812,7 @@ function renderEnvironmentFiles(environmentFiles: EnvironmentFileConfig[]): any[ ret.push({ type: environmentFile.fileType, - value: `arn:aws:s3:::${s3Location.bucketName}/${s3Location.objectKey}`, + value: `arn:${partition}:s3:::${s3Location.bucketName}/${s3Location.objectKey}`, }); } return ret; diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts index 3b65516ba7dfa..6056682b73666 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts @@ -20,9 +20,9 @@ export interface Ec2TaskDefinitionProps extends CommonTaskDefinitionProps { /** * The Docker networking mode to use for the containers in the task. * - * The valid values are none, bridge, awsvpc, and host. + * The valid values are NONE, BRIDGE, AWS_VPC, and HOST. * - * @default - NetworkMode.Bridge for EC2 tasks, AwsVpc for Fargate tasks. + * @default - NetworkMode.BRIDGE for EC2 tasks, AWS_VPC for Fargate tasks. */ readonly networkMode?: NetworkMode; 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 803ec6a449969..a1e957ed7a21c 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 @@ -9,6 +9,7 @@ import { NetworkMode, TaskDefinition, } from '../base/task-definition'; +import { RuntimePlatform } from '../runtime-platform'; /** * The properties for a task definition. @@ -59,6 +60,15 @@ export interface FargateTaskDefinitionProps extends CommonTaskDefinitionProps { * @default 20 */ readonly ephemeralStorageGiB?: number; + + /** + * The operating system that your task definitions are running on. + * + * A runtimePlatform is supported only for tasks using the Fargate launch type. + * + * @default - Undefined. + */ + readonly runtimePlatform?: RuntimePlatform; } /** diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 09d355dd18b38..498e8a0db5081 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -42,6 +42,7 @@ export * from './log-drivers/log-drivers'; export * from './proxy-configuration/app-mesh-proxy-configuration'; export * from './proxy-configuration/proxy-configuration'; export * from './proxy-configuration/proxy-configurations'; +export * from './runtime-platform'; // AWS::ECS CloudFormation Resources: // diff --git a/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts b/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts index e6da02b29797e..2bca7326e18f3 100644 --- a/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts +++ b/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts @@ -1,5 +1,4 @@ import * as logs from '@aws-cdk/aws-logs'; -import { Stack } from '@aws-cdk/core'; import { ContainerDefinition } from '../container-definition'; import { LogDriver, LogDriverConfig } from './log-driver'; import { removeEmpty } from './utils'; @@ -127,7 +126,7 @@ export class AwsLogDriver extends LogDriver { options: removeEmpty({ 'awslogs-group': this.logGroup.logGroupName, 'awslogs-stream-prefix': this.props.streamPrefix, - 'awslogs-region': Stack.of(containerDefinition).region, + 'awslogs-region': this.logGroup.env.region, 'awslogs-datetime-format': this.props.datetimeFormat, 'awslogs-multiline-pattern': this.props.multilinePattern, 'mode': this.props.mode, diff --git a/packages/@aws-cdk/aws-ecs/lib/runtime-platform.ts b/packages/@aws-cdk/aws-ecs/lib/runtime-platform.ts new file mode 100644 index 0000000000000..84e9b896b01a0 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/runtime-platform.ts @@ -0,0 +1,111 @@ +/** + * The CpuArchitecture for Fargate Runtime Platform. + */ +export class CpuArchitecture { + /** + * ARM64 + */ + public static readonly ARM64 = CpuArchitecture.of('ARM64'); + + /** + * X86_64 + */ + public static readonly X86_64 = CpuArchitecture.of('X86_64'); + + /** + * Other cpu architecture. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-taskdefinition-runtimeplatform.html#cfn-ecs-taskdefinition-runtimeplatform-cpuarchitecture for all available cpu architecture. + * + * @param cpuArchitecture cpu architecture. + * + */ + public static of(cpuArchitecture: string) { return new CpuArchitecture(cpuArchitecture); } + + /** + * + * @param _cpuArchitecture The CPU architecture. + */ + private constructor(public readonly _cpuArchitecture: string) { } +} + +/** + * The operating system for Fargate Runtime Platform. + */ +export class OperatingSystemFamily { + /** + * LINUX + */ + public static readonly LINUX = OperatingSystemFamily.of('LINUX'); + + /** + * WINDOWS_SERVER_2004_CORE + */ + public static readonly WINDOWS_SERVER_2004_CORE = OperatingSystemFamily.of('WINDOWS_SERVER_2004_CORE'); + + /** + * WINDOWS_SERVER_2016_FULL + */ + public static readonly WINDOWS_SERVER_2016_FULL = OperatingSystemFamily.of('WINDOWS_SERVER_2016_FULL'); + + /** + * WINDOWS_SERVER_2019_CORE + */ + public static readonly WINDOWS_SERVER_2019_CORE = OperatingSystemFamily.of('WINDOWS_SERVER_2019_CORE'); + + /** + * WINDOWS_SERVER_2019_FULL + */ + public static readonly WINDOWS_SERVER_2019_FULL = OperatingSystemFamily.of('WINDOWS_SERVER_2019_FULL'); + + /** + * WINDOWS_SERVER_2022_CORE + */ + public static readonly WINDOWS_SERVER_2022_CORE = OperatingSystemFamily.of('WINDOWS_SERVER_2022_CORE'); + + /** + * WINDOWS_SERVER_2022_FULL + */ + public static readonly WINDOWS_SERVER_2022_FULL = OperatingSystemFamily.of('WINDOWS_SERVER_2022_FULL'); + + /** + * WINDOWS_SERVER_20H2_CORE + */ + public static readonly WINDOWS_SERVER_20H2_CORE = OperatingSystemFamily.of('WINDOWS_SERVER_20H2_CORE'); + + /** + * Other operating system family. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-taskdefinition-runtimeplatform.html#cfn-ecs-taskdefinition-runtimeplatform-operatingsystemfamily for all available operating system family. + * + * @param family operating system family. + * + */ + public static of(family: string) { return new OperatingSystemFamily(family); } + + /** + * + * @param _operatingSystemFamily The operating system family. + */ + private constructor(public readonly _operatingSystemFamily: string) { } +} + + +/** + * The interface for Runtime Platform. + */ +export interface RuntimePlatform { + /** + * The CpuArchitecture for Fargate Runtime Platform. + * + * @default - Undefined. + */ + readonly cpuArchitecture?: CpuArchitecture, + + /** + * The operating system for Fargate Runtime Platform. + * + * @default - Undefined. + */ + readonly operatingSystemFamily?: OperatingSystemFamily, +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index 66be00bdf5608..931a7d1b4ae1b 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -79,16 +79,16 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-s3-deployment": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "@types/proxyquire": "^1.3.28", - "jest": "^27.4.5", + "jest": "^27.5.1", "proxyquire": "^2.1.3" }, "dependencies": { 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 7c8bd5f880f77..683a7301b54dd 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,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as ecs from '../lib'; @@ -33,7 +33,7 @@ describe('app mesh proxy configuration', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ProxyConfiguration: { ContainerName: 'envoy', ProxyConfigurationProperties: [ @@ -99,7 +99,7 @@ describe('app mesh proxy configuration', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ProxyConfiguration: { ContainerName: 'envoy', ProxyConfigurationProperties: [ @@ -155,7 +155,7 @@ describe('app mesh proxy configuration', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ProxyConfiguration: { ContainerName: 'envoy', ProxyConfigurationProperties: [ 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 5e570a7d39eeb..de7e4faa24f19 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,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as logs from '@aws-cdk/aws-logs'; import * as cdk from '@aws-cdk/core'; import * as ecs from '../lib'; @@ -29,13 +29,13 @@ describe('aws log driver', () => { }); // THEN - expect(stack).toHaveResource('AWS::Logs::LogGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogGroup', { RetentionInDays: logs.RetentionDays.ONE_MONTH, }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'awslogs', Options: { @@ -47,11 +47,9 @@ describe('aws log driver', () => { 'mode': 'non-blocking', }, }, - }, + }), ], }); - - }); test('create an aws log driver using awsLogs', () => { @@ -67,13 +65,13 @@ describe('aws log driver', () => { }); // THEN - expect(stack).toHaveResource('AWS::Logs::LogGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogGroup', { RetentionInDays: logs.RetentionDays.ONE_MONTH, }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'awslogs', Options: { @@ -84,11 +82,9 @@ describe('aws log driver', () => { 'awslogs-multiline-pattern': 'pattern', }, }, - }, + }), ], }); - - }); test('with a defined log group', () => { @@ -105,13 +101,13 @@ describe('aws log driver', () => { }); // THEN - expect(stack).toHaveResource('AWS::Logs::LogGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogGroup', { RetentionInDays: logs.RetentionDays.TWO_YEARS, }); - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'awslogs', Options: { @@ -120,11 +116,9 @@ describe('aws log driver', () => { 'awslogs-region': { Ref: 'AWS::Region' }, }, }, - }, + }), ], }); - - }); test('without a defined log group: creates one anyway', () => { @@ -137,9 +131,7 @@ describe('aws log driver', () => { }); // THEN - expect(stack).toHaveResource('AWS::Logs::LogGroup', {}); - - + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogGroup', {}); }); test('throws when specifying log retention and log group', () => { @@ -155,4 +147,37 @@ describe('aws log driver', () => { }); + + test('allows cross-region log group', () => { + // GIVEN + const logGroupRegion = 'asghard'; + const logGroup = logs.LogGroup.fromLogGroupArn(stack, 'LogGroup', + `arn:aws:logs:${logGroupRegion}:1234:log-group:my_log_group`); + + // WHEN + td.addContainer('Container', { + image, + logging: new ecs.AwsLogDriver({ + logGroup, + streamPrefix: 'hello', + }), + }); + + // THEN + Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 0); + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + Match.objectLike({ + LogConfiguration: { + LogDriver: 'awslogs', + Options: { + 'awslogs-group': logGroup.logGroupName, + 'awslogs-stream-prefix': 'hello', + 'awslogs-region': logGroupRegion, + }, + }, + }), + ], + }); + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/base-service.test.ts b/packages/@aws-cdk/aws-ecs/test/base-service.test.ts new file mode 100644 index 0000000000000..6e3b563cf4e1e --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/base-service.test.ts @@ -0,0 +1,44 @@ +import * as cdk from '@aws-cdk/core'; +import * as ecs from '../lib'; + +let stack: cdk.Stack; + +beforeEach(() => { + stack = new cdk.Stack(); +}); + +describe('When import an ECS Service', () => { + test('with serviceArnWithCluster', () => { + // GIVEN + const clusterName = 'cluster-name'; + const serviceName = 'my-http-service'; + const region = 'service-region'; + const account = 'service-account'; + const serviceArn = `arn:aws:ecs:${region}:${account}:service/${clusterName}/${serviceName}`; + + // WHEN + const service = ecs.BaseService.fromServiceArnWithCluster(stack, 'Service', serviceArn); + + // THEN + expect(service.serviceArn).toEqual(serviceArn); + expect(service.serviceName).toEqual(serviceName); + expect(service.env.account).toEqual(account); + expect(service.env.region).toEqual(region); + + expect(service.cluster.clusterName).toEqual(clusterName); + expect(service.cluster.env.account).toEqual(account); + expect(service.cluster.env.region).toEqual(region); + }); + + test('throws an expection if no resourceName provided on fromServiceArnWithCluster', () => { + expect(() => { + ecs.BaseService.fromServiceArnWithCluster(stack, 'Service', 'arn:aws:ecs:service-region:service-account:service'); + }).toThrowError(/Missing resource Name from service ARN/); + }); + + test('throws an expection if not using cluster arn format on fromServiceArnWithCluster', () => { + expect(() => { + ecs.BaseService.fromServiceArnWithCluster(stack, 'Service', 'arn:aws:ecs:service-region:service-account:service/my-http-service'); + }).toThrowError(/is not using the ARN cluster format/); + }); +}); diff --git a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts index 15a25f6987559..d167c30989ded 100644 --- a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as kms from '@aws-cdk/aws-kms'; @@ -21,9 +20,9 @@ describe('cluster', () => { instanceType: new ec2.InstanceType('t2.micro'), }); - expect(stack).toHaveResource('AWS::ECS::Cluster'); + Template.fromStack(stack).resourceCountIs('AWS::ECS::Cluster', 1); - expect(stack).toHaveResource('AWS::EC2::VPC', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPC', { CidrBlock: '10.0.0.0/16', EnableDnsHostnames: true, EnableDnsSupport: true, @@ -36,7 +35,7 @@ describe('cluster', () => { ], }); - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -69,7 +68,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MaxSize: '1', MinSize: '1', LaunchConfigurationName: { @@ -92,7 +91,7 @@ describe('cluster', () => { ], }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup', SecurityGroupEgress: [ { @@ -112,7 +111,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -127,7 +126,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -192,9 +191,9 @@ describe('cluster', () => { instanceType: new ec2.InstanceType('t2.micro'), }); - expect(stack).toHaveResource('AWS::ECS::Cluster'); + Template.fromStack(stack).resourceCountIs('AWS::ECS::Cluster', 1); - expect(stack).toHaveResource('AWS::EC2::VPC', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPC', { CidrBlock: '10.0.0.0/16', EnableDnsHostnames: true, EnableDnsSupport: true, @@ -207,7 +206,7 @@ describe('cluster', () => { ], }); - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -240,7 +239,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MaxSize: '1', MinSize: '1', LaunchConfigurationName: { @@ -263,7 +262,7 @@ describe('cluster', () => { ], }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup', SecurityGroupEgress: [ { @@ -283,7 +282,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -298,7 +297,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -381,7 +380,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LifecycleHook', { AutoScalingGroupName: { Ref: 'EcsClusterDefaultAutoScalingGroupASGC1A785DB' }, LifecycleTransition: 'autoscaling:EC2_INSTANCE_TERMINATING', DefaultResult: 'CONTINUE', @@ -390,7 +389,7 @@ describe('cluster', () => { RoleARN: { 'Fn::GetAtt': ['EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleA38EC83B', 'Arn'] }, }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Timeout: 310, Environment: { Variables: { @@ -402,7 +401,7 @@ describe('cluster', () => { Handler: 'index.lambda_handler', }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -522,7 +521,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::SNS::Topic', { + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Topic', { KmsMasterKeyId: { 'Fn::GetAtt': [ 'Key961B73FD', @@ -549,16 +548,16 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::PrivateDnsNamespace', { Name: 'foo.com', Vpc: { Ref: 'MyVpcF9F0CA6F', }, }); - expect(stack).toHaveResource('AWS::ECS::Cluster'); + Template.fromStack(stack).resourceCountIs('AWS::ECS::Cluster', 1); - expect(stack).toHaveResource('AWS::EC2::VPC', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPC', { CidrBlock: '10.0.0.0/16', EnableDnsHostnames: true, EnableDnsSupport: true, @@ -571,7 +570,7 @@ describe('cluster', () => { ], }); - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -604,7 +603,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MaxSize: '1', MinSize: '1', LaunchConfigurationName: { @@ -627,7 +626,7 @@ describe('cluster', () => { ], }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup', SecurityGroupEgress: [ { @@ -647,7 +646,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -662,7 +661,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -727,7 +726,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm3.large', }); @@ -746,7 +745,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MaxSize: '3', }); @@ -767,7 +766,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiwindowsserver2019englishfullrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -826,7 +825,7 @@ describe('cluster', () => { // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinux2gpurecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -1028,7 +1027,7 @@ describe('cluster', () => { // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinux2gpurecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -1059,7 +1058,7 @@ describe('cluster', () => { // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -1112,7 +1111,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { SpotPrice: '0.31', }); @@ -1131,7 +1130,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LifecycleHook', { HeartbeatTimeout: 60, }); @@ -1151,7 +1150,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { UserData: { 'Fn::Base64': { 'Fn::Join': [ @@ -1183,7 +1182,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { UserData: { 'Fn::Base64': { 'Fn::Join': [ @@ -1219,7 +1218,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::PrivateDnsNamespace', { Name: 'foo.com', Vpc: { Ref: 'MyVpcF9F0CA6F', @@ -1246,7 +1245,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::ServiceDiscovery::PublicDnsNamespace', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::PublicDnsNamespace', { Name: 'foo.com', }); @@ -1331,11 +1330,11 @@ describe('cluster', () => { cluster.connections.allowToAnyIpv4(ec2.Port.tcp(443)); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: 'sg-1', }); - expect(stack).toCountResources('AWS::EC2::SecurityGroupEgress', 1); + Template.fromStack(stack).resourceCountIs('AWS::EC2::SecurityGroupEgress', 1); }); @@ -1403,9 +1402,9 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::ECS::Cluster'); + Template.fromStack(stack).resourceCountIs('AWS::ECS::Cluster', 1); - expect(stack).toHaveResource('AWS::EC2::VPC', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPC', { CidrBlock: '10.0.0.0/16', EnableDnsHostnames: true, EnableDnsSupport: true, @@ -1418,7 +1417,7 @@ describe('cluster', () => { ], }); - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -1452,7 +1451,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MaxSize: '1', MinSize: '1', LaunchConfigurationName: { @@ -1475,7 +1474,7 @@ describe('cluster', () => { ], }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup', SecurityGroupEgress: [ { @@ -1507,7 +1506,7 @@ describe('cluster', () => { new ecs.Cluster(stack, 'EcsCluster', { containerInsights: true }); // THEN - expect(stack).toHaveResource('AWS::ECS::Cluster', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Cluster', { ClusterSettings: [ { Name: 'containerInsights', @@ -1527,7 +1526,7 @@ describe('cluster', () => { new ecs.Cluster(stack, 'EcsCluster', { containerInsights: false }); // THEN - expect(stack).toHaveResource('AWS::ECS::Cluster', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Cluster', { ClusterSettings: [ { Name: 'containerInsights', @@ -1593,9 +1592,9 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Cluster'); - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup'); - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).resourceCountIs('AWS::ECS::Cluster', 1); + Template.fromStack(stack).resourceCountIs('AWS::AutoScaling::AutoScalingGroup', 1); + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsservicebottlerocketawsecs1x8664latestimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -1614,7 +1613,7 @@ describe('cluster', () => { }, }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -1685,7 +1684,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsservicebottlerocketawsecs1arm64latestimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -1714,7 +1713,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { UserData: { 'Fn::Base64': { 'Fn::Join': [ @@ -1742,11 +1741,11 @@ describe('cluster', () => { new ecs.Cluster(stack, 'EcsCluster', { capacityProviders: ['FARGATE_SPOT'] }); // THEN - expect(stack).toHaveResource('AWS::ECS::Cluster', { - CapacityProviders: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Cluster', { + CapacityProviders: Match.absent(), }); - expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE_SPOT'], }); @@ -1764,11 +1763,11 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Cluster', { - CapacityProviders: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Cluster', { + CapacityProviders: Match.absent(), }); - expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE', 'FARGATE_SPOT'], }); @@ -1785,11 +1784,11 @@ describe('cluster', () => { cluster.enableFargateCapacityProviders(); // THEN - expect(stack).toHaveResource('AWS::ECS::Cluster', { - CapacityProviders: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Cluster', { + CapacityProviders: Match.absent(), }); - expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE', 'FARGATE_SPOT'], }); @@ -1807,11 +1806,11 @@ describe('cluster', () => { cluster.addCapacityProvider('FARGATE'); // does not add twice // THEN - expect(stack).toHaveResource('AWS::ECS::Cluster', { - CapacityProviders: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Cluster', { + CapacityProviders: Match.absent(), }); - expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE'], }); @@ -1829,11 +1828,11 @@ describe('cluster', () => { cluster.addCapacityProvider('FARGATE'); // does not add twice // THEN - expect(stack).toHaveResource('AWS::ECS::Cluster', { - CapacityProviders: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Cluster', { + CapacityProviders: Match.absent(), }); - expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE'], }); @@ -1871,7 +1870,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::CapacityProvider', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::CapacityProvider', { AutoScalingGroupProvider: { AutoScalingGroupArn: { Ref: 'asgASG4D014670', @@ -1904,12 +1903,12 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::CapacityProvider', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::CapacityProvider', { AutoScalingGroupProvider: { AutoScalingGroupArn: { Ref: 'asgASG4D014670', }, - ManagedScaling: ABSENT, + ManagedScaling: Match.absent(), ManagedTerminationProtection: 'ENABLED', }, }); @@ -1933,7 +1932,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { NewInstancesProtectedFromScaleIn: true, }); @@ -1957,10 +1956,9 @@ describe('cluster', () => { }); // THEN - expect(stack).not.toHaveResource('AWS::AutoScaling::AutoScalingGroup', { - NewInstancesProtectedFromScaleIn: true, + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { + NewInstancesProtectedFromScaleIn: Match.absent(), }); - }); test('can add ASG capacity via Capacity Provider', () => { @@ -1989,7 +1987,7 @@ describe('cluster', () => { cluster.addAsgCapacityProvider(capacityProvider); // THEN - expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::ClusterCapacityProviderAssociations', { Cluster: { Ref: 'EcsCluster97242B84', }, @@ -2036,7 +2034,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Cluster', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Cluster', { Configuration: { ExecuteCommandConfiguration: { KmsKeyId: { @@ -2142,6 +2140,30 @@ describe('cluster', () => { }); + + test('When importing ECS Cluster via Arn', () => { + // GIVEN + const stack = new cdk.Stack(); + const clusterName = 'my-cluster'; + const region = 'service-region'; + const account = 'service-account'; + const cluster = ecs.Cluster.fromClusterArn(stack, 'Cluster', `arn:aws:ecs:${region}:${account}:cluster/${clusterName}`); + + // THEN + expect(cluster.clusterName).toEqual(clusterName); + expect(cluster.env.region).toEqual(region); + expect(cluster.env.account).toEqual(account); + }); + + test('throws error when import ECS Cluster without resource name in arn', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => { + ecs.Cluster.fromClusterArn(stack, 'Cluster', 'arn:aws:ecs:service-region:service-account:cluster'); + }).toThrowError(/Missing required Cluster Name from Cluster ARN: /); + }); }); test('can add ASG capacity via Capacity Provider by not specifying machineImageType', () => { @@ -2186,7 +2208,7 @@ test('can add ASG capacity via Capacity Provider by not specifying machineImageT // THEN Bottlerocket LaunchConfiguration - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsservicebottlerocketawsecs1x8664latestimageidC96584B6F00A464EAD1953AFF4B05118Parameter', @@ -2208,7 +2230,7 @@ test('can add ASG capacity via Capacity Provider by not specifying machineImageT }); // THEN AmazonLinux2 LaunchConfiguration - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -2229,7 +2251,7 @@ test('can add ASG capacity via Capacity Provider by not specifying machineImageT }, }); - expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: [ 'FARGATE', 'FARGATE_SPOT', diff --git a/packages/@aws-cdk/aws-ecs/test/container-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/container-definition.test.ts index ac7a7a7824450..1aed207c1cdd5 100644 --- a/packages/@aws-cdk/aws-ecs/test/container-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/container-definition.test.ts @@ -1,6 +1,5 @@ -import '@aws-cdk/assert-internal/jest'; import * as path from 'path'; -import { InspectionFailure } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ecr_assets from '@aws-cdk/aws-ecr-assets'; import * as s3 from '@aws-cdk/aws-s3'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; @@ -24,7 +23,7 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Essential: true, @@ -91,7 +90,7 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Command: [ @@ -132,7 +131,11 @@ describe('container definition', () => { 'Fn::Join': [ '', [ - 'arn:aws:s3:::', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::', { Ref: 'AssetParameters872561bf078edd1685d50c9ff821cdd60d2b2ddfb0013c4087e79bf2bb50724dS3Bucket7B2069B7', }, @@ -491,16 +494,9 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', (props: any, inspection: InspectionFailure) => { - if (props.NetworkMode === undefined) { - return true; - } - - inspection.failureReason = 'CF template should not have NetworkMode defined for a task definition that relies on NAT network mode.'; - return false; + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + NetworkMode: Match.absent(), }); - - }); }); }); @@ -700,9 +696,9 @@ describe('container definition', () => { container.addEnvironment('SECOND_ENVIRONEMENT_VARIABLE', 'second test value'); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Environment: [{ Name: 'TEST_ENVIRONMENT_VARIABLE', Value: 'test environment variable value', @@ -711,11 +707,9 @@ describe('container definition', () => { Name: 'SECOND_ENVIRONEMENT_VARIABLE', Value: 'second test value', }], - }, + }), ], }); - - }); test('can add environment variables to container definition with no environment', () => { @@ -731,18 +725,16 @@ describe('container definition', () => { container.addEnvironment('SECOND_ENVIRONEMENT_VARIABLE', 'second test value'); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Environment: [{ Name: 'SECOND_ENVIRONEMENT_VARIABLE', Value: 'second test value', }], - }, + }), ], }); - - }); test('can add port mappings to the container definition by props', () => { @@ -758,11 +750,11 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { - PortMappings: [{ ContainerPort: 80 }], - }, + Match.objectLike({ + PortMappings: [Match.objectLike({ ContainerPort: 80 })], + }), ], }); }); @@ -782,14 +774,14 @@ describe('container definition', () => { containerDefinition.addPortMappings({ containerPort: 443 }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ PortMappings: [ - { ContainerPort: 80 }, - { ContainerPort: 443 }, + Match.objectLike({ ContainerPort: 80 }), + Match.objectLike({ ContainerPort: 443 }), ], - }, + }), ], }); }); @@ -810,9 +802,9 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ SystemControls: [ { Namespace: 'SomeNamespace1', @@ -823,7 +815,7 @@ describe('container definition', () => { Value: 'SomeValue2', }, ], - }, + }), ], }); }); @@ -843,16 +835,20 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ EnvironmentFiles: [{ Type: 's3', Value: { 'Fn::Join': [ '', [ - 'arn:aws:s3:::', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::', { Ref: 'AssetParameters872561bf078edd1685d50c9ff821cdd60d2b2ddfb0013c4087e79bf2bb50724dS3Bucket7B2069B7', }, @@ -887,11 +883,10 @@ describe('container definition', () => { ], }, }], - }, + }), ], }); - }); test('can add s3 bucket environment file to the container definition', () => { // GIVEN @@ -909,16 +904,20 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ EnvironmentFiles: [{ Type: 's3', Value: { 'Fn::Join': [ '', [ - 'arn:aws:s3:::', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::', { Ref: 'Bucket83908E77', }, @@ -927,13 +926,12 @@ describe('container definition', () => { ], }, }], - }, + }), ], }); - - }); }); + describe('with Fargate task definitions', () => { test('can add asset environment file to the container definition', () => { // GIVEN @@ -948,16 +946,20 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ EnvironmentFiles: [{ Type: 's3', Value: { 'Fn::Join': [ '', [ - 'arn:aws:s3:::', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::', { Ref: 'AssetParameters872561bf078edd1685d50c9ff821cdd60d2b2ddfb0013c4087e79bf2bb50724dS3Bucket7B2069B7', }, @@ -992,12 +994,11 @@ describe('container definition', () => { ], }, }], - }, + }), ], }); - - }); + test('can add s3 bucket environment file to the container definition', () => { // GIVEN const stack = new cdk.Stack(); @@ -1014,16 +1015,20 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ EnvironmentFiles: [{ Type: 's3', Value: { 'Fn::Join': [ '', [ - 'arn:aws:s3:::', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::', { Ref: 'Bucket83908E77', }, @@ -1032,11 +1037,9 @@ describe('container definition', () => { ], }, }], - }, + }), ], }); - - }); }); }); @@ -1055,9 +1058,9 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Image: 'test', ResourceRequirements: [ { @@ -1065,11 +1068,9 @@ describe('container definition', () => { Value: '4', }, ], - }, + }), ], }); - - }); }); @@ -1097,14 +1098,14 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'Ec2TaskDef', InferenceAccelerators: [{ DeviceName: 'device1', DeviceType: 'eia2.medium', }], ContainerDefinitions: [ - { + Match.objectLike({ Image: 'test', ResourceRequirements: [ { @@ -1112,12 +1113,11 @@ describe('container definition', () => { Value: 'device1', }, ], - }, + }), ], }); - - }); + test('correctly adds resource requirements to container definition using both props and addInferenceAcceleratorResource method', () => { // GIVEN const stack = new cdk.Stack(); @@ -1146,7 +1146,7 @@ describe('container definition', () => { container.addInferenceAcceleratorResource('device2'); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'Ec2TaskDef', InferenceAccelerators: [{ DeviceName: 'device1', @@ -1156,7 +1156,7 @@ describe('container definition', () => { DeviceType: 'eia2.large', }], ContainerDefinitions: [ - { + Match.objectLike({ Image: 'test', ResourceRequirements: [ { @@ -1168,11 +1168,11 @@ describe('container definition', () => { Value: 'device2', }, ], - }, + }), ], }); - }); + test('throws when the value of inference accelerator resource does not match any inference accelerators defined in the Task Definition', () => { // GIVEN const stack = new cdk.Stack(); @@ -1222,14 +1222,14 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'Ec2TaskDef', InferenceAccelerators: [{ DeviceName: 'device1', DeviceType: 'eia2.medium', }], ContainerDefinitions: [ - { + Match.objectLike({ Image: 'test', ResourceRequirements: [{ Type: 'InferenceAccelerator', @@ -1238,7 +1238,7 @@ describe('container definition', () => { Type: 'GPU', Value: '2', }], - }, + }), ], }); }); @@ -1261,13 +1261,15 @@ describe('container definition', () => { secrets: { SECRET: ecs.Secret.fromSecretsManager(secret), PARAMETER: ecs.Secret.fromSsmParameter(parameter), + SECRET_ID: ecs.Secret.fromSecretsManagerVersion(secret, { versionId: 'version-id' }), + SECRET_STAGE: ecs.Secret.fromSecretsManagerVersion(secret, { versionStage: 'version-stage' }), }, }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Secrets: [ { Name: 'SECRET', @@ -1298,12 +1300,40 @@ describe('container definition', () => { ], }, }, + { + Name: 'SECRET_ID', + ValueFrom: { + 'Fn::Join': [ + '', + [ + { + Ref: 'SecretA720EF05', + }, + ':::version-id', + ], + ], + }, + }, + { + Name: 'SECRET_STAGE', + ValueFrom: { + 'Fn::Join': [ + '', + [ + { + Ref: 'SecretA720EF05', + }, + '::version-stage:', + ], + ], + }, + }, ], - }, + }), ], }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -1349,8 +1379,6 @@ describe('container definition', () => { Version: '2012-10-17', }, }); - - }); test('use a specific secret JSON key as environment variable', () => { @@ -1370,9 +1398,9 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Secrets: [ { Name: 'SECRET_KEY', @@ -1389,11 +1417,9 @@ describe('container definition', () => { }, }, ], - }, + }), ], }); - - }); test('use a specific secret JSON field as environment variable for a Fargate task', () => { @@ -1409,13 +1435,15 @@ describe('container definition', () => { memoryLimitMiB: 1024, secrets: { SECRET_KEY: ecs.Secret.fromSecretsManager(secret, 'specificKey'), + SECRET_KEY_ID: ecs.Secret.fromSecretsManagerVersion(secret, { versionId: 'version-id' }, 'specificKey'), + SECRET_KEY_STAGE: ecs.Secret.fromSecretsManagerVersion(secret, { versionStage: 'version-stage' }, 'specificKey'), }, }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Secrets: [ { Name: 'SECRET_KEY', @@ -1431,12 +1459,38 @@ describe('container definition', () => { ], }, }, + { + Name: 'SECRET_KEY_ID', + ValueFrom: { + 'Fn::Join': [ + '', + [ + { + Ref: 'SecretA720EF05', + }, + ':specificKey::version-id', + ], + ], + }, + }, + { + Name: 'SECRET_KEY_STAGE', + ValueFrom: { + 'Fn::Join': [ + '', + [ + { + Ref: 'SecretA720EF05', + }, + ':specificKey:version-stage:', + ], + ], + }, + }, ], - }, + }), ], }); - - }); test('can add AWS logging to container definition', () => { @@ -1452,9 +1506,9 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'awslogs', Options: { @@ -1463,11 +1517,11 @@ describe('container definition', () => { 'awslogs-region': { Ref: 'AWS::Region' }, }, }, - }, + }), ], }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -1479,8 +1533,6 @@ describe('container definition', () => { Version: '2012-10-17', }, }); - - }); test('can set Health Check with defaults', () => { @@ -1499,20 +1551,18 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ HealthCheck: { Command: ['CMD-SHELL', hcCommand], Interval: 30, Retries: 3, Timeout: 5, }, - }, + }), ], }); - - }); test('throws when setting Health Check with no commands', () => { @@ -1531,7 +1581,7 @@ describe('container definition', () => { // THEN expect(() => { - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { HealthCheck: { @@ -1567,9 +1617,9 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ HealthCheck: { Command: ['CMD-SHELL', hcCommand], Interval: 20, @@ -1577,11 +1627,9 @@ describe('container definition', () => { Timeout: 5, StartPeriod: 10, }, - }, + }), ], }); - - }); test('can specify Health Check values in array form starting with CMD-SHELL', () => { @@ -1603,9 +1651,9 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ HealthCheck: { Command: ['CMD-SHELL', hcCommand], Interval: 20, @@ -1613,11 +1661,9 @@ describe('container definition', () => { Timeout: 5, StartPeriod: 10, }, - }, + }), ], }); - - }); test('can specify Health Check values in array form starting with CMD', () => { @@ -1639,9 +1685,9 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ HealthCheck: { Command: ['CMD', hcCommand], Interval: 20, @@ -1649,11 +1695,9 @@ describe('container definition', () => { Timeout: 5, StartPeriod: 10, }, - }, + }), ], }); - - }); test('can specify private registry credentials', () => { @@ -1673,18 +1717,18 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Image: 'user-x/my-app', RepositoryCredentials: { CredentialsParameter: mySecretArn, }, - }, + }), ], }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -1698,8 +1742,6 @@ describe('container definition', () => { ], }, }); - - }); describe('_linkContainer works properly', () => { @@ -1756,18 +1798,16 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Image: 'test', LinuxParameters: { Capabilities: {}, }, - }, + }), ], }); - - }); test('before calling addContainer', () => { @@ -1791,9 +1831,9 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Image: 'test', LinuxParameters: { Capabilities: { @@ -1803,11 +1843,9 @@ describe('container definition', () => { InitProcessEnabled: true, SharedMemorySize: 1024, }, - }, + }), ], }); - - }); test('after calling addContainer', () => { @@ -1833,9 +1871,9 @@ describe('container definition', () => { linuxParameters.dropCapabilities(ecs.Capability.SETUID); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Image: 'test', LinuxParameters: { Capabilities: { @@ -1845,11 +1883,9 @@ describe('container definition', () => { InitProcessEnabled: true, SharedMemorySize: 1024, }, - }, + }), ], }); - - }); test('with one or more host devices', () => { @@ -1874,9 +1910,9 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Image: 'test', LinuxParameters: { Devices: [ @@ -1887,11 +1923,9 @@ describe('container definition', () => { InitProcessEnabled: true, SharedMemorySize: 1024, }, - }, + }), ], }); - - }); test('with the tmpfs mount for a container', () => { @@ -1917,9 +1951,9 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Image: 'test', LinuxParameters: { Tmpfs: [ @@ -1931,11 +1965,9 @@ describe('container definition', () => { InitProcessEnabled: true, SharedMemorySize: 1024, }, - }, + }), ], }); - - }); }); @@ -1954,7 +1986,7 @@ describe('container definition', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Essential: true, @@ -1976,7 +2008,7 @@ describe('container definition', () => { }, ], }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2033,4 +2065,20 @@ describe('container definition', () => { }); }); + + testFutureBehavior('exposes image name', { '@aws-cdk/core:newStyleStackSynthesis': true }, cdk.App, (app) => { + // GIVEN + const stack = new cdk.Stack(app, 'MyStack'); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef'); + + // WHEN + const container = taskDefinition.addContainer('cont', { + image: ecs.ContainerImage.fromAsset(path.join(__dirname, 'demo-image')), + }); + + // THEN + expect(stack.resolve(container.imageName)).toEqual({ + 'Fn::Sub': '${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:baa2d6eb2a17c75424df631c8c70ff39f2d5f3bee8b9e1a109ee24ca17300540', + }); + }); }); 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 aebf98820d885..4ad3830fa7a9a 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,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import { App, Stack } from '@aws-cdk/core'; @@ -50,7 +50,7 @@ describe('cross stack', () => { }); // THEN: it shouldn't throw due to cyclic dependencies - expect(stack2).toHaveResource('AWS::ECS::Service'); + Template.fromStack(stack2).resourceCountIs('AWS::ECS::Service', 1); expectIngress(stack2); @@ -67,7 +67,7 @@ describe('cross stack', () => { }); // THEN: it shouldn't throw due to cyclic dependencies - expect(stack2).toHaveResource('AWS::ECS::Service'); + Template.fromStack(stack2).resourceCountIs('AWS::ECS::Service', 1); expectIngress(stack2); @@ -84,7 +84,7 @@ describe('cross stack', () => { }); // THEN: it shouldn't throw due to cyclic dependencies - expect(stack2).toHaveResource('AWS::ECS::Service'); + Template.fromStack(stack2).resourceCountIs('AWS::ECS::Service', 1); expectIngress(stack2); @@ -92,7 +92,7 @@ describe('cross stack', () => { }); function expectIngress(stack: Stack) { - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { FromPort: 32768, ToPort: 65535, GroupId: { 'Fn::ImportValue': 'Stack1:ExportsOutputFnGetAttDefaultAutoScalingGroupInstanceSecurityGroupFBA881D0GroupId2F7C804A' }, 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 7e1f5532fc207..61dc1dc3c1c8a 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,5 +1,4 @@ -import { SynthUtils } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elb from '@aws-cdk/aws-elasticloadbalancing'; @@ -36,7 +35,7 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { TaskDefinition: { Ref: 'Ec2TaskDef0226F28C', }, @@ -77,7 +76,7 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { TaskDefinition: { Ref: 'Ec2TaskDef0226F28C', }, @@ -94,7 +93,7 @@ describe('ec2 service', () => { EnableExecuteCommand: true, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -168,7 +167,7 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -231,7 +230,7 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -260,7 +259,11 @@ describe('ec2 service', () => { 'Fn::Join': [ '', [ - 'arn:aws:logs:', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':logs:', { Ref: 'AWS::Region', }, @@ -289,7 +292,11 @@ describe('ec2 service', () => { 'Fn::Join': [ '', [ - 'arn:aws:s3:::', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::', { Ref: 'ExecBucket29559356', }, @@ -351,7 +358,7 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -393,7 +400,11 @@ describe('ec2 service', () => { 'Fn::Join': [ '', [ - 'arn:aws:logs:', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':logs:', { Ref: 'AWS::Region', }, @@ -422,7 +433,11 @@ describe('ec2 service', () => { 'Fn::Join': [ '', [ - 'arn:aws:s3:::', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::', { Ref: 'EcsExecBucket4F468651', }, @@ -442,7 +457,7 @@ describe('ec2 service', () => { ], }); - expect(stack).toHaveResource('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -492,7 +507,11 @@ describe('ec2 service', () => { 'Fn::Join': [ '', [ - 'arn:aws:iam::', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', { Ref: 'AWS::AccountId', }, @@ -557,7 +576,7 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -599,7 +618,11 @@ describe('ec2 service', () => { 'Fn::Join': [ '', [ - 'arn:aws:logs:', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':logs:', { Ref: 'AWS::Region', }, @@ -628,7 +651,11 @@ describe('ec2 service', () => { 'Fn::Join': [ '', [ - 'arn:aws:s3:::', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::', { Ref: 'EcsExecBucket4F468651', }, @@ -644,7 +671,11 @@ describe('ec2 service', () => { 'Fn::Join': [ '', [ - 'arn:aws:s3:::', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::', { Ref: 'EcsExecBucket4F468651', }, @@ -663,7 +694,7 @@ describe('ec2 service', () => { ], }); - expect(stack).toHaveResource('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -713,7 +744,11 @@ describe('ec2 service', () => { 'Fn::Join': [ '', [ - 'arn:aws:iam::', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', { Ref: 'AWS::AccountId', }, @@ -738,7 +773,11 @@ describe('ec2 service', () => { 'Fn::Join': [ '', [ - 'arn:aws:logs:', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':logs:', { Ref: 'AWS::Region', }, @@ -807,7 +846,7 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -835,7 +874,7 @@ describe('ec2 service', () => { }, }); - expect(stack).toHaveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::PrivateDnsNamespace', { Name: 'scorekeep.com', Vpc: { Ref: 'MyVpcF9F0CA6F', @@ -898,7 +937,7 @@ describe('ec2 service', () => { service.addPlacementStrategies(PlacementStrategy.spreadAcross(ecs.BuiltInAttributes.AVAILABILITY_ZONE)); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { TaskDefinition: { Ref: 'Ec2TaskDef0226F28C', }, @@ -1004,7 +1043,7 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { CapacityProviderStrategy: [ { CapacityProvider: { @@ -1055,7 +1094,7 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { TaskDefinition: { Ref: 'Ec2TaskDef0226F28C', }, @@ -1095,7 +1134,7 @@ describe('ec2 service', () => { ServiceName: 'bonjour', }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Example', GroupName: 'Bingo', SecurityGroupEgress: [ @@ -1110,7 +1149,7 @@ describe('ec2 service', () => { }, }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Example', GroupName: 'Rolly', SecurityGroupEgress: [ @@ -1223,7 +1262,7 @@ describe('ec2 service', () => { // THEN expect(service.node.metadataEntry[0].data).toEqual('taskDefinition and launchType are blanked out when using external deployment controller.'); - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { Cluster: { Ref: 'EcsCluster97242B84', }, @@ -1329,7 +1368,7 @@ describe('ec2 service', () => { // THEN expect(() => { - SynthUtils.synthesize(stack); + Template.fromStack(stack); }).toThrow(/one essential container/); @@ -1355,15 +1394,13 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Name: 'main', - }, + }), ], }); - - }); test('sets daemon scheduling strategy', () => { @@ -1386,7 +1423,7 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { SchedulingStrategy: 'DAEMON', DeploymentConfiguration: { MaximumPercent: 100, @@ -1572,7 +1609,7 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { NetworkConfiguration: { AwsvpcConfiguration: { AssignPublicIp: 'DISABLED', @@ -1647,7 +1684,7 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { PlacementConstraints: [{ Type: 'distinctInstance', }], @@ -1677,7 +1714,7 @@ describe('ec2 service', () => { service.addPlacementConstraints(PlacementConstraint.memberOf('attribute:ecs.instance-type =~ t2.*')); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { PlacementConstraints: [{ Expression: 'attribute:ecs.instance-type =~ t2.*', Type: 'memberOf', @@ -1709,7 +1746,7 @@ describe('ec2 service', () => { service.addPlacementStrategies(PlacementStrategy.spreadAcrossInstances()); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { PlacementStrategies: [{ Field: 'instanceId', Type: 'spread', @@ -1740,7 +1777,7 @@ describe('ec2 service', () => { service.addPlacementStrategies(PlacementStrategy.spreadAcross(ecs.BuiltInAttributes.AVAILABILITY_ZONE)); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { PlacementStrategies: [{ Field: 'attribute:ecs.availability-zone', Type: 'spread', @@ -1841,11 +1878,9 @@ describe('ec2 service', () => { }); // THEN - expect(stack).not.toHaveResource('AWS::ECS::Service', { - PlacementConstraints: undefined, + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + PlacementConstraints: Match.absent(), }); - - }); testDeprecated('with both propagateTags and propagateTaskTagsFrom defined', () => { @@ -1892,11 +1927,9 @@ describe('ec2 service', () => { }); // THEN - expect(stack).not.toHaveResource('AWS::ECS::Service', { - PlacementStrategies: undefined, + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + PlacementStrategies: Match.absent(), }); - - }); test('with random placement strategy', () => { @@ -1920,7 +1953,7 @@ describe('ec2 service', () => { service.addPlacementStrategies(PlacementStrategy.randomly()); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { PlacementStrategies: [{ Type: 'random', }], @@ -1977,7 +2010,7 @@ describe('ec2 service', () => { service.addPlacementStrategies(PlacementStrategy.packedByCpu()); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { PlacementStrategies: [{ Field: 'cpu', Type: 'binpack', @@ -2008,7 +2041,7 @@ describe('ec2 service', () => { service.addPlacementStrategies(PlacementStrategy.packedByMemory()); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { PlacementStrategies: [{ Field: 'memory', Type: 'binpack', @@ -2039,7 +2072,7 @@ describe('ec2 service', () => { service.addPlacementStrategies(PlacementStrategy.packedBy(ecs.BinPackResource.MEMORY)); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { PlacementStrategies: [{ Field: 'memory', Type: 'binpack', @@ -2272,13 +2305,13 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { Description: 'Load balancer to target', FromPort: 32768, ToPort: 65535, }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { Description: 'Load balancer to target', FromPort: 32768, ToPort: 65535, @@ -2320,13 +2353,13 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { Description: 'Load balancer to target', FromPort: 80, ToPort: 80, }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { Description: 'Load balancer to target', FromPort: 80, ToPort: 80, @@ -2367,13 +2400,13 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { Description: 'Load balancer to target', FromPort: 8001, ToPort: 8001, }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { Description: 'Load balancer to target', FromPort: 8001, ToPort: 8001, @@ -2413,13 +2446,13 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { Description: 'Load balancer to target', FromPort: 8001, ToPort: 8001, }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { Description: 'Load balancer to target', FromPort: 8001, ToPort: 8001, @@ -2513,7 +2546,7 @@ describe('ec2 service', () => { lb.addTarget(service); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'web', @@ -2523,7 +2556,7 @@ describe('ec2 service', () => { ], }); - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { // if any load balancer is configured and healthCheckGracePeriodSeconds is not // set, then it should default to 60 seconds. HealthCheckGracePeriodSeconds: 60, @@ -2558,7 +2591,7 @@ describe('ec2 service', () => { })); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'web', @@ -2663,7 +2696,7 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { ServiceRegistries: [ { ContainerName: 'MainContainer', @@ -2678,7 +2711,7 @@ describe('ec2 service', () => { ], }); - expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -2740,7 +2773,7 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { ServiceRegistries: [ { ContainerName: 'MainContainer', @@ -2755,7 +2788,7 @@ describe('ec2 service', () => { ], }); - expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -2851,7 +2884,7 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { ServiceRegistries: [ { RegistryArn: { @@ -2864,7 +2897,7 @@ describe('ec2 service', () => { ], }); - expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -2927,7 +2960,7 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { ServiceRegistries: [ { ContainerName: 'MainContainer', @@ -2942,7 +2975,7 @@ describe('ec2 service', () => { ], }); - expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -3011,7 +3044,7 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { ServiceRegistries: [ { RegistryArn: { 'Fn::GetAtt': ['ServiceCloudmapService046058A4', 'Arn'] }, @@ -3020,8 +3053,6 @@ describe('ec2 service', () => { }, ], }); - - }); test('By default, the container name is the default', () => { @@ -3056,14 +3087,12 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { - ServiceRegistries: [{ + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + ServiceRegistries: [Match.objectLike({ ContainerName: 'main', - ContainerPort: undefined, - }], + ContainerPort: Match.anyValue(), + })], }); - - }); test('For SRV, by default, container name is default container and port is the default container port', () => { @@ -3100,14 +3129,12 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { - ServiceRegistries: [{ + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + ServiceRegistries: [Match.objectLike({ ContainerName: 'main', ContainerPort: 1234, - }], + })], }); - - }); test('allows SRV service discovery to select the container and port', () => { @@ -3147,14 +3174,12 @@ describe('ec2 service', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { - ServiceRegistries: [{ + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + ServiceRegistries: [Match.objectLike({ ContainerName: 'second', ContainerPort: 4321, - }], + })], }); - - }); test('throws if SRV and container is not part of task definition', () => { diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-task-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-task-definition.test.ts index ac5597bea8737..4978a35b1e0f2 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-task-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-task-definition.test.ts @@ -1,5 +1,5 @@ -import '@aws-cdk/assert-internal/jest'; import * as path from 'path'; +import { Match, Template } from '@aws-cdk/assertions'; import { Protocol } from '@aws-cdk/aws-ec2'; import { Repository } from '@aws-cdk/aws-ecr'; import * as iam from '@aws-cdk/aws-iam'; @@ -18,14 +18,13 @@ describe('ec2 task definition', () => { new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'Ec2TaskDef', NetworkMode: ecs.NetworkMode.BRIDGE, RequiresCompatibilities: ['EC2'], }); // test error if no container defs? - }); test('with all properties set', () => { @@ -56,7 +55,7 @@ describe('ec2 task definition', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ExecutionRoleArn: { 'Fn::GetAtt': [ 'ExecutionRole605A040B', @@ -104,7 +103,7 @@ describe('ec2 task definition', () => { taskDefinition.addPlacementConstraint(ecs.PlacementConstraint.memberOf('attribute:ecs.instance-type =~ t2.*')); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { PlacementConstraints: [ { Expression: 'attribute:ecs.instance-type =~ t2.*', @@ -125,7 +124,7 @@ describe('ec2 task definition', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { NetworkMode: ecs.NetworkMode.AWS_VPC, }); @@ -140,7 +139,7 @@ describe('ec2 task definition', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { IpcMode: ecs.IpcMode.TASK, }); @@ -155,7 +154,7 @@ describe('ec2 task definition', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { PidMode: ecs.PidMode.HOST, }); @@ -194,7 +193,7 @@ describe('ec2 task definition', () => { })); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'Ec2TaskDef', ContainerDefinitions: [{ Essential: true, @@ -222,7 +221,7 @@ describe('ec2 task definition', () => { }], }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -288,7 +287,7 @@ describe('ec2 task definition', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'Ec2TaskDef', ContainerDefinitions: [ { @@ -324,7 +323,11 @@ describe('ec2 task definition', () => { 'Fn::Join': [ '', [ - 'arn:aws:s3:::', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::', { Ref: 'AssetParameters872561bf078edd1685d50c9ff821cdd60d2b2ddfb0013c4087e79bf2bb50724dS3Bucket7B2069B7', }, @@ -461,7 +464,7 @@ describe('ec2 task definition', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECR::Repository', { + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', { ImageScanningConfiguration: { ScanOnPush: false, }, @@ -473,7 +476,7 @@ describe('ec2 task definition', () => { RepositoryName: 'project-a/amazon-ecs-sample', }); - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'Ec2TaskDef', ContainerDefinitions: [{ Essential: true, @@ -546,7 +549,7 @@ describe('ec2 task definition', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [{ Essential: true, Memory: 512, @@ -616,7 +619,7 @@ describe('ec2 task definition', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [{ Essential: true, Memory: 512, @@ -687,13 +690,11 @@ describe('ec2 task definition', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECR::Repository', { + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', { ImageScanningConfiguration: { ScanOnPush: false, }, }); - - }); test('warns when setting containers from ECR repository using fromRegistry method', () => { @@ -746,7 +747,7 @@ describe('ec2 task definition', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'StackEc2TaskDefF03698CF', ContainerDefinitions: [ { @@ -813,9 +814,9 @@ describe('ec2 task definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'Ec2TaskDef', - ContainerDefinitions: [{ + ContainerDefinitions: [Match.objectLike({ MountPoints: [ { ContainerPath: './cache', @@ -823,7 +824,7 @@ describe('ec2 task definition', () => { SourceVolume: 'scratch', }, ], - }], + })], Volumes: [{ Host: { SourcePath: '/tmp/cache', @@ -831,9 +832,8 @@ describe('ec2 task definition', () => { Name: 'scratch', }], }); - - }); + test('correctly sets container dependenices', () => { // GIVEN const stack = new cdk.Stack(); @@ -864,15 +864,15 @@ describe('ec2 task definition', () => { ); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'Ec2TaskDef', - ContainerDefinitions: [{ + ContainerDefinitions: [Match.objectLike({ Name: 'dependency1', - }, - { + }), + Match.objectLike({ Name: 'dependency2', - }, - { + }), + Match.objectLike({ Name: 'web', DependsOn: [{ Condition: 'HEALTHY', @@ -882,7 +882,7 @@ describe('ec2 task definition', () => { Condition: 'SUCCESS', ContainerName: 'dependency2', }], - }], + })], }); @@ -913,25 +913,23 @@ describe('ec2 task definition', () => { container.addLink(linkedContainer2); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Links: [ 'linked1:linked', 'linked2', ], Name: 'web', - }, - { + }), + Match.objectLike({ Name: 'linked1', - }, - { + }), + Match.objectLike({ Name: 'linked2', - }, + }), ], }); - - }); test('correctly set policy statement to the task IAM role', () => { @@ -946,7 +944,7 @@ describe('ec2 task definition', () => { })); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -958,9 +956,8 @@ describe('ec2 task definition', () => { ], }, }); - - }); + test('correctly sets volumes from', () => { const stack = new cdk.Stack(); @@ -977,18 +974,16 @@ describe('ec2 task definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { - ContainerDefinitions: [{ + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [Match.objectLike({ VolumesFrom: [ { SourceContainer: 'SourceContainer', ReadOnly: true, }, ], - }], + })], }); - - }); test('correctly set policy statement to the task execution IAM role', () => { @@ -1003,7 +998,7 @@ describe('ec2 task definition', () => { })); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -1047,9 +1042,9 @@ describe('ec2 task definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'Ec2TaskDef', - ContainerDefinitions: [{ + ContainerDefinitions: [Match.objectLike({ MountPoints: [ { ContainerPath: './cache', @@ -1057,7 +1052,7 @@ describe('ec2 task definition', () => { SourceVolume: 'scratch', }, ], - }], + })], Volumes: [{ Host: { SourcePath: '/tmp/cache', @@ -1065,8 +1060,6 @@ describe('ec2 task definition', () => { Name: 'scratch', }], }); - - }); test('correctly sets placement constraints', () => { @@ -1084,7 +1077,7 @@ describe('ec2 task definition', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { PlacementConstraints: [ { Expression: 'attribute:ecs.instance-type =~ t2.*', @@ -1111,7 +1104,7 @@ describe('ec2 task definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { TaskRoleArn: stack.resolve(taskDefinition.taskRole.roleArn), }); @@ -1124,7 +1117,7 @@ describe('ec2 task definition', () => { const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { TaskRoleArn: stack.resolve(taskDefinition.taskRole.roleArn), }); @@ -1155,7 +1148,7 @@ describe('ec2 task definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'Ec2TaskDef', Volumes: [{ Name: 'scratch', @@ -1192,7 +1185,7 @@ describe('ec2 task definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'Ec2TaskDef', Volumes: [{ Name: 'scratch', @@ -1226,7 +1219,7 @@ describe('ec2 task definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'Ec2TaskDef', InferenceAccelerators: [{ DeviceName: 'device1', @@ -1259,7 +1252,7 @@ describe('ec2 task definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'Ec2TaskDef', InferenceAccelerators: [{ DeviceName: 'device1', diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.environment-file.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.environment-file.expected.json index 2f4e3c8498ea6..9219258ba7016 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.environment-file.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.environment-file.expected.json @@ -120,7 +120,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3Bucket2C6C817C" + "Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3Bucket09A62232" }, "S3Key": { "Fn::Join": [ @@ -133,7 +133,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6" + "Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE" } ] } @@ -146,7 +146,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6" + "Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE" } ] } @@ -940,6 +940,17 @@ } } }, + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicACD2D4A4": { + "Type": "AWS::SNS::Topic", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/EcsCluster/DefaultAutoScalingGroup" + } + ] + } + }, "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleA38EC83B": { "Type": "AWS::IAM::Role", "Properties": { @@ -986,17 +997,6 @@ ] } }, - "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicACD2D4A4": { - "Type": "AWS::SNS::Topic", - "Properties": { - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/EcsCluster/DefaultAutoScalingGroup" - } - ] - } - }, "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookFFA63029": { "Type": "AWS::AutoScaling::LifecycleHook", "Properties": { @@ -1085,7 +1085,11 @@ "Fn::Join": [ "", [ - "arn:aws:s3:::", + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", { "Ref": "AssetParameters872561bf078edd1685d50c9ff821cdd60d2b2ddfb0013c4087e79bf2bb50724dS3Bucket7B2069B7" }, @@ -1126,7 +1130,11 @@ "Fn::Join": [ "", [ - "arn:aws:s3:::", + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", { "Ref": "Bucket83908E77" }, @@ -1346,6 +1354,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -1388,7 +1400,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3Bucket1BE31DB0" + "Ref": "AssetParameterse3d9996b6fafcc7da88312672e15e3cc925b02cffc6f01a615d81f22303e3ae0S3BucketB3DDCC13" }, "S3Key": { "Fn::Join": [ @@ -1401,7 +1413,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3VersionKeyDC38E49C" + "Ref": "AssetParameterse3d9996b6fafcc7da88312672e15e3cc925b02cffc6f01a615d81f22303e3ae0S3VersionKey3418DF70" } ] } @@ -1414,7 +1426,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3VersionKeyDC38E49C" + "Ref": "AssetParameterse3d9996b6fafcc7da88312672e15e3cc925b02cffc6f01a615d81f22303e3ae0S3VersionKey3418DF70" } ] } @@ -1501,17 +1513,17 @@ } }, "Parameters": { - "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3Bucket2C6C817C": { + "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3Bucket09A62232": { "Type": "String", - "Description": "S3 bucket for asset \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\"" + "Description": "S3 bucket for asset \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\"" }, - "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6": { + "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE": { "Type": "String", - "Description": "S3 key for asset version \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\"" + "Description": "S3 key for asset version \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\"" }, - "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709ArtifactHash17D48178": { + "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824ArtifactHash76F8FCF2": { "Type": "String", - "Description": "Artifact hash for asset \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\"" + "Description": "Artifact hash for asset \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\"" }, "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7": { "Type": "String", @@ -1525,17 +1537,17 @@ "Type": "String", "Description": "Artifact hash for asset \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\"" }, - "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3Bucket1BE31DB0": { + "AssetParameterse3d9996b6fafcc7da88312672e15e3cc925b02cffc6f01a615d81f22303e3ae0S3BucketB3DDCC13": { "Type": "String", - "Description": "S3 bucket for asset \"983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4c\"" + "Description": "S3 bucket for asset \"e3d9996b6fafcc7da88312672e15e3cc925b02cffc6f01a615d81f22303e3ae0\"" }, - "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3VersionKeyDC38E49C": { + "AssetParameterse3d9996b6fafcc7da88312672e15e3cc925b02cffc6f01a615d81f22303e3ae0S3VersionKey3418DF70": { "Type": "String", - "Description": "S3 key for asset version \"983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4c\"" + "Description": "S3 key for asset version \"e3d9996b6fafcc7da88312672e15e3cc925b02cffc6f01a615d81f22303e3ae0\"" }, - "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cArtifactHashBA6352EA": { + "AssetParameterse3d9996b6fafcc7da88312672e15e3cc925b02cffc6f01a615d81f22303e3ae0ArtifactHash9D8F179A": { "Type": "String", - "Description": "Artifact hash for asset \"983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4c\"" + "Description": "Artifact hash for asset \"e3d9996b6fafcc7da88312672e15e3cc925b02cffc6f01a615d81f22303e3ae0\"" }, "AssetParameters972240f9dd6e036a93d5f081af9a24315b2053828ac049b3b19b2fa12d7ae64aS3Bucket1F1A8472": { "Type": "String", diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.exec-command.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.exec-command.expected.json index 3f032ba6cc07e..8748132b68636 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.exec-command.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.exec-command.expected.json @@ -1,384 +1,399 @@ { - "Resources": { - "Vpc8378EB38": { - "Type": "AWS::EC2::VPC", - "Properties": { - "CidrBlock": "10.0.0.0/16", - "EnableDnsHostnames": true, - "EnableDnsSupport": true, - "InstanceTenancy": "default", - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc" - } - ] - } - }, - "VpcPublicSubnet1Subnet5C2D37C4": { - "Type": "AWS::EC2::Subnet", - "Properties": { - "CidrBlock": "10.0.0.0/18", - "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": "aws-ecs-integ-exec-command/Vpc/PublicSubnet1" - } - ] - } - }, - "VpcPublicSubnet1RouteTable6C95E38E": { - "Type": "AWS::EC2::RouteTable", - "Properties": { - "VpcId": { - "Ref": "Vpc8378EB38" - }, - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet1" - } - ] - } - }, - "VpcPublicSubnet1RouteTableAssociation97140677": { - "Type": "AWS::EC2::SubnetRouteTableAssociation", - "Properties": { - "RouteTableId": { - "Ref": "VpcPublicSubnet1RouteTable6C95E38E" - }, - "SubnetId": { - "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc" } - } - }, - "VpcPublicSubnet1DefaultRoute3DA9E72A": { - "Type": "AWS::EC2::Route", - "Properties": { - "RouteTableId": { - "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" }, - "DestinationCidrBlock": "0.0.0.0/0", - "GatewayId": { - "Ref": "VpcIGWD7BA715C" + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet1" } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" }, - "DependsOn": [ - "VpcVPCGWBF912B6E" + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet1" + } ] - }, - "VpcPublicSubnet1EIPD7E02669": { - "Type": "AWS::EC2::EIP", - "Properties": { - "Domain": "vpc", - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet1" - } - ] - } - }, - "VpcPublicSubnet1NATGateway4D7517AA": { - "Type": "AWS::EC2::NatGateway", - "Properties": { - "AllocationId": { - "Fn::GetAtt": [ - "VpcPublicSubnet1EIPD7E02669", - "AllocationId" - ] - }, - "SubnetId": { - "Ref": "VpcPublicSubnet1Subnet5C2D37C4" - }, - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet1" - } - ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" } - }, - "VpcPublicSubnet2Subnet691E08A3": { - "Type": "AWS::EC2::Subnet", - "Properties": { - "CidrBlock": "10.0.64.0/18", - "VpcId": { - "Ref": "Vpc8378EB38" - }, - "AvailabilityZone": "test-region-1b", - "MapPublicIpOnLaunch": true, - "Tags": [ - { - "Key": "aws-cdk:subnet-name", - "Value": "Public" - }, - { - "Key": "aws-cdk:subnet-type", - "Value": "Public" - }, - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet2" - } - ] + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" } }, - "VpcPublicSubnet2RouteTable94F7E489": { - "Type": "AWS::EC2::RouteTable", - "Properties": { - "VpcId": { - "Ref": "Vpc8378EB38" - }, - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet2" - } + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" ] - } - }, - "VpcPublicSubnet2RouteTableAssociationDD5762D8": { - "Type": "AWS::EC2::SubnetRouteTableAssociation", - "Properties": { - "RouteTableId": { - "Ref": "VpcPublicSubnet2RouteTable94F7E489" - }, - "SubnetId": { - "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet1" } - } - }, - "VpcPublicSubnet2DefaultRoute97F91067": { - "Type": "AWS::EC2::Route", - "Properties": { - "RouteTableId": { - "Ref": "VpcPublicSubnet2RouteTable94F7E489" + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" }, - "DestinationCidrBlock": "0.0.0.0/0", - "GatewayId": { - "Ref": "VpcIGWD7BA715C" + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet2" } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" }, - "DependsOn": [ - "VpcVPCGWBF912B6E" + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet2" + } ] - }, - "VpcPublicSubnet2EIP3C605A87": { - "Type": "AWS::EC2::EIP", - "Properties": { - "Domain": "vpc", - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet2" - } - ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" } - }, - "VpcPublicSubnet2NATGateway9182C01D": { - "Type": "AWS::EC2::NatGateway", - "Properties": { - "AllocationId": { - "Fn::GetAtt": [ - "VpcPublicSubnet2EIP3C605A87", - "AllocationId" - ] - }, - "SubnetId": { - "Ref": "VpcPublicSubnet2Subnet691E08A3" - }, - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet2" - } - ] + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" } }, - "VpcPrivateSubnet1Subnet536B997A": { - "Type": "AWS::EC2::Subnet", - "Properties": { - "CidrBlock": "10.0.128.0/18", - "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": "aws-ecs-integ-exec-command/Vpc/PrivateSubnet1" - } + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" ] - } - }, - "VpcPrivateSubnet1RouteTableB2C5B500": { - "Type": "AWS::EC2::RouteTable", - "Properties": { - "VpcId": { - "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" }, - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc/PrivateSubnet1" - } - ] - } - }, - "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { - "Type": "AWS::EC2::SubnetRouteTableAssociation", - "Properties": { - "RouteTableId": { - "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" }, - "SubnetId": { - "Ref": "VpcPrivateSubnet1Subnet536B997A" + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PrivateSubnet1" } - } - }, - "VpcPrivateSubnet1DefaultRouteBE02A9ED": { - "Type": "AWS::EC2::Route", - "Properties": { - "RouteTableId": { - "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" - }, - "DestinationCidrBlock": "0.0.0.0/0", - "NatGatewayId": { - "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PrivateSubnet1" } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" } - }, - "VpcPrivateSubnet2Subnet3788AAA1": { - "Type": "AWS::EC2::Subnet", - "Properties": { - "CidrBlock": "10.0.192.0/18", - "VpcId": { - "Ref": "Vpc8378EB38" - }, - "AvailabilityZone": "test-region-1b", - "MapPublicIpOnLaunch": false, - "Tags": [ - { - "Key": "aws-cdk:subnet-name", - "Value": "Private" - }, - { - "Key": "aws-cdk:subnet-type", - "Value": "Private" - }, - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc/PrivateSubnet2" - } - ] + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" } - }, - "VpcPrivateSubnet2RouteTableA678073B": { - "Type": "AWS::EC2::RouteTable", - "Properties": { - "VpcId": { - "Ref": "Vpc8378EB38" + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" }, - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc/PrivateSubnet2" - } - ] - } - }, - "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { - "Type": "AWS::EC2::SubnetRouteTableAssociation", - "Properties": { - "RouteTableId": { - "Ref": "VpcPrivateSubnet2RouteTableA678073B" + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" }, - "SubnetId": { - "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PrivateSubnet2" } - } - }, - "VpcPrivateSubnet2DefaultRoute060D2087": { - "Type": "AWS::EC2::Route", - "Properties": { - "RouteTableId": { - "Ref": "VpcPrivateSubnet2RouteTableA678073B" - }, - "DestinationCidrBlock": "0.0.0.0/0", - "NatGatewayId": { - "Ref": "VpcPublicSubnet2NATGateway9182C01D" + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PrivateSubnet2" } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" } - }, - "VpcIGWD7BA715C": { - "Type": "AWS::EC2::InternetGateway", - "Properties": { - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc" - } - ] - } - }, - "VpcVPCGWBF912B6E": { - "Type": "AWS::EC2::VPCGatewayAttachment", - "Properties": { - "VpcId": { - "Ref": "Vpc8378EB38" - }, - "InternetGatewayId": { - "Ref": "VpcIGWD7BA715C" - } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" } - }, - "LogGroupF5B46931": { - "Type": "AWS::Logs::LogGroup", - "Properties": { - "RetentionInDays": "731", - "KmsKeyId": { - "Fn::GetAtt": [ - "KmsKey46693ADD", - "Arn" - ] + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc" } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" }, - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" - }, - "KmsKey46693ADD": { - "Type": "AWS::KMS::Key", - "Properties": { - "KeyPolicy": { - "Statement": [ - { - "Action": "kms:*", - "Effect": "Allow", - "Principal": { - "AWS": { + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "KmsKey46693ADD": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Encrypt*", + "kms:Decrypt*", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:Describe*" + ], + "Condition": { + "ArnLike": { + "kms:EncryptionContext:aws:logs:arn": { "Fn::Join": [ "", [ @@ -386,788 +401,772 @@ { "Ref": "AWS::Partition" }, - ":iam::", + ":logs:", { - "Ref": "AWS::AccountId" + "Ref": "AWS::Region" }, - ":root" - ] - ] - } - }, - "Resource": "*" - }, - { - "Action": "kms:*", - "Effect": "Allow", - "Principal": { - "AWS": { - "Fn::Join": [ - "", - [ - "arn:aws:iam::", + ":", { "Ref": "AWS::AccountId" }, - ":root" + ":*" ] ] } - }, - "Resource": "*" + } }, - { - "Action": [ - "kms:Encrypt*", - "kms:Decrypt*", - "kms:ReEncrypt*", - "kms:GenerateDataKey*", - "kms:Describe*" - ], - "Condition": { - "ArnLike": { - "kms:EncryptionContext:aws:logs:arn": { - "Fn::Join": [ - "", - [ - "arn:aws:logs:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":*" - ] - ] - } - } - }, - "Effect": "Allow", - "Principal": { - "Service": { - "Fn::Join": [ - "", - [ - "logs.", - { - "Ref": "AWS::Region" - }, - ".amazonaws.com" - ] + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "logs.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" ] - } - }, - "Resource": "*" - } - ], - "Version": "2012-10-17" - } + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "LogGroupF5B46931": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "KmsKeyId": { + "Fn::GetAtt": [ + "KmsKey46693ADD", + "Arn" + ] }, - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + "RetentionInDays": 731 }, - "EcsExecBucket4F468651": { - "Type": "AWS::S3::Bucket", - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain", - "Properties": { - "BucketEncryption": { - "ServerSideEncryptionConfiguration": [ - { - "ServerSideEncryptionByDefault": { - "KMSMasterKeyID": { - "Fn::GetAtt": [ - "KmsKey46693ADD", - "Arn" - ] - }, - "SSEAlgorithm": "aws:kms" - } + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "EcsExecBucket4F468651": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "KMSMasterKeyID": { + "Fn::GetAtt": [ + "KmsKey46693ADD", + "Arn" + ] + }, + "SSEAlgorithm": "aws:kms" } - ] - } + } + ] } }, - "Ec2ClusterEE43E89D": { - "Type": "AWS::ECS::Cluster", - "Properties": { - "Configuration": { - "ExecuteCommandConfiguration": { - "KmsKeyId": { - "Fn::GetAtt": [ - "KmsKey46693ADD", - "Arn" - ] + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "Ec2ClusterEE43E89D": { + "Type": "AWS::ECS::Cluster", + "Properties": { + "Configuration": { + "ExecuteCommandConfiguration": { + "KmsKeyId": { + "Fn::GetAtt": [ + "KmsKey46693ADD", + "Arn" + ] + }, + "LogConfiguration": { + "CloudWatchEncryptionEnabled": true, + "CloudWatchLogGroupName": { + "Ref": "LogGroupF5B46931" }, - "LogConfiguration": { - "CloudWatchEncryptionEnabled": true, - "CloudWatchLogGroupName": { - "Ref": "LogGroupF5B46931" - }, - "S3BucketName": { - "Ref": "EcsExecBucket4F468651" - }, - "S3EncryptionEnabled": true, - "S3KeyPrefix": "exec-output" + "S3BucketName": { + "Ref": "EcsExecBucket4F468651" }, + "S3EncryptionEnabled": true, + "S3KeyPrefix": "exec-output" + }, "Logging": "OVERRIDE" - } } } - }, - "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E": { - "Type": "AWS::EC2::SecurityGroup", - "Properties": { - "GroupDescription": "aws-ecs-integ-exec-command/Ec2Cluster/DefaultAutoScalingGroup/InstanceSecurityGroup", - "SecurityGroupEgress": [ - { - "CidrIp": "0.0.0.0/0", - "Description": "Allow all outbound traffic by default", - "IpProtocol": "-1" - } - ], - "Tags": [ + } + }, + "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ-exec-command/Ec2Cluster/DefaultAutoScalingGroup/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Ec2Cluster/DefaultAutoScalingGroup" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Ec2Cluster/DefaultAutoScalingGroup" + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ec2.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } } ], - "VpcId": { - "Ref": "Vpc8378EB38" + "Version": "2012-10-17" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Ec2Cluster/DefaultAutoScalingGroup" } - } - }, - "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": { - "Fn::Join": [ - "", - [ - "ec2.", - { - "Ref": "AWS::URLSuffix" - } - ] + ] + } + }, + "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecs:DeregisterContainerInstance", + "ecs:RegisterContainerInstance", + "ecs:Submit*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Ec2ClusterEE43E89D", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:Poll", + "ecs:StartTelemetrySession" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "Ec2ClusterEE43E89D", + "Arn" ] } } - } - ], - "Version": "2012-10-17" - }, - "Tags": [ + }, + "Effect": "Allow", + "Resource": "*" + }, { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Ec2Cluster/DefaultAutoScalingGroup" + "Action": [ + "ecs:DiscoverPollEndpoint", + "ecr:GetAuthorizationToken", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "*" } - ] + ], + "Version": "2012-10-17" + }, + "PolicyName": "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD", + "Roles": [ + { + "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898" + } + ] + } + }, + "Ec2ClusterDefaultAutoScalingGroupInstanceProfileDB232471": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898" + } + ] + } + }, + "Ec2ClusterDefaultAutoScalingGroupLaunchConfig7B2FED3A": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": { + "Ref": "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "InstanceType": "t2.micro", + "IamInstanceProfile": { + "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceProfileDB232471" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\necho ECS_CLUSTER=", + { + "Ref": "Ec2ClusterEE43E89D" + }, + " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save\necho ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config" + ] + ] + } } }, - "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": [ - "ecs:DeregisterContainerInstance", - "ecs:RegisterContainerInstance", - "ecs:Submit*" - ], - "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "Ec2ClusterEE43E89D", - "Arn" - ] - } - }, - { - "Action": [ - "ecs:Poll", - "ecs:StartTelemetrySession" - ], - "Condition": { - "ArnEquals": { - "ecs:cluster": { - "Fn::GetAtt": [ - "Ec2ClusterEE43E89D", - "Arn" - ] - } - } - }, - "Effect": "Allow", - "Resource": "*" - }, - { - "Action": [ - "ecs:DiscoverPollEndpoint", - "ecr:GetAuthorizationToken", - "logs:CreateLogStream", - "logs:PutLogEvents" - ], - "Effect": "Allow", - "Resource": "*" - } - ], - "Version": "2012-10-17" + "DependsOn": [ + "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD", + "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898" + ] + }, + "Ec2ClusterDefaultAutoScalingGroupASGC5A6D4C0": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": "1", + "MinSize": "1", + "LaunchConfigurationName": { + "Ref": "Ec2ClusterDefaultAutoScalingGroupLaunchConfig7B2FED3A" + }, + "Tags": [ + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "aws-ecs-integ-exec-command/Ec2Cluster/DefaultAutoScalingGroup" + } + ], + "VPCZoneIdentifier": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" }, - "PolicyName": "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD", - "Roles": [ - { - "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898" - } - ] - } + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] }, - "Ec2ClusterDefaultAutoScalingGroupInstanceProfileDB232471": { - "Type": "AWS::IAM::InstanceProfile", - "Properties": { - "Roles": [ - { - "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898" - } - ] + "UpdatePolicy": { + "AutoScalingReplacingUpdate": { + "WillReplace": true + }, + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true } - }, - "Ec2ClusterDefaultAutoScalingGroupLaunchConfig7B2FED3A": { - "Type": "AWS::AutoScaling::LaunchConfiguration", - "Properties": { - "ImageId": { - "Ref": "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" - }, - "InstanceType": "t2.micro", - "IamInstanceProfile": { - "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceProfileDB232471" - }, - "SecurityGroups": [ + } + }, + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole23116FA3": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ { - "Fn::GetAtt": [ - "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E", - "GroupId" - ] + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } } ], - "UserData": { - "Fn::Base64": { - "Fn::Join": [ - "", - [ - "#!/bin/bash\necho ECS_CLUSTER=", - { - "Ref": "Ec2ClusterEE43E89D" - }, - " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save\necho ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config" - ] + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" ] - } + ] + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Ec2Cluster/DefaultAutoScalingGroup" } - }, - "DependsOn": [ - "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD", - "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898" ] - }, - "Ec2ClusterDefaultAutoScalingGroupASGC5A6D4C0": { - "Type": "AWS::AutoScaling::AutoScalingGroup", - "Properties": { - "MaxSize": "1", - "MinSize": "1", - "LaunchConfigurationName": { - "Ref": "Ec2ClusterDefaultAutoScalingGroupLaunchConfig7B2FED3A" - }, - "Tags": [ - { - "Key": "Name", - "PropagateAtLaunch": true, - "Value": "aws-ecs-integ-exec-command/Ec2Cluster/DefaultAutoScalingGroup" - } - ], - "VPCZoneIdentifier": [ + } + }, + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicy638C9E33": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ { - "Ref": "VpcPrivateSubnet1Subnet536B997A" + "Action": [ + "ec2:DescribeInstances", + "ec2:DescribeInstanceAttribute", + "ec2:DescribeInstanceStatus", + "ec2:DescribeHosts" + ], + "Effect": "Allow", + "Resource": "*" }, { - "Ref": "VpcPrivateSubnet2Subnet3788AAA1" - } - ] - }, - "UpdatePolicy": { - "AutoScalingReplacingUpdate": { - "WillReplace": true - }, - "AutoScalingScheduledAction": { - "IgnoreUnmodifiedGroupSizeProperties": true - } - } - }, - "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole23116FA3": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "lambda.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "ManagedPolicyArns": [ - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + "Action": "autoscaling:CompleteLifecycleAction", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":autoscaling:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":autoScalingGroup:*:autoScalingGroupName/", + { + "Ref": "Ec2ClusterDefaultAutoScalingGroupASGC5A6D4C0" + } + ] ] - ] - } - ], - "Tags": [ + } + }, { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Ec2Cluster/DefaultAutoScalingGroup" - } - ] - } - }, - "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicy638C9E33": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": [ - "ec2:DescribeInstances", - "ec2:DescribeInstanceAttribute", - "ec2:DescribeInstanceStatus", - "ec2:DescribeHosts" - ], - "Effect": "Allow", - "Resource": "*" - }, - { - "Action": "autoscaling:CompleteLifecycleAction", - "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":autoscaling:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":autoScalingGroup:*:autoScalingGroupName/", - { - "Ref": "Ec2ClusterDefaultAutoScalingGroupASGC5A6D4C0" - } + "Action": [ + "ecs:DescribeContainerInstances", + "ecs:DescribeTasks" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "Ec2ClusterEE43E89D", + "Arn" ] - ] - } - }, - { - "Action": [ - "ecs:DescribeContainerInstances", - "ecs:DescribeTasks" - ], - "Condition": { - "ArnEquals": { - "ecs:cluster": { - "Fn::GetAtt": [ - "Ec2ClusterEE43E89D", - "Arn" - ] - } } - }, - "Effect": "Allow", - "Resource": "*" - }, - { - "Action": [ - "ecs:ListContainerInstances", - "ecs:SubmitContainerStateChange", - "ecs:SubmitTaskStateChange" - ], - "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "Ec2ClusterEE43E89D", - "Arn" - ] } }, - { - "Action": [ - "ecs:UpdateContainerInstancesState", - "ecs:ListTasks" - ], - "Condition": { - "ArnEquals": { - "ecs:cluster": { - "Fn::GetAtt": [ - "Ec2ClusterEE43E89D", - "Arn" - ] - } - } - }, - "Effect": "Allow", - "Resource": "*" - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicy638C9E33", - "Roles": [ + "Effect": "Allow", + "Resource": "*" + }, { - "Ref": "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole23116FA3" - } - ] - } - }, - "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionE0DEFB31": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Code": { - "ZipFile": "import boto3, json, os, time\n\necs = boto3.client('ecs')\nautoscaling = boto3.client('autoscaling')\n\n\ndef lambda_handler(event, context):\n print(json.dumps(event))\n cluster = os.environ['CLUSTER']\n snsTopicArn = event['Records'][0]['Sns']['TopicArn']\n lifecycle_event = json.loads(event['Records'][0]['Sns']['Message'])\n instance_id = lifecycle_event.get('EC2InstanceId')\n if not instance_id:\n print('Got event without EC2InstanceId: %s', json.dumps(event))\n return\n\n instance_arn = container_instance_arn(cluster, instance_id)\n print('Instance %s has container instance ARN %s' % (lifecycle_event['EC2InstanceId'], instance_arn))\n\n if not instance_arn:\n return\n\n task_arns = container_instance_task_arns(cluster, instance_arn)\n \n if task_arns:\n print('Instance ARN %s has task ARNs %s' % (instance_arn, ', '.join(task_arns)))\n\n while has_tasks(cluster, instance_arn, task_arns):\n time.sleep(10)\n\n try:\n print('Terminating instance %s' % instance_id)\n autoscaling.complete_lifecycle_action(\n LifecycleActionResult='CONTINUE',\n **pick(lifecycle_event, 'LifecycleHookName', 'LifecycleActionToken', 'AutoScalingGroupName'))\n except Exception as e:\n # Lifecycle action may have already completed.\n print(str(e))\n\n\ndef container_instance_arn(cluster, instance_id):\n \"\"\"Turn an instance ID into a container instance ARN.\"\"\"\n arns = ecs.list_container_instances(cluster=cluster, filter='ec2InstanceId==' + instance_id)['containerInstanceArns']\n if not arns:\n return None\n return arns[0]\n\ndef container_instance_task_arns(cluster, instance_arn):\n \"\"\"Fetch tasks for a container instance ARN.\"\"\"\n arns = ecs.list_tasks(cluster=cluster, containerInstance=instance_arn)['taskArns']\n return arns\n\ndef has_tasks(cluster, instance_arn, task_arns):\n \"\"\"Return True if the instance is running tasks for the given cluster.\"\"\"\n instances = ecs.describe_container_instances(cluster=cluster, containerInstances=[instance_arn])['containerInstances']\n if not instances:\n return False\n instance = instances[0]\n\n if instance['status'] == 'ACTIVE':\n # Start draining, then try again later\n set_container_instance_to_draining(cluster, instance_arn)\n return True\n\n task_count = None\n\n if task_arns:\n # Fetch details for tasks running on the container instance\n tasks = ecs.describe_tasks(cluster=cluster, tasks=task_arns)['tasks']\n if tasks:\n # Consider any non-stopped tasks as running\n task_count = sum(task['lastStatus'] != 'STOPPED' for task in tasks) + instance['pendingTasksCount']\n \n if not task_count:\n # Fallback to instance task counts if detailed task information is unavailable\n task_count = instance['runningTasksCount'] + instance['pendingTasksCount']\n \n print('Instance %s has %s tasks' % (instance_arn, task_count))\n\n return task_count > 0\n\ndef set_container_instance_to_draining(cluster, instance_arn):\n ecs.update_container_instances_state(\n cluster=cluster,\n containerInstances=[instance_arn], status='DRAINING')\n\n\ndef pick(dct, *keys):\n \"\"\"Pick a subset of a dict.\"\"\"\n return {k: v for k, v in dct.items() if k in keys}\n" - }, - "Role": { - "Fn::GetAtt": [ - "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole23116FA3", - "Arn" - ] - }, - "Environment": { - "Variables": { - "CLUSTER": { - "Ref": "Ec2ClusterEE43E89D" + "Action": [ + "ecs:ListContainerInstances", + "ecs:SubmitContainerStateChange", + "ecs:SubmitTaskStateChange" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Ec2ClusterEE43E89D", + "Arn" + ] } - } - }, - "Handler": "index.lambda_handler", - "Runtime": "python3.6", - "Tags": [ + }, { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Ec2Cluster/DefaultAutoScalingGroup" + "Action": [ + "ecs:UpdateContainerInstancesState", + "ecs:ListTasks" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "Ec2ClusterEE43E89D", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" } ], - "Timeout": 310 + "Version": "2012-10-17" }, - "DependsOn": [ - "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicy638C9E33", - "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole23116FA3" + "PolicyName": "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicy638C9E33", + "Roles": [ + { + "Ref": "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole23116FA3" + } ] - }, - "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionAllowInvokeawsecsintegexeccommandEc2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopic05F8C92983E1AD32": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "FunctionName": { - "Fn::GetAtt": [ - "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionE0DEFB31", - "Arn" - ] - }, - "Principal": "sns.amazonaws.com", - "SourceArn": { - "Ref": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicF7263B30" + } + }, + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionE0DEFB31": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "import boto3, json, os, time\n\necs = boto3.client('ecs')\nautoscaling = boto3.client('autoscaling')\n\n\ndef lambda_handler(event, context):\n print(json.dumps(event))\n cluster = os.environ['CLUSTER']\n snsTopicArn = event['Records'][0]['Sns']['TopicArn']\n lifecycle_event = json.loads(event['Records'][0]['Sns']['Message'])\n instance_id = lifecycle_event.get('EC2InstanceId')\n if not instance_id:\n print('Got event without EC2InstanceId: %s', json.dumps(event))\n return\n\n instance_arn = container_instance_arn(cluster, instance_id)\n print('Instance %s has container instance ARN %s' % (lifecycle_event['EC2InstanceId'], instance_arn))\n\n if not instance_arn:\n return\n\n task_arns = container_instance_task_arns(cluster, instance_arn)\n \n if task_arns:\n print('Instance ARN %s has task ARNs %s' % (instance_arn, ', '.join(task_arns)))\n\n while has_tasks(cluster, instance_arn, task_arns):\n time.sleep(10)\n\n try:\n print('Terminating instance %s' % instance_id)\n autoscaling.complete_lifecycle_action(\n LifecycleActionResult='CONTINUE',\n **pick(lifecycle_event, 'LifecycleHookName', 'LifecycleActionToken', 'AutoScalingGroupName'))\n except Exception as e:\n # Lifecycle action may have already completed.\n print(str(e))\n\n\ndef container_instance_arn(cluster, instance_id):\n \"\"\"Turn an instance ID into a container instance ARN.\"\"\"\n arns = ecs.list_container_instances(cluster=cluster, filter='ec2InstanceId==' + instance_id)['containerInstanceArns']\n if not arns:\n return None\n return arns[0]\n\ndef container_instance_task_arns(cluster, instance_arn):\n \"\"\"Fetch tasks for a container instance ARN.\"\"\"\n arns = ecs.list_tasks(cluster=cluster, containerInstance=instance_arn)['taskArns']\n return arns\n\ndef has_tasks(cluster, instance_arn, task_arns):\n \"\"\"Return True if the instance is running tasks for the given cluster.\"\"\"\n instances = ecs.describe_container_instances(cluster=cluster, containerInstances=[instance_arn])['containerInstances']\n if not instances:\n return False\n instance = instances[0]\n\n if instance['status'] == 'ACTIVE':\n # Start draining, then try again later\n set_container_instance_to_draining(cluster, instance_arn)\n return True\n\n task_count = None\n\n if task_arns:\n # Fetch details for tasks running on the container instance\n tasks = ecs.describe_tasks(cluster=cluster, tasks=task_arns)['tasks']\n if tasks:\n # Consider any non-stopped tasks as running\n task_count = sum(task['lastStatus'] != 'STOPPED' for task in tasks) + instance['pendingTasksCount']\n \n if not task_count:\n # Fallback to instance task counts if detailed task information is unavailable\n task_count = instance['runningTasksCount'] + instance['pendingTasksCount']\n \n print('Instance %s has %s tasks' % (instance_arn, task_count))\n\n return task_count > 0\n\ndef set_container_instance_to_draining(cluster, instance_arn):\n ecs.update_container_instances_state(\n cluster=cluster,\n containerInstances=[instance_arn], status='DRAINING')\n\n\ndef pick(dct, *keys):\n \"\"\"Pick a subset of a dict.\"\"\"\n return {k: v for k, v in dct.items() if k in keys}\n" + }, + "Role": { + "Fn::GetAtt": [ + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole23116FA3", + "Arn" + ] + }, + "Environment": { + "Variables": { + "CLUSTER": { + "Ref": "Ec2ClusterEE43E89D" + } } - } - }, - "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionTopic4795E0F6": { - "Type": "AWS::SNS::Subscription", - "Properties": { - "Protocol": "lambda", - "TopicArn": { - "Ref": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicF7263B30" - }, - "Endpoint": { - "Fn::GetAtt": [ - "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionE0DEFB31", - "Arn" - ] + }, + "Handler": "index.lambda_handler", + "Runtime": "python3.6", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Ec2Cluster/DefaultAutoScalingGroup" } - } + ], + "Timeout": 310 }, - "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole71045ED7": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "autoscaling.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Ec2Cluster/DefaultAutoScalingGroup" - } + "DependsOn": [ + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicy638C9E33", + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole23116FA3" + ] + }, + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionAllowInvokeawsecsintegexeccommandEc2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopic05F8C92983E1AD32": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionE0DEFB31", + "Arn" ] + }, + "Principal": "sns.amazonaws.com", + "SourceArn": { + "Ref": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicF7263B30" } - }, - "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicyE499974B": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": "sns:Publish", - "Effect": "Allow", - "Resource": { - "Ref": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicF7263B30" - } - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicyE499974B", - "Roles": [ - { - "Ref": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole71045ED7" - } + } + }, + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionTopic4795E0F6": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "lambda", + "TopicArn": { + "Ref": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicF7263B30" + }, + "Endpoint": { + "Fn::GetAtt": [ + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionE0DEFB31", + "Arn" ] } - }, - "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicF7263B30": { - "Type": "AWS::SNS::Topic", - "Properties": { - "Tags": [ + } + }, + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicF7263B30": { + "Type": "AWS::SNS::Topic", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Ec2Cluster/DefaultAutoScalingGroup" + } + ] + } + }, + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole71045ED7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Ec2Cluster/DefaultAutoScalingGroup" + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "autoscaling.amazonaws.com" + } } - ] - } - }, - "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHook5CB1467E": { - "Type": "AWS::AutoScaling::LifecycleHook", - "Properties": { - "AutoScalingGroupName": { - "Ref": "Ec2ClusterDefaultAutoScalingGroupASGC5A6D4C0" - }, - "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING", - "DefaultResult": "CONTINUE", - "HeartbeatTimeout": 300, - "NotificationTargetARN": { - "Ref": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicF7263B30" - }, - "RoleARN": { - "Fn::GetAtt": [ - "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole71045ED7", - "Arn" - ] - } + ], + "Version": "2012-10-17" }, - "DependsOn": [ - "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicyE499974B", - "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole71045ED7" + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Ec2Cluster/DefaultAutoScalingGroup" + } ] - }, - "TaskDefTaskRole1EDB4A67": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "ecs-tasks.amazonaws.com" - } + } + }, + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicyE499974B": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicF7263B30" } - ], - "Version": "2012-10-17" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicyE499974B", + "Roles": [ + { + "Ref": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole71045ED7" } + ] + } + }, + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHook5CB1467E": { + "Type": "AWS::AutoScaling::LifecycleHook", + "Properties": { + "AutoScalingGroupName": { + "Ref": "Ec2ClusterDefaultAutoScalingGroupASGC5A6D4C0" + }, + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING", + "DefaultResult": "CONTINUE", + "HeartbeatTimeout": 300, + "NotificationTargetARN": { + "Ref": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicF7263B30" + }, + "RoleARN": { + "Fn::GetAtt": [ + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole71045ED7", + "Arn" + ] } }, - "TaskDefTaskRoleDefaultPolicyA592CB18": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": [ - "ssmmessages:CreateControlChannel", - "ssmmessages:CreateDataChannel", - "ssmmessages:OpenControlChannel", - "ssmmessages:OpenDataChannel" - ], - "Effect": "Allow", - "Resource": "*" - }, - { - "Action": [ - "kms:Decrypt", - "kms:GenerateDataKey" - ], - "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "KmsKey46693ADD", - "Arn" - ] - } - }, - { - "Action": "logs:DescribeLogGroups", - "Effect": "Allow", - "Resource": "*" - }, - { - "Action": [ - "logs:CreateLogStream", - "logs:DescribeLogStreams", - "logs:PutLogEvents" - ], - "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - "arn:aws:logs:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":log-group:", - { - "Ref": "LogGroupF5B46931" - }, - ":*" - ] - ] - } - }, - { - "Action": "s3:GetBucketLocation", - "Effect": "Allow", - "Resource": "*" - }, - { - "Action": "s3:PutObject", - "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - "arn:aws:s3:::", - { - "Ref": "EcsExecBucket4F468651" - }, - "/*" - ] + "DependsOn": [ + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicyE499974B", + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole71045ED7" + ] + }, + "TaskDefTaskRole1EDB4A67": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDefTaskRoleDefaultPolicyA592CB18": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ssmmessages:CreateControlChannel", + "ssmmessages:CreateDataChannel", + "ssmmessages:OpenControlChannel", + "ssmmessages:OpenDataChannel" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:GenerateDataKey" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "KmsKey46693ADD", + "Arn" + ] + } + }, + { + "Action": "logs:DescribeLogGroups", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "logs:CreateLogStream", + "logs:DescribeLogStreams", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:", + { + "Ref": "LogGroupF5B46931" + }, + ":*" ] - } - }, - { - "Action": "s3:GetEncryptionConfiguration", - "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - "arn:aws:s3:::", - { - "Ref": "EcsExecBucket4F468651" - } - ] + ] + } + }, + { + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "s3:PutObject", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "EcsExecBucket4F468651" + }, + "/*" ] - } + ] } - ], - "Version": "2012-10-17" - }, - "PolicyName": "TaskDefTaskRoleDefaultPolicyA592CB18", - "Roles": [{ - "Ref": "TaskDefTaskRole1EDB4A67" - }] - } - }, - "TaskDef54694570": { - "Type": "AWS::ECS::TaskDefinition", - "Properties": { - "ContainerDefinitions": [ + }, { - "Essential": true, - "Image": "amazon/amazon-ecs-sample", - "Memory": "256", - "Name": "web" + "Action": "s3:GetEncryptionConfiguration", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "EcsExecBucket4F468651" + } + ] + ] + } } ], - "Family": "awsecsintegexeccommandTaskDef44709274", - "NetworkMode": "bridge", - "RequiresCompatibilities": [ - "EC2" - ], - "TaskRoleArn": { - "Fn::GetAtt": [ - "TaskDefTaskRole1EDB4A67", - "Arn" - ] + "Version": "2012-10-17" + }, + "PolicyName": "TaskDefTaskRoleDefaultPolicyA592CB18", + "Roles": [ + { + "Ref": "TaskDefTaskRole1EDB4A67" } - } - }, - "Ec2Service04A33183": { - "Type": "AWS::ECS::Service", - "Properties": { - "Cluster": { - "Ref": "Ec2ClusterEE43E89D" - }, - "DeploymentConfiguration": { - "MaximumPercent": 200, - "MinimumHealthyPercent": 50 - }, - "LaunchType": "EC2", - "EnableECSManagedTags": false, - "EnableExecuteCommand": true, - "SchedulingStrategy": "REPLICA", - "TaskDefinition": { - "Ref": "TaskDef54694570" + ] + } + }, + "TaskDef54694570": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": "amazon/amazon-ecs-sample", + "Memory": 256, + "Name": "web" } + ], + "Family": "awsecsintegexeccommandTaskDef44709274", + "NetworkMode": "bridge", + "RequiresCompatibilities": [ + "EC2" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] } } }, - "Parameters": { - "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { - "Type": "AWS::SSM::Parameter::Value", - "Default": "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id" + "Ec2Service04A33183": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Ref": "Ec2ClusterEE43E89D" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "EnableECSManagedTags": false, + "EnableExecuteCommand": true, + "LaunchType": "EC2", + "SchedulingStrategy": "REPLICA", + "TaskDefinition": { + "Ref": "TaskDef54694570" + } } } + }, + "Parameters": { + "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id" + } + } } \ No newline at end of file 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 815973a80e602..abc64332a326b 100644 --- a/packages/@aws-cdk/aws-ecs/test/environment-file.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/environment-file.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import * as path from 'path'; import * as cdk from '@aws-cdk/core'; import * as ecs from '../lib'; 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 b4a3edf73ea0d..69626f7473fb0 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 @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; @@ -29,7 +29,7 @@ describe('external service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { TaskDefinition: { Ref: 'ExternalTaskDef6CCBDB87', }, @@ -79,7 +79,7 @@ describe('external service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { TaskDefinition: { Ref: 'ExternalTaskDef6CCBDB87', }, @@ -165,7 +165,7 @@ describe('external service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { TaskDefinition: { Ref: 'ExternalTaskDef6CCBDB87', }, @@ -177,7 +177,7 @@ describe('external service', () => { ServiceName: 'bonjour', }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Example', GroupName: 'Bingo', SecurityGroupEgress: [ @@ -189,7 +189,7 @@ describe('external service', () => { ], }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Example', GroupName: 'Rolly', SecurityGroupEgress: [ 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 20f736b78f4f0..6331fa6ac7903 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 @@ -1,5 +1,5 @@ -import '@aws-cdk/assert-internal/jest'; import * as path from 'path'; +import { Template } from '@aws-cdk/assertions'; import { Protocol } from '@aws-cdk/aws-ec2'; import { Repository } from '@aws-cdk/aws-ecr'; import * as iam from '@aws-cdk/aws-iam'; @@ -16,13 +16,11 @@ describe('external task definition', () => { new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'ExternalTaskDef', NetworkMode: ecs.NetworkMode.BRIDGE, RequiresCompatibilities: ['EXTERNAL'], }); - - }); test('with all properties set', () => { @@ -43,7 +41,7 @@ describe('external task definition', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ExecutionRoleArn: { 'Fn::GetAtt': [ 'ExecutionRole605A040B', @@ -93,7 +91,7 @@ describe('external task definition', () => { })); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'ExternalTaskDef', NetworkMode: ecs.NetworkMode.BRIDGE, RequiresCompatibilities: ['EXTERNAL'], @@ -117,7 +115,7 @@ describe('external task definition', () => { }], }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -180,7 +178,7 @@ describe('external task definition', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'ExternalTaskDef', NetworkMode: ecs.NetworkMode.BRIDGE, RequiresCompatibilities: ['EXTERNAL'], @@ -218,7 +216,11 @@ describe('external task definition', () => { 'Fn::Join': [ '', [ - 'arn:aws:s3:::', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::', { Ref: 'AssetParameters872561bf078edd1685d50c9ff821cdd60d2b2ddfb0013c4087e79bf2bb50724dS3Bucket7B2069B7', }, @@ -353,7 +355,7 @@ describe('external task definition', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECR::Repository', { + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', { ImageScanningConfiguration: { ScanOnPush: false, }, @@ -365,7 +367,7 @@ describe('external task definition', () => { RepositoryName: 'project-a/amazon-ecs-sample', }); - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'ExternalTaskDef', NetworkMode: ecs.NetworkMode.BRIDGE, RequiresCompatibilities: ['EXTERNAL'], @@ -440,7 +442,7 @@ describe('external task definition', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'ExternalTaskDef', NetworkMode: ecs.NetworkMode.BRIDGE, RequiresCompatibilities: ['EXTERNAL'], @@ -513,7 +515,7 @@ describe('external task definition', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'ExternalTaskDef', NetworkMode: ecs.NetworkMode.BRIDGE, RequiresCompatibilities: ['EXTERNAL'], @@ -588,13 +590,11 @@ describe('external task definition', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECR::Repository', { + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', { ImageScanningConfiguration: { ScanOnPush: false, }, }); - - }); test('warns when setting containers from ECR repository using fromRegistry method', () => { 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 a8dbb29c98b10..e28ebda720b52 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,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT, SynthUtils } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; @@ -34,7 +33,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { TaskDefinition: { Ref: 'FargateTaskDefC6FB60B4', }, @@ -70,7 +69,7 @@ describe('fargate service', () => { }, }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/FargateService/SecurityGroup', SecurityGroupEgress: [ { @@ -149,15 +148,15 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Cluster', { - CapacityProviders: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Cluster', { + CapacityProviders: Match.absent(), }); - expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE', 'FARGATE_SPOT'], }); - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { TaskDefinition: { Ref: 'FargateTaskDefC6FB60B4', }, @@ -239,15 +238,15 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Cluster', { - CapacityProviders: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Cluster', { + CapacityProviders: Match.absent(), }); - expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE', 'FARGATE_SPOT'], }); - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { TaskDefinition: { Ref: 'FargateTaskDefC6FB60B4', }, @@ -259,7 +258,7 @@ describe('fargate service', () => { MinimumHealthyPercent: 50, }, // no launch type - LaunchType: ABSENT, + LaunchType: Match.absent(), CapacityProviderStrategy: [ { CapacityProvider: 'FARGATE_SPOT', @@ -326,7 +325,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -354,7 +353,7 @@ describe('fargate service', () => { }, }); - expect(stack).toHaveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::PrivateDnsNamespace', { Name: 'scorekeep.com', Vpc: { Ref: 'MyVpcF9F0CA6F', @@ -401,7 +400,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { ServiceRegistries: [ { ContainerName: 'web', @@ -507,7 +506,7 @@ describe('fargate service', () => { // THEN expect(svc.cloudMapService).toBeDefined(); - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { TaskDefinition: { Ref: 'FargateTaskDefC6FB60B4', }, @@ -634,7 +633,7 @@ describe('fargate service', () => { // THEN expect(service.node.metadataEntry[0].data).toEqual('taskDefinition and launchType are blanked out when using external deployment controller.'); - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { Cluster: { Ref: 'EcsCluster97242B84', }, @@ -687,7 +686,7 @@ describe('fargate service', () => { // THEN expect(() => { - SynthUtils.synthesize(stack); + Template.fromStack(stack); }).toThrow(/one essential container/); @@ -711,15 +710,13 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Name: 'main', - }, + }), ], }); - - }); test('allows specifying assignPublicIP as enabled', () => { @@ -740,7 +737,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { NetworkConfiguration: { AwsvpcConfiguration: { AssignPublicIp: 'ENABLED', @@ -769,7 +766,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentConfiguration: { MinimumHealthyPercent: 0, }, @@ -844,7 +841,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { TaskDefinition: { Ref: 'FargateTaskDefC6FB60B4', }, @@ -886,7 +883,7 @@ describe('fargate service', () => { }, }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Example', GroupName: 'Bingo', SecurityGroupEgress: [ @@ -901,7 +898,7 @@ describe('fargate service', () => { }, }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Example', GroupName: 'Rolly', SecurityGroupEgress: [ @@ -942,7 +939,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { HealthCheckGracePeriodSeconds: 10, }); @@ -978,7 +975,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { MaxCapacity: 10, MinCapacity: 1, ResourceId: { @@ -1001,7 +998,7 @@ describe('fargate service', () => { }, }); - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'ALBRequestCountPerTarget', @@ -1018,7 +1015,7 @@ describe('fargate service', () => { }, }); - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { // if any load balancer is configured and healthCheckGracePeriodSeconds is not // set, then it should default to 60 seconds. HealthCheckGracePeriodSeconds: 60, @@ -1058,7 +1055,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { MaxCapacity: 10, MinCapacity: 1, ResourceId: { @@ -1113,7 +1110,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1125,13 +1122,13 @@ describe('fargate service', () => { ], }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { Description: 'Load balancer to target', FromPort: 8000, ToPort: 8000, }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { Description: 'Load balancer to target', FromPort: 8000, ToPort: 8000, @@ -1347,7 +1344,7 @@ describe('fargate service', () => { ); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1359,7 +1356,7 @@ describe('fargate service', () => { ], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 80, Protocol: 'HTTP', }); @@ -1399,7 +1396,7 @@ describe('fargate service', () => { ); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1411,7 +1408,7 @@ describe('fargate service', () => { ], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 80, Protocol: 'HTTP', }); @@ -1451,7 +1448,7 @@ describe('fargate service', () => { ); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1463,7 +1460,7 @@ describe('fargate service', () => { ], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 443, Protocol: 'HTTPS', }); @@ -1504,7 +1501,7 @@ describe('fargate service', () => { ); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1516,7 +1513,7 @@ describe('fargate service', () => { ], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 83, Protocol: 'HTTP', }); @@ -1556,7 +1553,7 @@ describe('fargate service', () => { ); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1568,7 +1565,7 @@ describe('fargate service', () => { ], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 80, Protocol: 'TCP', }); @@ -1608,7 +1605,7 @@ describe('fargate service', () => { ); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1620,7 +1617,7 @@ describe('fargate service', () => { ], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 81, Protocol: 'TCP', }); @@ -1655,7 +1652,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { ScheduledActions: [ { ScalableTargetAction: { @@ -1698,7 +1695,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'StepScaling', ScalingTargetId: { Ref: 'ServiceTaskCountTarget23E25614', @@ -1741,7 +1738,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'ECSServiceAverageCPUUtilization' }, @@ -1775,7 +1772,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'ECSServiceAverageMemoryUtilization' }, @@ -1810,7 +1807,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { CustomizedMetricSpecification: { @@ -1878,7 +1875,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -1939,7 +1936,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -2001,7 +1998,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -2065,7 +2062,7 @@ describe('fargate service', () => { }, }); - expect(stack).toHaveResourceLike('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { ServiceRegistries: [ { RegistryArn: { 'Fn::GetAtt': ['ServiceCloudmapService046058A4', 'Arn'] }, @@ -2168,7 +2165,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { DeploymentConfiguration: { MaximumPercent: 200, MinimumHealthyPercent: 50, @@ -2233,7 +2230,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { TaskDefinition: { Ref: 'FargateTaskDefC6FB60B4', }, @@ -2270,7 +2267,7 @@ describe('fargate service', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2344,7 +2341,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2407,7 +2404,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2436,7 +2433,11 @@ describe('fargate service', () => { 'Fn::Join': [ '', [ - 'arn:aws:logs:', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':logs:', { Ref: 'AWS::Region', }, @@ -2465,7 +2466,11 @@ describe('fargate service', () => { 'Fn::Join': [ '', [ - 'arn:aws:s3:::', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::', { Ref: 'ExecBucket29559356', }, @@ -2527,7 +2532,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2569,7 +2574,11 @@ describe('fargate service', () => { 'Fn::Join': [ '', [ - 'arn:aws:logs:', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':logs:', { Ref: 'AWS::Region', }, @@ -2598,7 +2607,11 @@ describe('fargate service', () => { 'Fn::Join': [ '', [ - 'arn:aws:s3:::', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::', { Ref: 'EcsExecBucket4F468651', }, @@ -2618,7 +2631,7 @@ describe('fargate service', () => { ], }); - expect(stack).toHaveResource('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -2668,7 +2681,11 @@ describe('fargate service', () => { 'Fn::Join': [ '', [ - 'arn:aws:iam::', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', { Ref: 'AWS::AccountId', }, @@ -2733,7 +2750,7 @@ describe('fargate service', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2775,7 +2792,11 @@ describe('fargate service', () => { 'Fn::Join': [ '', [ - 'arn:aws:logs:', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':logs:', { Ref: 'AWS::Region', }, @@ -2804,7 +2825,11 @@ describe('fargate service', () => { 'Fn::Join': [ '', [ - 'arn:aws:s3:::', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::', { Ref: 'EcsExecBucket4F468651', }, @@ -2820,7 +2845,11 @@ describe('fargate service', () => { 'Fn::Join': [ '', [ - 'arn:aws:s3:::', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::', { Ref: 'EcsExecBucket4F468651', }, @@ -2839,7 +2868,7 @@ describe('fargate service', () => { ], }); - expect(stack).toHaveResource('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -2889,7 +2918,11 @@ describe('fargate service', () => { 'Fn::Join': [ '', [ - 'arn:aws:iam::', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', { Ref: 'AWS::AccountId', }, @@ -2914,7 +2947,11 @@ describe('fargate service', () => { 'Fn::Join': [ '', [ - 'arn:aws:logs:', + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':logs:', { Ref: 'AWS::Region', }, 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 3ff15ac22057e..e0b7afca3389f 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,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as ecs from '../../lib'; @@ -11,7 +11,7 @@ describe('fargate task definition', () => { new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Family: 'FargateTaskDef', NetworkMode: ecs.NetworkMode.AWS_VPC, RequiresCompatibilities: ['FARGATE'], @@ -32,7 +32,7 @@ describe('fargate task definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Cpu: '128', Memory: '1024', }); @@ -58,6 +58,10 @@ describe('fargate task definition', () => { assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), }), ephemeralStorageGiB: 21, + runtimePlatform: { + cpuArchitecture: ecs.CpuArchitecture.X86_64, + operatingSystemFamily: ecs.OperatingSystemFamily.LINUX, + }, }); taskDefinition.addVolume({ @@ -68,7 +72,7 @@ describe('fargate task definition', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { Cpu: '128', ExecutionRoleArn: { 'Fn::GetAtt': [ @@ -85,6 +89,10 @@ describe('fargate task definition', () => { RequiresCompatibilities: [ ecs.LaunchType.FARGATE, ], + RuntimePlatform: { + CpuArchitecture: 'X86_64', + OperatingSystemFamily: 'LINUX', + }, TaskRoleArn: { 'Fn::GetAtt': [ 'TaskRole30FC0FBB', @@ -241,8 +249,116 @@ describe('fargate task definition', () => { taskDefinition.taskRole; }).toThrow('This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); + }); + + + test('runtime testing for windows container', () => { + // GIVEN + const stack = new cdk.Stack(); + new ecs.FargateTaskDefinition(stack, 'FargateTaskDef', { + cpu: 1024, + memoryLimitMiB: 2048, + runtimePlatform: { + operatingSystemFamily: ecs.OperatingSystemFamily.WINDOWS_SERVER_2019_CORE, + cpuArchitecture: ecs.CpuArchitecture.X86_64, + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + Cpu: '1024', + Family: 'FargateTaskDef', + Memory: '2048', + NetworkMode: 'awsvpc', + RequiresCompatibilities: [ + ecs.LaunchType.FARGATE, + ], + RuntimePlatform: { + CpuArchitecture: 'X86_64', + OperatingSystemFamily: 'WINDOWS_SERVER_2019_CORE', + }, + TaskRoleArn: { + 'Fn::GetAtt': [ + 'FargateTaskDefTaskRole0B257552', + 'Arn', + ], + }, + }); + }); + + test('runtime testing for linux container', () => { + // GIVEN + const stack = new cdk.Stack(); + new ecs.FargateTaskDefinition(stack, 'FargateTaskDef', { + cpu: 1024, + memoryLimitMiB: 2048, + runtimePlatform: { + operatingSystemFamily: ecs.OperatingSystemFamily.LINUX, + cpuArchitecture: ecs.CpuArchitecture.ARM64, + }, + }); + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + Cpu: '1024', + Family: 'FargateTaskDef', + Memory: '2048', + NetworkMode: 'awsvpc', + RequiresCompatibilities: [ + ecs.LaunchType.FARGATE, + ], + RuntimePlatform: { + CpuArchitecture: 'ARM64', + OperatingSystemFamily: 'LINUX', + }, + TaskRoleArn: { + 'Fn::GetAtt': [ + 'FargateTaskDefTaskRole0B257552', + 'Arn', + ], + }, + }); + }); + + test('creating a Fargate TaskDefinition with WINDOWS_SERVER_X operatingSystemFamily and incorrect cpu throws an error', () => { + // GIVEN + const stack = new cdk.Stack(); + + // Not in CPU Ranage. + expect(() => { + new ecs.FargateTaskDefinition(stack, 'FargateTaskDefCPU', { + cpu: 128, + memoryLimitMiB: 1024, + runtimePlatform: { + cpuArchitecture: ecs.CpuArchitecture.X86_64, + operatingSystemFamily: ecs.OperatingSystemFamily.WINDOWS_SERVER_2019_CORE, + }, + }); + }).toThrowError(`If operatingSystemFamily is ${ecs.OperatingSystemFamily.WINDOWS_SERVER_2019_CORE._operatingSystemFamily}, then cpu must be in 1024 (1 vCPU), 2048 (2 vCPU), or 4096 (4 vCPU).`); + + // Memory is not in 1 GB increments. + expect(() => { + new ecs.FargateTaskDefinition(stack, 'FargateTaskDefMemory', { + cpu: 1024, + memoryLimitMiB: 1025, + runtimePlatform: { + cpuArchitecture: ecs.CpuArchitecture.X86_64, + operatingSystemFamily: ecs.OperatingSystemFamily.WINDOWS_SERVER_2019_CORE, + }, + }); + }).toThrowError('If provided cpu is 1024, then memoryMiB must have a min of 1024 and a max of 8192, in 1024 increments. Provided memoryMiB was 1025.'); + + // Check runtimePlatform was been defined ,but not undefined cpu and memoryLimitMiB. + expect(() => { + new ecs.FargateTaskDefinition(stack, 'FargateTaskDef', { + runtimePlatform: { + cpuArchitecture: ecs.CpuArchitecture.X86_64, + operatingSystemFamily: ecs.OperatingSystemFamily.WINDOWS_SERVER_2004_CORE, + }, + }); + }).toThrowError('If operatingSystemFamily is WINDOWS_SERVER_2004_CORE, then cpu must be in 1024 (1 vCPU), 2048 (2 vCPU), or 4096 (4 vCPU). Provided value was: 256'); }); + }); -}); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.exec-command.expected.json b/packages/@aws-cdk/aws-ecs/test/fargate/integ.exec-command.expected.json index 083a16ee75820..19e6073340ac7 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.exec-command.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.exec-command.expected.json @@ -1,384 +1,399 @@ { - "Resources": { - "Vpc8378EB38": { - "Type": "AWS::EC2::VPC", - "Properties": { - "CidrBlock": "10.0.0.0/16", - "EnableDnsHostnames": true, - "EnableDnsSupport": true, - "InstanceTenancy": "default", - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc" - } - ] - } - }, - "VpcPublicSubnet1Subnet5C2D37C4": { - "Type": "AWS::EC2::Subnet", - "Properties": { - "CidrBlock": "10.0.0.0/18", - "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": "aws-ecs-integ-exec-command/Vpc/PublicSubnet1" - } - ] - } - }, - "VpcPublicSubnet1RouteTable6C95E38E": { - "Type": "AWS::EC2::RouteTable", - "Properties": { - "VpcId": { - "Ref": "Vpc8378EB38" - }, - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet1" - } - ] - } - }, - "VpcPublicSubnet1RouteTableAssociation97140677": { - "Type": "AWS::EC2::SubnetRouteTableAssociation", - "Properties": { - "RouteTableId": { - "Ref": "VpcPublicSubnet1RouteTable6C95E38E" - }, - "SubnetId": { - "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc" } - } - }, - "VpcPublicSubnet1DefaultRoute3DA9E72A": { - "Type": "AWS::EC2::Route", - "Properties": { - "RouteTableId": { - "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" }, - "DestinationCidrBlock": "0.0.0.0/0", - "GatewayId": { - "Ref": "VpcIGWD7BA715C" + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet1" } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" }, - "DependsOn": [ - "VpcVPCGWBF912B6E" + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet1" + } ] - }, - "VpcPublicSubnet1EIPD7E02669": { - "Type": "AWS::EC2::EIP", - "Properties": { - "Domain": "vpc", - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet1" - } - ] - } - }, - "VpcPublicSubnet1NATGateway4D7517AA": { - "Type": "AWS::EC2::NatGateway", - "Properties": { - "AllocationId": { - "Fn::GetAtt": [ - "VpcPublicSubnet1EIPD7E02669", - "AllocationId" - ] - }, - "SubnetId": { - "Ref": "VpcPublicSubnet1Subnet5C2D37C4" - }, - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet1" - } - ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" } - }, - "VpcPublicSubnet2Subnet691E08A3": { - "Type": "AWS::EC2::Subnet", - "Properties": { - "CidrBlock": "10.0.64.0/18", - "VpcId": { - "Ref": "Vpc8378EB38" - }, - "AvailabilityZone": "test-region-1b", - "MapPublicIpOnLaunch": true, - "Tags": [ - { - "Key": "aws-cdk:subnet-name", - "Value": "Public" - }, - { - "Key": "aws-cdk:subnet-type", - "Value": "Public" - }, - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet2" - } - ] + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" } }, - "VpcPublicSubnet2RouteTable94F7E489": { - "Type": "AWS::EC2::RouteTable", - "Properties": { - "VpcId": { - "Ref": "Vpc8378EB38" - }, - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet2" - } + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" ] - } - }, - "VpcPublicSubnet2RouteTableAssociationDD5762D8": { - "Type": "AWS::EC2::SubnetRouteTableAssociation", - "Properties": { - "RouteTableId": { - "Ref": "VpcPublicSubnet2RouteTable94F7E489" - }, - "SubnetId": { - "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet1" } - } - }, - "VpcPublicSubnet2DefaultRoute97F91067": { - "Type": "AWS::EC2::Route", - "Properties": { - "RouteTableId": { - "Ref": "VpcPublicSubnet2RouteTable94F7E489" + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" }, - "DestinationCidrBlock": "0.0.0.0/0", - "GatewayId": { - "Ref": "VpcIGWD7BA715C" + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet2" } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" }, - "DependsOn": [ - "VpcVPCGWBF912B6E" + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet2" + } ] - }, - "VpcPublicSubnet2EIP3C605A87": { - "Type": "AWS::EC2::EIP", - "Properties": { - "Domain": "vpc", - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet2" - } - ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" } - }, - "VpcPublicSubnet2NATGateway9182C01D": { - "Type": "AWS::EC2::NatGateway", - "Properties": { - "AllocationId": { - "Fn::GetAtt": [ - "VpcPublicSubnet2EIP3C605A87", - "AllocationId" - ] - }, - "SubnetId": { - "Ref": "VpcPublicSubnet2Subnet691E08A3" - }, - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet2" - } - ] + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" } }, - "VpcPrivateSubnet1Subnet536B997A": { - "Type": "AWS::EC2::Subnet", - "Properties": { - "CidrBlock": "10.0.128.0/18", - "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": "aws-ecs-integ-exec-command/Vpc/PrivateSubnet1" - } + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" ] - } - }, - "VpcPrivateSubnet1RouteTableB2C5B500": { - "Type": "AWS::EC2::RouteTable", - "Properties": { - "VpcId": { - "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" }, - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc/PrivateSubnet1" - } - ] - } - }, - "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { - "Type": "AWS::EC2::SubnetRouteTableAssociation", - "Properties": { - "RouteTableId": { - "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" }, - "SubnetId": { - "Ref": "VpcPrivateSubnet1Subnet536B997A" + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PrivateSubnet1" } - } - }, - "VpcPrivateSubnet1DefaultRouteBE02A9ED": { - "Type": "AWS::EC2::Route", - "Properties": { - "RouteTableId": { - "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" - }, - "DestinationCidrBlock": "0.0.0.0/0", - "NatGatewayId": { - "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PrivateSubnet1" } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" } - }, - "VpcPrivateSubnet2Subnet3788AAA1": { - "Type": "AWS::EC2::Subnet", - "Properties": { - "CidrBlock": "10.0.192.0/18", - "VpcId": { - "Ref": "Vpc8378EB38" - }, - "AvailabilityZone": "test-region-1b", - "MapPublicIpOnLaunch": false, - "Tags": [ - { - "Key": "aws-cdk:subnet-name", - "Value": "Private" - }, - { - "Key": "aws-cdk:subnet-type", - "Value": "Private" - }, - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc/PrivateSubnet2" - } - ] + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" } - }, - "VpcPrivateSubnet2RouteTableA678073B": { - "Type": "AWS::EC2::RouteTable", - "Properties": { - "VpcId": { - "Ref": "Vpc8378EB38" + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" }, - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc/PrivateSubnet2" - } - ] - } - }, - "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { - "Type": "AWS::EC2::SubnetRouteTableAssociation", - "Properties": { - "RouteTableId": { - "Ref": "VpcPrivateSubnet2RouteTableA678073B" + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" }, - "SubnetId": { - "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PrivateSubnet2" } - } - }, - "VpcPrivateSubnet2DefaultRoute060D2087": { - "Type": "AWS::EC2::Route", - "Properties": { - "RouteTableId": { - "Ref": "VpcPrivateSubnet2RouteTableA678073B" - }, - "DestinationCidrBlock": "0.0.0.0/0", - "NatGatewayId": { - "Ref": "VpcPublicSubnet2NATGateway9182C01D" + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc/PrivateSubnet2" } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" } - }, - "VpcIGWD7BA715C": { - "Type": "AWS::EC2::InternetGateway", - "Properties": { - "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-exec-command/Vpc" - } - ] - } - }, - "VpcVPCGWBF912B6E": { - "Type": "AWS::EC2::VPCGatewayAttachment", - "Properties": { - "VpcId": { - "Ref": "Vpc8378EB38" - }, - "InternetGatewayId": { - "Ref": "VpcIGWD7BA715C" - } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" } - }, - "LogGroupF5B46931": { - "Type": "AWS::Logs::LogGroup", - "Properties": { - "RetentionInDays": "731", - "KmsKeyId": { - "Fn::GetAtt": [ - "KmsKey46693ADD", - "Arn" - ] + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-exec-command/Vpc" } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" }, - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" - }, - "KmsKey46693ADD": { - "Type": "AWS::KMS::Key", - "Properties": { - "KeyPolicy": { - "Statement": [ - { - "Action": "kms:*", - "Effect": "Allow", - "Principal": { - "AWS": { + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "KmsKey46693ADD": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Encrypt*", + "kms:Decrypt*", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:Describe*" + ], + "Condition": { + "ArnLike": { + "kms:EncryptionContext:aws:logs:arn": { "Fn::Join": [ "", [ @@ -386,339 +401,323 @@ { "Ref": "AWS::Partition" }, - ":iam::", + ":logs:", { - "Ref": "AWS::AccountId" + "Ref": "AWS::Region" }, - ":root" - ] - ] - } - }, - "Resource": "*" - }, - { - "Action": "kms:*", - "Effect": "Allow", - "Principal": { - "AWS": { - "Fn::Join": [ - "", - [ - "arn:aws:iam::", + ":", { "Ref": "AWS::AccountId" }, - ":root" - ] - ] - } - }, - "Resource": "*" - }, - { - "Action": [ - "kms:Encrypt*", - "kms:Decrypt*", - "kms:ReEncrypt*", - "kms:GenerateDataKey*", - "kms:Describe*" - ], - "Condition": { - "ArnLike": { - "kms:EncryptionContext:aws:logs:arn": { - "Fn::Join": [ - "", - [ - "arn:aws:logs:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":*" - ] - ] - } - } - }, - "Effect": "Allow", - "Principal": { - "Service": { - "Fn::Join": [ - "", - [ - "logs.", - { - "Ref": "AWS::Region" - }, - ".amazonaws.com" + ":*" ] ] } - }, - "Resource": "*" - } - ], - "Version": "2012-10-17" - } - }, - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" - }, - "EcsExecBucket4F468651": { - "Type": "AWS::S3::Bucket", - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain", - "Properties": { - "BucketEncryption": { - "ServerSideEncryptionConfiguration": [ - { - "ServerSideEncryptionByDefault": { - "KMSMasterKeyID": { - "Fn::GetAtt": [ - "KmsKey46693ADD", - "Arn" - ] - }, - "SSEAlgorithm": "aws:kms" - } - } - ] - } - } - }, - "FargateCluster7CCD5F93": { - "Type": "AWS::ECS::Cluster", - "Properties": { - "Configuration": { - "ExecuteCommandConfiguration": { - "KmsKeyId": { - "Fn::GetAtt": [ - "KmsKey46693ADD", - "Arn" - ] - }, - "LogConfiguration": { - "CloudWatchEncryptionEnabled": true, - "CloudWatchLogGroupName": { - "Ref": "LogGroupF5B46931" - }, - "S3BucketName": { - "Ref": "EcsExecBucket4F468651" - }, - "S3EncryptionEnabled": true, - "S3KeyPrefix": "exec-output" - }, - "Logging": "OVERRIDE" - } - } - } - }, - "TaskDefTaskRole1EDB4A67": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "ecs-tasks.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - } - } - }, - "TaskDefTaskRoleDefaultPolicyA592CB18": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": [ - "ssmmessages:CreateControlChannel", - "ssmmessages:CreateDataChannel", - "ssmmessages:OpenControlChannel", - "ssmmessages:OpenDataChannel" - ], - "Effect": "Allow", - "Resource": "*" - }, - { - "Action": [ - "kms:Decrypt", - "kms:GenerateDataKey" - ], - "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "KmsKey46693ADD", - "Arn" - ] } }, - { - "Action": "logs:DescribeLogGroups", - "Effect": "Allow", - "Resource": "*" - }, - { - "Action": [ - "logs:CreateLogStream", - "logs:DescribeLogStreams", - "logs:PutLogEvents" - ], - "Effect": "Allow", - "Resource": { + "Effect": "Allow", + "Principal": { + "Service": { "Fn::Join": [ "", [ - "arn:aws:logs:", + "logs.", { "Ref": "AWS::Region" }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":log-group:", - { - "Ref": "LogGroupF5B46931" - }, - ":*" - ] - ] - } - }, - { - "Action": "s3:GetBucketLocation", - "Effect": "Allow", - "Resource": "*" - }, - { - "Action": "s3:PutObject", - "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - "arn:aws:s3:::", - { - "Ref": "EcsExecBucket4F468651" - }, - "/*" + ".amazonaws.com" ] ] } }, - { - "Action": "s3:GetEncryptionConfiguration", - "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - "arn:aws:s3:::", - { - "Ref": "EcsExecBucket4F468651" - } - ] - ] - } - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "TaskDefTaskRoleDefaultPolicyA592CB18", - "Roles": [{ - "Ref": "TaskDefTaskRole1EDB4A67" - }] - } - }, - "TaskDef54694570": { - "Type": "AWS::ECS::TaskDefinition", - "Properties": { - "ContainerDefinitions": [ - { - "Essential": true, - "Image": "amazon/amazon-ecs-sample", - "Name": "web" + "Resource": "*" } ], - "Cpu": "256", - "Family": "awsecsintegexeccommandTaskDef44709274", - "Memory": "512", - "NetworkMode": "awsvpc", - "RequiresCompatibilities": [ - "FARGATE" - ], - "TaskRoleArn": { - "Fn::GetAtt": [ - "TaskDefTaskRole1EDB4A67", - "Arn" - ] - } + "Version": "2012-10-17" } }, - "FargateServiceAC2B3B85": { - "Type": "AWS::ECS::Service", - "Properties": { - "Cluster": { - "Ref": "FargateCluster7CCD5F93" - }, - "DeploymentConfiguration": { - "MaximumPercent": 200, - "MinimumHealthyPercent": 50 - }, - "LaunchType": "FARGATE", - "EnableECSManagedTags": false, - "EnableExecuteCommand": true, - "NetworkConfiguration": { - "AwsvpcConfiguration": { - "AssignPublicIp": "DISABLED", - "SecurityGroups": [ - { + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "LogGroupF5B46931": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "KmsKeyId": { + "Fn::GetAtt": [ + "KmsKey46693ADD", + "Arn" + ] + }, + "RetentionInDays": 731 + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "EcsExecBucket4F468651": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "KMSMasterKeyID": { "Fn::GetAtt": [ - "FargateServiceSecurityGroup0A0E79CB", - "GroupId" + "KmsKey46693ADD", + "Arn" ] - } - ], - "Subnets": [ - { - "Ref": "VpcPrivateSubnet1Subnet536B997A" }, - { - "Ref": "VpcPrivateSubnet2Subnet3788AAA1" - } - ] + "SSEAlgorithm": "aws:kms" + } } - }, - "TaskDefinition": { - "Ref": "TaskDef54694570" - } + ] } }, - "FargateServiceSecurityGroup0A0E79CB": { - "Type": "AWS::EC2::SecurityGroup", - "Properties": { - "GroupDescription": "aws-ecs-integ-exec-command/FargateService/SecurityGroup", - "SecurityGroupEgress": [ + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "FargateCluster7CCD5F93": { + "Type": "AWS::ECS::Cluster", + "Properties": { + "Configuration": { + "ExecuteCommandConfiguration": { + "KmsKeyId": { + "Fn::GetAtt": [ + "KmsKey46693ADD", + "Arn" + ] + }, + "LogConfiguration": { + "CloudWatchEncryptionEnabled": true, + "CloudWatchLogGroupName": { + "Ref": "LogGroupF5B46931" + }, + "S3BucketName": { + "Ref": "EcsExecBucket4F468651" + }, + "S3EncryptionEnabled": true, + "S3KeyPrefix": "exec-output" + }, + "Logging": "OVERRIDE" + } + } + } + }, + "TaskDefTaskRole1EDB4A67": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDefTaskRoleDefaultPolicyA592CB18": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ssmmessages:CreateControlChannel", + "ssmmessages:CreateDataChannel", + "ssmmessages:OpenControlChannel", + "ssmmessages:OpenDataChannel" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:GenerateDataKey" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "KmsKey46693ADD", + "Arn" + ] + } + }, + { + "Action": "logs:DescribeLogGroups", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "logs:CreateLogStream", + "logs:DescribeLogStreams", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:", + { + "Ref": "LogGroupF5B46931" + }, + ":*" + ] + ] + } + }, + { + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "s3:PutObject", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "EcsExecBucket4F468651" + }, + "/*" + ] + ] + } + }, { - "CidrIp": "0.0.0.0/0", - "Description": "Allow all outbound traffic by default", - "IpProtocol": "-1" + "Action": "s3:GetEncryptionConfiguration", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "EcsExecBucket4F468651" + } + ] + ] + } } ], - "VpcId": { - "Ref": "Vpc8378EB38" + "Version": "2012-10-17" + }, + "PolicyName": "TaskDefTaskRoleDefaultPolicyA592CB18", + "Roles": [ + { + "Ref": "TaskDefTaskRole1EDB4A67" + } + ] + } + }, + "TaskDef54694570": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": "amazon/amazon-ecs-sample", + "Name": "web" + } + ], + "Cpu": "256", + "Family": "awsecsintegexeccommandTaskDef44709274", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + } + } + }, + "FargateServiceAC2B3B85": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Ref": "FargateCluster7CCD5F93" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "EnableECSManagedTags": false, + "EnableExecuteCommand": true, + "LaunchType": "FARGATE", + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "FargateServiceSecurityGroup0A0E79CB", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + } + }, + "TaskDefinition": { + "Ref": "TaskDef54694570" + } + } + }, + "FargateServiceSecurityGroup0A0E79CB": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ-exec-command/FargateService/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" } + ], + "VpcId": { + "Ref": "Vpc8378EB38" } } } + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.runtime.expected.json b/packages/@aws-cdk/aws-ecs/test/fargate/integ.runtime.expected.json new file mode 100644 index 0000000000000..839283e8c6c46 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.runtime.expected.json @@ -0,0 +1,712 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-runtime/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "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": "aws-ecs-integ-runtime/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-runtime/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": "aws-ecs-integ-runtime/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-runtime/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-runtime/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-runtime/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-runtime/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-runtime/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "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": "aws-ecs-integ-runtime/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-runtime/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" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-runtime/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-runtime/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-runtime/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "FargateCluster7CCD5F93": { + "Type": "AWS::ECS::Cluster" + }, + "TaskDefWindowsTaskRole87844D4F": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDefWindows46D24ABF": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": "mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019", + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "TaskDefWindowswindowsservercoreLogGroupCF570877" + }, + "awslogs-stream-prefix": "win-iis-on-fargate", + "awslogs-region": { + "Ref": "AWS::Region" + } + } + }, + "Name": "windowsservercore", + "PortMappings": [ + { + "ContainerPort": 80, + "Protocol": "tcp" + } + ] + } + ], + "Cpu": "1024", + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "TaskDefWindowsExecutionRole7DDEBC2E", + "Arn" + ] + }, + "Family": "awsecsintegruntimeTaskDefWindows19C23F8C", + "Memory": "2048", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "RuntimePlatform": { + "CpuArchitecture": "X86_64", + "OperatingSystemFamily": "WINDOWS_SERVER_2019_CORE" + }, + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefWindowsTaskRole87844D4F", + "Arn" + ] + } + } + }, + "TaskDefWindowswindowsservercoreLogGroupCF570877": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "TaskDefWindowsExecutionRole7DDEBC2E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDefWindowsExecutionRoleDefaultPolicyF0E0215E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TaskDefWindowswindowsservercoreLogGroupCF570877", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TaskDefWindowsExecutionRoleDefaultPolicyF0E0215E", + "Roles": [ + { + "Ref": "TaskDefWindowsExecutionRole7DDEBC2E" + } + ] + } + }, + "TaskDefGraviton2TaskRole32C7B421": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDefGraviton21BE43931": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": "public.ecr.aws/nginx/nginx:latest-arm64v8", + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "TaskDefGraviton2webarm64LogGroup7D0FFEB3" + }, + "awslogs-stream-prefix": "graviton2-on-fargate", + "awslogs-region": { + "Ref": "AWS::Region" + } + } + }, + "Name": "webarm64", + "PortMappings": [ + { + "ContainerPort": 80, + "Protocol": "tcp" + } + ] + } + ], + "Cpu": "256", + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "TaskDefGraviton2ExecutionRoleFB11C2FF", + "Arn" + ] + }, + "Family": "awsecsintegruntimeTaskDefGraviton28E28B263", + "Memory": "1024", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "RuntimePlatform": { + "CpuArchitecture": "ARM64", + "OperatingSystemFamily": "LINUX" + }, + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefGraviton2TaskRole32C7B421", + "Arn" + ] + } + } + }, + "TaskDefGraviton2webarm64LogGroup7D0FFEB3": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "TaskDefGraviton2ExecutionRoleFB11C2FF": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDefGraviton2ExecutionRoleDefaultPolicyB09F36E7": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TaskDefGraviton2webarm64LogGroup7D0FFEB3", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TaskDefGraviton2ExecutionRoleDefaultPolicyB09F36E7", + "Roles": [ + { + "Ref": "TaskDefGraviton2ExecutionRoleFB11C2FF" + } + ] + } + }, + "FargateServiceWindowsRuntimeServiceBBDEC2BF": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Ref": "FargateCluster7CCD5F93" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "EnableECSManagedTags": false, + "LaunchType": "FARGATE", + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "FargateServiceWindowsRuntimeSecurityGroupABEA7E23", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + } + }, + "TaskDefinition": { + "Ref": "TaskDefWindows46D24ABF" + } + } + }, + "FargateServiceWindowsRuntimeSecurityGroupABEA7E23": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ-runtime/FargateServiceWindowsRuntime/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "FargateServiceGraviton2RuntimeService2BDDD2C2": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Ref": "FargateCluster7CCD5F93" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "EnableECSManagedTags": false, + "LaunchType": "FARGATE", + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "FargateServiceGraviton2RuntimeSecurityGroup9D707C93", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + } + }, + "TaskDefinition": { + "Ref": "TaskDefGraviton21BE43931" + } + } + }, + "FargateServiceGraviton2RuntimeSecurityGroup9D707C93": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ-runtime/FargateServiceGraviton2Runtime/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.runtime.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.runtime.ts new file mode 100644 index 0000000000000..2fa66c3de43f7 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.runtime.ts @@ -0,0 +1,55 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import * as ecs from '../../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-ecs-integ-runtime'); + +const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 2 }); + + +const cluster = new ecs.Cluster(stack, 'FargateCluster', { + vpc, +}); + +const taskDefinitionwindows = new ecs.FargateTaskDefinition(stack, 'TaskDefWindows', { + runtimePlatform: { + operatingSystemFamily: ecs.OperatingSystemFamily.WINDOWS_SERVER_2019_CORE, + cpuArchitecture: ecs.CpuArchitecture.X86_64, + }, + cpu: 1024, + memoryLimitMiB: 2048, +}); + +const taskDefinitiongraviton2 = new ecs.FargateTaskDefinition(stack, 'TaskDefGraviton2', { + runtimePlatform: { + operatingSystemFamily: ecs.OperatingSystemFamily.LINUX, + cpuArchitecture: ecs.CpuArchitecture.ARM64, + }, + cpu: 256, + memoryLimitMiB: 1024, +}); + +taskDefinitionwindows.addContainer('windowsservercore', { + logging: ecs.LogDriver.awsLogs({ streamPrefix: 'win-iis-on-fargate' }), + portMappings: [{ containerPort: 80 }], + image: ecs.ContainerImage.fromRegistry('mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019'), +}); + +taskDefinitiongraviton2.addContainer('webarm64', { + logging: ecs.LogDriver.awsLogs({ streamPrefix: 'graviton2-on-fargate' }), + portMappings: [{ containerPort: 80 }], + image: ecs.ContainerImage.fromRegistry('public.ecr.aws/nginx/nginx:latest-arm64v8'), +}); + +new ecs.FargateService(stack, 'FargateServiceWindowsRuntime', { + cluster, + taskDefinition: taskDefinitionwindows, +}); + +new ecs.FargateService(stack, 'FargateServiceGraviton2Runtime', { + cluster, + taskDefinition: taskDefinitiongraviton2, +}); + +app.synth(); \ No newline at end of file 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 6ab96b05df364..547382ab8d3de 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,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as ssm from '@aws-cdk/aws-ssm'; import * as cdk from '@aws-cdk/core'; @@ -24,23 +24,21 @@ describe('firelens log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'awsfirelens', }, - }, - { + }), + Match.objectLike({ Essential: true, FirelensConfiguration: { Type: 'fluentbit', }, - }, + }), ], }); - - }); test('create a firelens log driver with secret options', () => { @@ -71,9 +69,9 @@ describe('firelens log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'awsfirelens', Options: { @@ -116,19 +114,19 @@ describe('firelens log driver', () => { }, ], }, - }, - { + }), + Match.objectLike({ Essential: true, FirelensConfiguration: { Type: 'fluentbit', }, - }, + }), ], }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: [ + Statement: Match.arrayWith([ { Action: [ 'secretsmanager:GetSecretValue', @@ -168,12 +166,10 @@ describe('firelens log driver', () => { ], }, }, - ], + ]), Version: '2012-10-17', }, }); - - }); test('create a firelens log driver to route logs to CloudWatch Logs with Fluent Bit', () => { @@ -193,9 +189,9 @@ describe('firelens log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'awsfirelens', Options: { @@ -206,17 +202,15 @@ describe('firelens log driver', () => { log_stream_prefix: 'from-fluent-bit', }, }, - }, - { + }), + Match.objectLike({ Essential: true, FirelensConfiguration: { Type: 'fluentbit', }, - }, + }), ], }); - - }); test('create a firelens log driver to route logs to kinesis firehose Logs with Fluent Bit', () => { @@ -234,9 +228,9 @@ describe('firelens log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'awsfirelens', Options: { @@ -245,17 +239,15 @@ describe('firelens log driver', () => { delivery_stream: 'my-stream', }, }, - }, - { + }), + Match.objectLike({ Essential: true, FirelensConfiguration: { Type: 'fluentbit', }, - }, + }), ], }); - - }); describe('Firelens Configuration', () => { @@ -270,7 +262,7 @@ describe('firelens log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Essential: true, @@ -283,7 +275,6 @@ describe('firelens log driver', () => { }, ], }); - }); test('fluent-bit log router container with options', () => { @@ -304,9 +295,9 @@ describe('firelens log driver', () => { }); // THEN - expect(stack2).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack2).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Essential: true, MemoryReservation: 50, Name: 'log_router', @@ -318,11 +309,9 @@ describe('firelens log driver', () => { 'config-file-value': 'arn:aws:s3:::mybucket/fluent.conf', }, }, - }, + }), ], }); - - }); test('fluent-bit log router with file config type', () => { @@ -342,9 +331,9 @@ describe('firelens log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Essential: true, MemoryReservation: 50, Name: 'log_router', @@ -356,11 +345,9 @@ describe('firelens log driver', () => { 'config-file-value': '/my/working/dir/firelens/config', }, }, - }, + }), ], }); - - }); }); }); 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 81c17b8c0b76f..7bd81fed41221 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,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as ecs from '../lib'; @@ -10,8 +10,6 @@ describe('fluentd log driver', () => { beforeEach(() => { stack = new cdk.Stack(); td = new ecs.Ec2TaskDefinition(stack, 'TaskDefinition'); - - }); test('create a fluentd log driver with options', () => { @@ -25,20 +23,18 @@ describe('fluentd log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'fluentd', Options: { tag: 'hello', }, }, - }, + }), ], }); - - }); test('create a fluentd log driver without options', () => { @@ -50,17 +46,15 @@ describe('fluentd log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'fluentd', }, - }, + }), ], }); - - }); test('create a fluentd log driver with all possible options', () => { @@ -91,9 +85,9 @@ describe('fluentd log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'fluentd', Options: { @@ -109,11 +103,9 @@ describe('fluentd log driver', () => { 'env-regex': '[0-9]{1}', }, }, - }, + }), ], }); - - }); test('create a fluentd log driver using fluentd', () => { @@ -125,16 +117,14 @@ describe('fluentd log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'fluentd', }, - }, + }), ], }); - - }); }); 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 e8cdce10736e1..f6800bb48ac01 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,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as ecs from '../lib'; @@ -25,20 +25,18 @@ describe('gelf log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'gelf', Options: { 'gelf-address': 'my-gelf-address', }, }, - }, + }), ], }); - - }); test('create a gelf log driver using gelf with minimum options', () => { @@ -52,19 +50,17 @@ describe('gelf log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'gelf', Options: { 'gelf-address': 'my-gelf-address', }, }, - }, + }), ], }); - - }); }); 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 3f409739d76ed..9ff3023b1e965 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,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { SynthUtils } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ecr from '@aws-cdk/aws-ecr'; import * as cdk from '@aws-cdk/core'; import * as ecs from '../../lib'; @@ -16,10 +15,8 @@ describe('tag parameter container image', () => { }); expect(() => { - SynthUtils.synthesize(stack); + Template.fromStack(stack); }).toThrow(/TagParameterContainerImage must be used in a container definition when using tagParameterName/); - - }); test('throws an error when tagParameterValue() is used without binding the image', () => { @@ -32,10 +29,8 @@ describe('tag parameter container image', () => { }); expect(() => { - SynthUtils.synthesize(stack); + Template.fromStack(stack); }).toThrow(/TagParameterContainerImage must be used in a container definition when using tagParameterValue/); - - }); test('can be used in a cross-account manner', () => { @@ -67,7 +62,7 @@ describe('tag parameter container image', () => { }); // THEN - expect(pipelineStack).toHaveResourceLike('AWS::ECR::Repository', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::ECR::Repository', { RepositoryName: repositoryName, RepositoryPolicyText: { Statement: [{ @@ -89,12 +84,12 @@ describe('tag parameter container image', () => { }], }, }); - expect(serviceStack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(serviceStack).hasResourceProperties('AWS::IAM::Role', { RoleName: 'servicestackionexecutionrolee7e2d9a783a54eb795f4', }); - expect(serviceStack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(serviceStack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ Image: { 'Fn::Join': ['', [ { @@ -126,11 +121,9 @@ describe('tag parameter container image', () => { { Ref: 'ServiceTaskDefinitionContainerImageTagParamCEC9D0BA' }, ]], }, - }, + }), ], }); - - }); }); }); 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 fdf67efc3bb4f..b3f25650b9e58 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,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as ecs from '../lib'; @@ -10,8 +10,6 @@ describe('journald log driver', () => { beforeEach(() => { stack = new cdk.Stack(); td = new ecs.Ec2TaskDefinition(stack, 'TaskDefinition'); - - }); test('create a journald log driver with options', () => { @@ -25,20 +23,18 @@ describe('journald log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'journald', Options: { tag: 'hello', }, }, - }, + }), ], }); - - }); test('create a journald log driver without options', () => { @@ -50,17 +46,15 @@ describe('journald log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'journald', }, - }, + }), ], }); - - }); test('create a journald log driver using journald', () => { @@ -72,17 +66,15 @@ describe('journald log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'journald', Options: {}, }, - }, + }), ], }); - - }); }); 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 0c21ff2d0b5f1..ed2622dc79eaa 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,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as ecs from '../lib'; @@ -25,20 +25,18 @@ describe('json file log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'json-file', Options: { env: 'hello', }, }, - }, + }), ], }); - - }); test('create a json-file log driver without options', () => { @@ -50,17 +48,15 @@ describe('json file log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'json-file', }, - }, + }), ], }); - - }); test('create a json-file log driver using json-file', () => { @@ -72,17 +68,15 @@ describe('json file log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'json-file', Options: {}, }, - }, + }), ], }); - - }); }); 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 cb7678d9bc6e4..b33b4155615a4 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,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as ssm from '@aws-cdk/aws-ssm'; import * as cdk from '@aws-cdk/core'; @@ -28,9 +28,9 @@ describe('splunk log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'splunk', Options: { @@ -45,11 +45,9 @@ describe('splunk log driver', () => { }, }], }, - }, + }), ], }); - - }); test('create a splunk log driver using splunk with minimum options', () => { @@ -64,9 +62,9 @@ describe('splunk log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'splunk', Options: { @@ -81,11 +79,9 @@ describe('splunk log driver', () => { }, }], }, - }, + }), ], }); - - }); test('create a splunk log driver using splunk with sourcetype defined', () => { @@ -101,9 +97,9 @@ describe('splunk log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'splunk', Options: { @@ -119,11 +115,9 @@ describe('splunk log driver', () => { }, }], }, - }, + }), ], }); - - }); test('create a splunk log driver using secret splunk token from a new secret', () => { @@ -139,9 +133,9 @@ describe('splunk log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'splunk', Options: { @@ -156,11 +150,9 @@ describe('splunk log driver', () => { }, ], }, - }, + }), ], }); - - }); test('create a splunk log driver using secret splunk token from systems manager parameter store', () => { @@ -179,9 +171,9 @@ describe('splunk log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'splunk', Options: { @@ -213,11 +205,9 @@ describe('splunk log driver', () => { }, ], }, - }, + }), ], }); - - }); test('throws when neither token nor secret token are provided', () => { @@ -230,7 +220,5 @@ describe('splunk log driver', () => { memoryLimitMiB: 128, }); }).toThrow('Please provide either token or secretToken.'); - - }); }); 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 ae32f55ecd863..fa77acb52e09b 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,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as ecs from '../lib'; @@ -10,8 +10,6 @@ describe('syslog log driver', () => { beforeEach(() => { stack = new cdk.Stack(); td = new ecs.Ec2TaskDefinition(stack, 'TaskDefinition'); - - }); test('create a syslog log driver with options', () => { @@ -25,20 +23,18 @@ describe('syslog log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'syslog', Options: { tag: 'hello', }, }, - }, + }), ], }); - - }); test('create a syslog log driver without options', () => { @@ -50,17 +46,15 @@ describe('syslog log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'syslog', }, - }, + }), ], }); - - }); test('create a syslog log driver using syslog', () => { @@ -72,17 +66,15 @@ describe('syslog log driver', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ - { + Match.objectLike({ LogConfiguration: { LogDriver: 'syslog', Options: {}, }, - }, + }), ], }); - - }); }); 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 07b3a8211da06..11e383565af81 100644 --- a/packages/@aws-cdk/aws-ecs/test/task-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/task-definition.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as ecs from '../lib'; @@ -17,7 +17,7 @@ describe('task definition', () => { }); // THEN - expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { NetworkMode: 'awsvpc', }); diff --git a/packages/@aws-cdk/aws-efs/README.md b/packages/@aws-cdk/aws-efs/README.md index 1a26f9e0e9c5a..058198d0c897e 100644 --- a/packages/@aws-cdk/aws-efs/README.md +++ b/packages/@aws-cdk/aws-efs/README.md @@ -36,7 +36,7 @@ const fileSystem = new efs.FileSystem(this, 'MyEfsFileSystem', { vpc: new ec2.Vpc(this, 'VPC'), lifecyclePolicy: efs.LifecyclePolicy.AFTER_14_DAYS, // files are not transitioned to infrequent access (IA) storage by default performanceMode: efs.PerformanceMode.GENERAL_PURPOSE, // default - outInfrequentAccessPolicy: efs.OutOfInfrequentAccessPolicy.AFTER_1_ACCESS, // files are not transitioned back from (infrequent access) IA to primary storage by default + outOfInfrequentAccessPolicy: efs.OutOfInfrequentAccessPolicy.AFTER_1_ACCESS, // files are not transitioned back from (infrequent access) IA to primary storage by default }); ``` @@ -159,6 +159,6 @@ You can configure the file system to be destroyed on stack deletion by setting a ```ts const fileSystem = new efs.FileSystem(this, 'EfsFileSystem', { vpc: new ec2.Vpc(this, 'VPC'), - removalPolicy: RemovalPolicy.DESTROY + removalPolicy: RemovalPolicy.DESTROY, }); ``` diff --git a/packages/@aws-cdk/aws-efs/lib/access-point.ts b/packages/@aws-cdk/aws-efs/lib/access-point.ts index 07039df8d6a6e..055be8306e521 100644 --- a/packages/@aws-cdk/aws-efs/lib/access-point.ts +++ b/packages/@aws-cdk/aws-efs/lib/access-point.ts @@ -267,7 +267,7 @@ class ImportedAccessPoint extends AccessPointBase { public get fileSystem() { if (!this._fileSystem) { - throw new Error("fileSystem is not available when 'fromAccessPointId()' is used. Use 'fromAccessPointAttributes()' instead"); + throw new Error("fileSystem is only available if 'fromAccessPointAttributes()' is used and a fileSystem is passed in as an attribute."); } return this._fileSystem; diff --git a/packages/@aws-cdk/aws-efs/package.json b/packages/@aws-cdk/aws-efs/package.json index 6ab9f630d9c60..73151d18d3aff 100644 --- a/packages/@aws-cdk/aws-efs/package.json +++ b/packages/@aws-cdk/aws-efs/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -77,7 +84,7 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-ec2": "0.0.0", diff --git a/packages/@aws-cdk/aws-efs/rosetta/with-filesystem-instance.ts-fixture b/packages/@aws-cdk/aws-efs/rosetta/with-filesystem-instance.ts-fixture index 092b572afa726..427e2ed030b4b 100644 --- a/packages/@aws-cdk/aws-efs/rosetta/with-filesystem-instance.ts-fixture +++ b/packages/@aws-cdk/aws-efs/rosetta/with-filesystem-instance.ts-fixture @@ -3,6 +3,7 @@ import { Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import * as efs from '@aws-cdk/aws-efs'; import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; class Fixture extends Stack { constructor(scope: Construct, id: string) { diff --git a/packages/@aws-cdk/aws-efs/test/access-point.test.ts b/packages/@aws-cdk/aws-efs/test/access-point.test.ts index dd29d5aff5dc3..488dbb93dee4f 100644 --- a/packages/@aws-cdk/aws-efs/test/access-point.test.ts +++ b/packages/@aws-cdk/aws-efs/test/access-point.test.ts @@ -48,7 +48,7 @@ test('import an AccessPoint using fromAccessPointId throws when accessing fileSy }); const imported = AccessPoint.fromAccessPointId(stack, 'ImportedAccessPoint', ap.accessPointId); // THEN - expect(() => imported.fileSystem).toThrow(/fileSystem is not available when 'fromAccessPointId\(\)' is used. Use 'fromAccessPointAttributes\(\)' instead/); + expect(() => imported.fileSystem).toThrow(/fileSystem is only available if 'fromAccessPointAttributes\(\)' is used and a fileSystem is passed in as an attribute./); }); test('import an AccessPoint using fromAccessPointAttributes and the accessPointId', () => { diff --git a/packages/@aws-cdk/aws-eks-legacy/README.md b/packages/@aws-cdk/aws-eks-legacy/README.md index 2260236537399..c6316569d2bc2 100644 --- a/packages/@aws-cdk/aws-eks-legacy/README.md +++ b/packages/@aws-cdk/aws-eks-legacy/README.md @@ -49,7 +49,7 @@ cluster.addResource('mypod', { }); ``` -Here is a [complete sample](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/aws-eks/test/integ.eks-kubectl.lit.ts). +Here is a [complete sample](https://github.com/aws/aws-cdk/blob/master/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-kubectl.lit.ts). ### Capacity diff --git a/packages/@aws-cdk/aws-eks-legacy/package.json b/packages/@aws-cdk/aws-eks-legacy/package.json index 995df2bc8db26..ede28f6869cdf 100644 --- a/packages/@aws-cdk/aws-eks-legacy/package.json +++ b/packages/@aws-cdk/aws-eks-legacy/package.json @@ -77,13 +77,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-autoscaling": "0.0.0", diff --git a/packages/@aws-cdk/aws-eks-legacy/test/awsauth.test.ts b/packages/@aws-cdk/aws-eks-legacy/test/awsauth.test.ts index 3a5f28f648441..f642836ded6e2 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/awsauth.test.ts +++ b/packages/@aws-cdk/aws-eks-legacy/test/awsauth.test.ts @@ -1,6 +1,6 @@ -import '@aws-cdk/assert-internal/jest'; -import { describeDeprecated } from '@aws-cdk/cdk-build-tools'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; +import { describeDeprecated } from '@aws-cdk/cdk-build-tools'; import { Cluster, KubernetesResource } from '../lib'; import { AwsAuth } from '../lib/aws-auth'; import { testFixtureNoVpc } from './util'; @@ -17,7 +17,7 @@ describeDeprecated('awsauth', () => { new AwsAuth(stack, 'AwsAuth', { cluster }); // THEN - expect(stack).toHaveResource(KubernetesResource.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesResource.RESOURCE_TYPE, { Manifest: JSON.stringify([{ apiVersion: 'v1', kind: 'ConfigMap', @@ -44,8 +44,8 @@ describeDeprecated('awsauth', () => { cluster.awsAuth.addAccount('5566776655'); // THEN - expect(stack).toCountResources(KubernetesResource.RESOURCE_TYPE, 1); - expect(stack).toHaveResource(KubernetesResource.RESOURCE_TYPE, { + Template.fromStack(stack).resourceCountIs(KubernetesResource.RESOURCE_TYPE, 1); + Template.fromStack(stack).hasResourceProperties(KubernetesResource.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -106,7 +106,7 @@ describeDeprecated('awsauth', () => { cluster.awsAuth.addUserMapping(user, { groups: ['group2'] }); // THEN - expect(stack).toHaveResource(KubernetesResource.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesResource.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', diff --git a/packages/@aws-cdk/aws-eks-legacy/test/cluster.test.ts b/packages/@aws-cdk/aws-eks-legacy/test/cluster.test.ts index b7f1666fb79b0..61188e858110a 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-eks-legacy/test/cluster.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import { describeDeprecated } from '@aws-cdk/cdk-build-tools'; @@ -18,7 +18,7 @@ describeDeprecated('cluster', () => { new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0 }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Cluster', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Cluster', { ResourcesVpcConfig: { SubnetIds: [ { Ref: 'VPCPublicSubnet1SubnetB4246D30' }, @@ -40,7 +40,7 @@ describeDeprecated('cluster', () => { new eks.Cluster(stack, 'cluster'); // THEN - expect(stack).toHaveResource('AWS::EC2::VPC'); + Template.fromStack(stack).resourceCountIs('AWS::EC2::VPC', 1); }); @@ -55,8 +55,8 @@ describeDeprecated('cluster', () => { // THEN expect(cluster.defaultCapacity).toBeDefined(); - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { DesiredCapacity: '2' }); - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm5.large' }); + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { DesiredCapacity: '2' }); + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm5.large' }); }); @@ -72,8 +72,8 @@ describeDeprecated('cluster', () => { // THEN expect(cluster.defaultCapacity).toBeDefined(); - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { DesiredCapacity: '10' }); - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm2.xlarge' }); + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { DesiredCapacity: '10' }); + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm2.xlarge' }); }); @@ -86,8 +86,8 @@ describeDeprecated('cluster', () => { // THEN expect(cluster.defaultCapacity).toBeUndefined(); - expect(stack).not.toHaveResource('AWS::AutoScaling::AutoScalingGroup'); - expect(stack).not.toHaveResource('AWS::AutoScaling::LaunchConfiguration'); + Template.fromStack(stack).resourceCountIs('AWS::AutoScaling::AutoScalingGroup', 0); + Template.fromStack(stack).resourceCountIs('AWS::AutoScaling::LaunchConfiguration', 0); }); }); @@ -100,7 +100,7 @@ describeDeprecated('cluster', () => { new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0 }); // THEN - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { Tags: [ { Key: 'aws-cdk:subnet-name', Value: 'Private' }, { Key: 'aws-cdk:subnet-type', Value: 'Private' }, @@ -120,7 +120,7 @@ describeDeprecated('cluster', () => { new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0 }); // THEN - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { MapPublicIpOnLaunch: true, Tags: [ { Key: 'aws-cdk:subnet-name', Value: 'Public' }, @@ -144,7 +144,7 @@ describeDeprecated('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { Tags: [ { Key: { 'Fn::Join': ['', ['kubernetes.io/cluster/', { Ref: 'ClusterEB0386A7' }]] }, @@ -182,7 +182,7 @@ describeDeprecated('cluster', () => { new cdk.CfnOutput(stack2, 'ClusterARN', { value: imported.clusterArn }); // THEN - expect(stack2).toMatchTemplate({ + Template.fromStack(stack2).templateMatches({ Outputs: { ClusterARN: { Value: { @@ -216,7 +216,7 @@ describeDeprecated('cluster', () => { new eks.Cluster(stack, 'Cluster', { vpc, mastersRole: role, defaultCapacity: 0 }); // THEN - expect(stack).toHaveResource(eks.KubernetesResource.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -247,11 +247,11 @@ describeDeprecated('cluster', () => { cluster.addResource('manifest2', { bar: 123 }, { boor: [1, 2, 3] }); // THEN - expect(stack).toHaveResource(eks.KubernetesResource.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: '[{"foo":123}]', }); - expect(stack).toHaveResource(eks.KubernetesResource.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: '[{"bar":123},{"boor":[1,2,3]}]', }); @@ -269,7 +269,7 @@ describeDeprecated('cluster', () => { }); // THEN - expect(stack).toHaveResource(eks.KubernetesResource.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -302,7 +302,7 @@ describeDeprecated('cluster', () => { }); // THEN - expect(stack).not.toHaveResource(eks.KubernetesResource.RESOURCE_TYPE); + Template.fromStack(stack).resourceCountIs(eks.KubernetesResource.RESOURCE_TYPE, 0); }); @@ -317,7 +317,7 @@ describeDeprecated('cluster', () => { }); // THEN - expect(stack).not.toHaveResource(eks.KubernetesResource.RESOURCE_TYPE); + Template.fromStack(stack).resourceCountIs(eks.KubernetesResource.RESOURCE_TYPE, 0); }); @@ -524,7 +524,7 @@ describeDeprecated('cluster', () => { }); // THEN - expect(stack).toHaveResource(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: JSON.stringify(spotInterruptHandler()) }); + Template.fromStack(stack).hasResourceProperties(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: JSON.stringify(spotInterruptHandler()) }); }); @@ -540,7 +540,7 @@ describeDeprecated('cluster', () => { }); // THEN - expect(stack).not.toHaveResource(eks.KubernetesResource.RESOURCE_TYPE); + Template.fromStack(stack).resourceCountIs(eks.KubernetesResource.RESOURCE_TYPE, 0); }); 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 index 9606f892d5f1c..90b814a8bc0b3 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/helm-chart.test.ts +++ b/packages/@aws-cdk/aws-eks-legacy/test/helm-chart.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { describeDeprecated } from '@aws-cdk/cdk-build-tools'; import * as eks from '../lib'; import { testFixtureCluster } from './util'; @@ -15,7 +15,7 @@ describeDeprecated('helm chart', () => { new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Namespace: 'default' }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Namespace: 'default' }); }); test('should have a lowercase default release name', () => { @@ -26,7 +26,7 @@ describeDeprecated('helm chart', () => { new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Release: 'stackmychartff398361' }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Release: 'stackmychartff398361' }); }); test('should trim the last 63 of the default release name', () => { @@ -37,7 +37,7 @@ describeDeprecated('helm chart', () => { new eks.HelmChart(stack, 'MyChartNameWhichISMostProbablyLongerThenSixtyThreeCharacters', { cluster, chart: 'chart' }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Release: 'rtnamewhichismostprobablylongerthensixtythreecharactersb800614d' }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Release: 'rtnamewhichismostprobablylongerthensixtythreecharactersb800614d' }); }); test('with values', () => { @@ -48,7 +48,7 @@ describeDeprecated('helm chart', () => { new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart', values: { foo: 123 } }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Values: '{\"foo\":123}' }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Values: '{\"foo\":123}' }); }); }); diff --git a/packages/@aws-cdk/aws-eks-legacy/test/manifest.test.ts b/packages/@aws-cdk/aws-eks-legacy/test/manifest.test.ts index 9a3de8f587f4c..2558945d61e94 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/manifest.test.ts +++ b/packages/@aws-cdk/aws-eks-legacy/test/manifest.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { describeDeprecated } from '@aws-cdk/cdk-build-tools'; import { Cluster, KubernetesResource } from '../lib'; import { testFixtureNoVpc } from './util'; @@ -69,7 +69,7 @@ describeDeprecated('manifest', () => { manifest, }); - expect(stack).toHaveResource(KubernetesResource.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesResource.RESOURCE_TYPE, { Manifest: JSON.stringify(manifest), }); diff --git a/packages/@aws-cdk/aws-eks/README.md b/packages/@aws-cdk/aws-eks/README.md index 1403e56d817fe..001a2cbb3022a 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -1144,6 +1144,24 @@ cluster.addHelmChart('test-chart', { }); ``` +### OCI Charts + +OCI charts are also supported. +Also replace the `${VARS}` with appropriate values. + +```ts +declare const cluster: eks.Cluster; +// option 1: use a construct +new eks.HelmChart(this, 'MyOCIChart', { + cluster, + chart: 'some-chart', + repository: 'oci://${ACCOUNT_ID}.dkr.ecr.${ACCOUNT_REGION}.amazonaws.com/${REPO_NAME}', + namespace: 'oci', + version: '0.0.1' +}); + +``` + Helm charts are implemented as CloudFormation resources in CDK. This means that if the chart is deleted from your code (or the stack is deleted), the next `cdk deploy` will issue a `helm uninstall` command and the @@ -1174,11 +1192,11 @@ chart2.node.addDependency(chart1); [CDK8s](https://cdk8s.io/) is an open-source library that enables Kubernetes manifest authoring using familiar programming languages. It is founded on the same technologies as the AWS CDK, such as [`constructs`](https://github.com/aws/constructs) and [`jsii`](https://github.com/aws/jsii). -> To learn more about cdk8s, visit the [Getting Started](https://github.com/awslabs/cdk8s/tree/master/docs/getting-started) tutorials. +> To learn more about cdk8s, visit the [Getting Started](https://cdk8s.io/docs/latest/getting-started/) tutorials. The EKS module natively integrates with cdk8s and allows you to apply cdk8s charts on AWS EKS clusters via the `cluster.addCdk8sChart` method. -In addition to `cdk8s`, you can also use [`cdk8s+`](https://github.com/awslabs/cdk8s/tree/master/packages/cdk8s-plus), which provides higher level abstraction for the core kubernetes api objects. +In addition to `cdk8s`, you can also use [`cdk8s+`](https://cdk8s.io/docs/latest/plus/), which provides higher level abstraction for the core kubernetes api objects. You can think of it like the `L2` constructs for Kubernetes. Any other `cdk8s` based libraries are also supported, for example [`cdk8s-debore`](https://github.com/toricls/cdk8s-debore). To get started, add the following dependencies to your `package.json` file: @@ -1290,7 +1308,7 @@ export class LoadBalancedWebService extends constructs.Construct { If you find yourself unable to use `cdk8s+`, or just like to directly use the `k8s` native objects or CRD's, you can do so by manually importing them using the `cdk8s-cli`. -See [Importing kubernetes objects](https://github.com/awslabs/cdk8s/tree/master/packages/cdk8s-cli#import) for detailed instructions. +See [Importing kubernetes objects](https://cdk8s.io/docs/latest/cli/import/) for detailed instructions. ## Patching Kubernetes Resources @@ -1397,6 +1415,31 @@ Kubernetes [endpoint access](#endpoint-access), you must also specify: * `kubectlPrivateSubnetIds` - a list of private VPC subnets IDs that will be used to access the Kubernetes endpoint. +## Logging + +EKS supports cluster logging for 5 different types of events: + +* API requests to the cluster. +* Cluster access via the Kubernetes API. +* Authentication requests into the cluster. +* State of cluster controllers. +* Scheduling decisions. + +You can enable logging for each one separately using the `clusterLogging` +property. For example: + +```ts +const cluster = new eks.Cluster(this, 'Cluster', { + // ... + version: eks.KubernetesVersion.V1_21, + clusterLogging: [ + eks.ClusterLoggingTypes.API, + eks.ClusterLoggingTypes.AUTHENTICATOR, + eks.ClusterLoggingTypes.SCHEDULER, + ], +}); +``` + ## Known Issues and Limitations * [One cluster per stack](https://github.com/aws/aws-cdk/issues/10073) diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts index 61a33ddb3ab05..0ad46af16eaef 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts @@ -285,6 +285,10 @@ function parseProps(props: any): aws.EKS.CreateClusterRequest { parsed.resourcesVpcConfig.endpointPublicAccess = parsed.resourcesVpcConfig.endpointPublicAccess === 'true'; } + if (typeof (parsed.logging?.clusterLogging[0].enabled) === 'string') { + parsed.logging.clusterLogging[0].enabled = parsed.logging.clusterLogging[0].enabled === 'true'; + } + return parsed; } diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts index ed0852338a527..db5dc023ae32a 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts @@ -28,6 +28,8 @@ export interface ClusterResourceProps { readonly secretsEncryptionKey?: kms.IKey; readonly onEventLayer?: lambda.ILayerVersion; readonly clusterHandlerSecurityGroup?: ec2.ISecurityGroup; + readonly tags?: { [key: string]: string }; + readonly logging?: { [key: string]: [ { [key: string]: any } ] }; } /** @@ -89,6 +91,8 @@ export class ClusterResource extends CoreConstruct { endpointPrivateAccess: props.endpointPrivateAccess, publicAccessCidrs: props.publicAccessCidrs, }, + tags: props.tags, + logging: props.logging, }, AssumeRoleArn: this.adminRole.roleArn, diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index 2b917f8f93f7e..66821aed579e9 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -744,13 +744,26 @@ export interface ClusterProps extends ClusterOptions { */ readonly defaultCapacityType?: DefaultCapacityType; - /** * The IAM role to pass to the Kubectl Lambda Handler. * * @default - Default Lambda IAM Execution Role */ readonly kubectlLambdaRole?: iam.IRole; + + /** + * The tags assigned to the EKS cluster + * + * @default - none + */ + readonly tags?: { [key: string]: string }; + + /** + * The cluster log types which you want to enable. + * + * @default - none + */ + readonly clusterLogging?: ClusterLoggingTypes[]; } /** @@ -759,21 +772,25 @@ export interface ClusterProps extends ClusterOptions { export class KubernetesVersion { /** * Kubernetes version 1.14 + * @deprecated Use newer version of EKS */ public static readonly V1_14 = KubernetesVersion.of('1.14'); /** * Kubernetes version 1.15 + * @deprecated Use newer version of EKS */ public static readonly V1_15 = KubernetesVersion.of('1.15'); /** * Kubernetes version 1.16 + * @deprecated Use newer version of EKS */ public static readonly V1_16 = KubernetesVersion.of('1.16'); /** * Kubernetes version 1.17 + * @deprecated Use newer version of EKS */ public static readonly V1_17 = KubernetesVersion.of('1.17'); @@ -809,6 +826,32 @@ export class KubernetesVersion { private constructor(public readonly version: string) { } } +/** + * EKS cluster logging types + */ +export enum ClusterLoggingTypes { + /** + * Logs pertaining to API requests to the cluster. + */ + API = 'api', + /** + * Logs pertaining to cluster access via the Kubernetes API. + */ + AUDIT = 'audit', + /** + * Logs pertaining to authentication requests into the cluster. + */ + AUTHENTICATOR = 'authenticator', + /** + * Logs pertaining to state of cluster controllers. + */ + CONTROLLER_MANAGER = 'controllerManager', + /** + * Logs pertaining to scheduling decisions. + */ + SCHEDULER = 'scheduler', +} + abstract class ClusterBase extends Resource implements ICluster { public abstract readonly connections: ec2.Connections; public abstract readonly vpc: ec2.IVpc; @@ -903,7 +946,7 @@ abstract class ClusterBase extends Resource implements ICluster { if (!this._spotInterruptHandler) { this._spotInterruptHandler = this.addHelmChart('spot-interrupt-handler', { chart: 'aws-node-termination-handler', - version: '0.13.2', + version: '1.14.1', repository: 'https://aws.github.io/eks-charts', namespace: 'kube-system', values: { @@ -1247,6 +1290,8 @@ export class Cluster extends ClusterBase { private readonly version: KubernetesVersion; + private readonly logging?: { [key: string]: [ { [key: string]: any } ] }; + /** * A dummy CloudFormation resource that is used as a wait barrier which * represents that the cluster is ready to receive "kubectl" commands. @@ -1307,6 +1352,14 @@ export class Cluster extends ClusterBase { // Get subnetIds for all selected subnets const subnetIds = Array.from(new Set(flatten(selectedSubnetIdsPerGroup))); + this.logging = props.clusterLogging ? { + clusterLogging: [ + { + enabled: true, + types: Object.values(props.clusterLogging), + }, + ], + } : undefined; this.endpointAccess = props.endpointAccess ?? EndpointAccess.PUBLIC_AND_PRIVATE; this.kubectlEnvironment = props.kubectlEnvironment; @@ -1372,6 +1425,8 @@ export class Cluster extends ClusterBase { subnets: placeClusterHandlerInVpc ? privateSubnets : undefined, clusterHandlerSecurityGroup: this.clusterHandlerSecurityGroup, onEventLayer: this.onEventLayer, + tags: props.tags, + logging: this.logging, }); if (this.endpointAccess._config.privateAccess && privateSubnets.length !== 0) { diff --git a/packages/@aws-cdk/aws-eks/lib/kubectl-handler/helm/__init__.py b/packages/@aws-cdk/aws-eks/lib/kubectl-handler/helm/__init__.py index f83fc204b73e3..7d51e26d7d09b 100644 --- a/packages/@aws-cdk/aws-eks/lib/kubectl-handler/helm/__init__.py +++ b/packages/@aws-cdk/aws-eks/lib/kubectl-handler/helm/__init__.py @@ -1,8 +1,10 @@ import json import logging import os +import re import subprocess import shutil +import tempfile import zipfile from urllib.parse import urlparse, unquote @@ -78,6 +80,12 @@ def helm_handler(event, context): # future work: support versions from s3 assets chart = get_chart_asset_from_url(chart_asset_url) + if repository.startswith('oci://'): + assert(repository is not None) + tmpdir = tempfile.TemporaryDirectory() + chart_dir = get_chart_from_oci(tmpdir.name, release, repository, version) + chart = chart_dir + helm('upgrade', release, chart, repository, values_file, namespace, version, wait, timeout, create_namespace) elif request_type == "Delete": try: @@ -85,6 +93,58 @@ def helm_handler(event, context): except Exception as e: logger.info("delete error: %s" % e) + +def get_oci_cmd(repository, version): + + cmnd = [] + pattern = '\d+.dkr.ecr.[a-z]+-[a-z]+-\d.amazonaws.com' + + registry = repository.rsplit('/', 1)[0].replace('oci://', '') + + if re.fullmatch(pattern, registry) is not None: + region = registry.replace('.amazonaws.com', '').split('.')[-1] + cmnd = [ + f"aws ecr get-login-password --region {region} | " \ + f"helm registry login --username AWS --password-stdin {registry}; helm pull {repository} --version {version} --untar" + ] + else: + logger.info("Non AWS OCI repository found") + cmnd = ['HELM_EXPERIMENTAL_OCI=1', 'helm', 'pull', repository, '--version', version, '--untar'] + + return cmnd + + +def get_chart_from_oci(tmpdir, release, repository = None, version = None): + + cmnd = get_oci_cmd(repository, version) + + maxAttempts = 3 + retry = maxAttempts + while retry > 0: + try: + logger.info(cmnd) + env = get_env_with_oci_flag() + output = subprocess.check_output(cmnd, stderr=subprocess.STDOUT, cwd=tmpdir, env=env, shell=True) + logger.info(output) + + return os.path.join(tmpdir, release) + except subprocess.CalledProcessError as exc: + output = exc.output + if b'Broken pipe' in output: + retry = retry - 1 + logger.info("Broken pipe, retries left: %s" % retry) + else: + raise Exception(output) + raise Exception(f'Operation failed after {maxAttempts} attempts: {output}') + + +def get_env_with_oci_flag(): + env = os.environ.copy() + env['HELM_EXPERIMENTAL_OCI'] = '1' + + return env + + def helm(verb, release, chart = None, repo = None, file = None, namespace = None, version = None, wait = False, timeout = None, create_namespace = None): import subprocess @@ -113,7 +173,8 @@ def helm(verb, release, chart = None, repo = None, file = None, namespace = None retry = maxAttempts while retry > 0: try: - output = subprocess.check_output(cmnd, stderr=subprocess.STDOUT, cwd=outdir) + env = get_env_with_oci_flag() + output = subprocess.check_output(cmnd, stderr=subprocess.STDOUT, cwd=outdir, env=env) logger.info(output) return except subprocess.CalledProcessError as exc: diff --git a/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts b/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts index 161ef77cfe3f3..eef07598abf27 100644 --- a/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts +++ b/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts @@ -168,6 +168,11 @@ export class KubectlProvider extends NestedStack implements IKubectlProvider { resources: [cluster.clusterArn], })); + // For OCI helm chart authorization. + this.handlerRole.addManagedPolicy( + iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerRegistryReadOnly'), + ); + // allow this handler to assume the kubectl role cluster.kubectlRole.grant(this.handlerRole, 'sts:AssumeRole'); diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index c28d366a31447..c20951c1559ac 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -79,19 +79,19 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/aws-lambda": "^8.10.89", - "@types/jest": "^27.0.3", + "@types/aws-lambda": "^8.10.92", + "@types/jest": "^27.4.1", "@types/sinon": "^9.0.11", "@types/yaml": "1.9.6", "aws-sdk": "^2.848.0", - "cdk8s": "^1.3.12", - "cdk8s-plus-21": "^1.0.0-beta.64", - "jest": "^27.4.5", + "cdk8s": "^1.5.24", + "cdk8s-plus-21": "^1.0.0-beta.90", + "jest": "^27.5.1", "sinon": "^9.2.4" }, "dependencies": { @@ -137,7 +137,8 @@ "props-no-arn-refs:@aws-cdk/aws-eks.ClusterProps.outputMastersRoleArn", "props-physical-name:@aws-cdk/aws-eks.OpenIdConnectProviderProps", "resource-attribute:@aws-cdk/aws-eks.Cluster.clusterKubernetesNetworkConfigServiceIpv6Cidr", - "resource-attribute:@aws-cdk/aws-eks.FargateCluster.clusterKubernetesNetworkConfigServiceIpv6Cidr" + "resource-attribute:@aws-cdk/aws-eks.FargateCluster.clusterKubernetesNetworkConfigServiceIpv6Cidr", + "resource-attribute:@aws-cdk/aws-eks.Nodegroup.nodegroupId" ] }, "stability": "stable", diff --git a/packages/@aws-cdk/aws-eks/test/alb-controller.test.ts b/packages/@aws-cdk/aws-eks/test/alb-controller.test.ts index 7a1720a93f844..66d2d3f0ff1fd 100644 --- a/packages/@aws-cdk/aws-eks/test/alb-controller.test.ts +++ b/packages/@aws-cdk/aws-eks/test/alb-controller.test.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; -import '@aws-cdk/assert-internal/jest'; import { Cluster, KubernetesVersion, AlbController, AlbControllerVersion, HelmChart } from '../lib'; import { testFixture } from './util'; @@ -40,7 +40,7 @@ test('can configure a custom repository', () => { repository: 'custom', }); - expect(stack).toHaveResource(HelmChart.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(HelmChart.RESOURCE_TYPE, { Values: { 'Fn::Join': [ '', diff --git a/packages/@aws-cdk/aws-eks/test/awsauth.test.ts b/packages/@aws-cdk/aws-eks/test/awsauth.test.ts index 0f6d3cc4b5891..c141965738084 100644 --- a/packages/@aws-cdk/aws-eks/test/awsauth.test.ts +++ b/packages/@aws-cdk/aws-eks/test/awsauth.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import { Cluster, KubernetesManifest, KubernetesVersion } from '../lib'; @@ -58,7 +58,7 @@ describe('aws auth', () => { new AwsAuth(stack, 'AwsAuth', { cluster }); // THEN - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: JSON.stringify([{ apiVersion: 'v1', kind: 'ConfigMap', @@ -85,8 +85,8 @@ describe('aws auth', () => { cluster.awsAuth.addAccount('5566776655'); // THEN - expect(stack).toCountResources(KubernetesManifest.RESOURCE_TYPE, 1); - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).resourceCountIs(KubernetesManifest.RESOURCE_TYPE, 1); + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -175,7 +175,7 @@ describe('aws auth', () => { cluster.awsAuth.addUserMapping(user, { groups: ['group2'] }); // THEN - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -236,7 +236,7 @@ describe('aws auth', () => { cluster.awsAuth.addMastersRole(role); // THEN - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', diff --git a/packages/@aws-cdk/aws-eks/test/cluster.test.ts b/packages/@aws-cdk/aws-eks/test/cluster.test.ts index e3c32b1e345c2..5e579c4f2a247 100644 --- a/packages/@aws-cdk/aws-eks/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-eks/test/cluster.test.ts @@ -1,7 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import '@aws-cdk/assert-internal/jest'; -import { SynthUtils } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as asg from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; @@ -32,7 +31,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('Custom::AWSCDK-EKS-HelmChart', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-HelmChart', { Chart: 'aws-load-balancer-controller', }); expect(cluster.albController).toBeDefined(); @@ -51,8 +50,9 @@ describe('cluster', () => { const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.ClusterResourceProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); - expect(template.Resources.OnEventHandler42BEBAE0.Properties.Environment).toEqual({ Variables: { foo: 'bar' } }); + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { + Environment: { Variables: { foo: 'bar' } }, + }); }); test('can specify security group to cluster resource handler', () => { @@ -70,8 +70,11 @@ describe('cluster', () => { const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.ClusterResourceProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); - expect(template.Resources.OnEventHandler42BEBAE0.Properties.VpcConfig.SecurityGroupIds).toEqual([{ Ref: 'referencetoStackProxyInstanceSG80B79D87GroupId' }]); + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { + VpcConfig: { + SecurityGroupIds: [{ Ref: 'referencetoStackProxyInstanceSG80B79D87GroupId' }], + }, + }); }); test('throws when trying to place cluster handlers in a vpc with no private subnets', () => { @@ -150,7 +153,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-Cluster', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-Cluster', { Config: { resourcesVpcConfig: { subnetIds: { @@ -162,8 +165,6 @@ describe('cluster', () => { }, }, }); - - }); }); @@ -176,8 +177,6 @@ describe('cluster', () => { }); expect(() => cluster.clusterSecurityGroup).toThrow(/"clusterSecurityGroup" is not defined for this imported cluster/); - - }); test('can place cluster handlers in the cluster vpc', () => { @@ -190,10 +189,11 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.ClusterResourceProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); + const template = Template.fromStack(nested); + const resources = template.findResources('AWS::Lambda::Function'); function assertFunctionPlacedInVpc(id: string) { - expect(template.Resources[id].Properties.VpcConfig.SubnetIds).toEqual([ + expect(resources[id].Properties.VpcConfig.SubnetIds).toEqual([ { Ref: 'referencetoStackClusterDefaultVpcPrivateSubnet1SubnetA64D1BF0Ref' }, { Ref: 'referencetoStackClusterDefaultVpcPrivateSubnet2Subnet32D85AB8Ref' }, ]); @@ -204,8 +204,6 @@ describe('cluster', () => { assertFunctionPlacedInVpc('ProviderframeworkonEvent83C1D0A7'); assertFunctionPlacedInVpc('ProviderframeworkisComplete26D7B0CB'); assertFunctionPlacedInVpc('ProviderframeworkonTimeout0B47CA38'); - - }); test('can access cluster security group for imported cluster with cluster security group id', () => { @@ -241,13 +239,12 @@ describe('cluster', () => { instanceType: new ec2.InstanceType('t2.medium'), }); - const template = SynthUtils.toCloudFormation(stack); - expect(template.Resources.ClusterselfmanagedLaunchConfigA5B57EF6.Properties.SecurityGroups).toEqual([ - { 'Fn::GetAtt': ['ClusterselfmanagedInstanceSecurityGroup64468C3A', 'GroupId'] }, - { 'Fn::GetAtt': ['Cluster9EE0221C', 'ClusterSecurityGroupId'] }, - ]); - - + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { + SecurityGroups: [ + { 'Fn::GetAtt': ['ClusterselfmanagedInstanceSecurityGroup64468C3A', 'GroupId'] }, + { 'Fn::GetAtt': ['Cluster9EE0221C', 'ClusterSecurityGroupId'] }, + ], + }); }); test('security group of self-managed asg is not tagged with owned', () => { @@ -264,11 +261,10 @@ describe('cluster', () => { instanceType: new ec2.InstanceType('t2.medium'), }); - const template = SynthUtils.toCloudFormation(stack); - // make sure the "kubernetes.io/cluster/: owned" tag isn't here. - expect(template.Resources.ClusterselfmanagedInstanceSecurityGroup64468C3A.Properties.Tags).toEqual([ - { Key: 'Name', Value: 'Stack/Cluster/self-managed' }, - ]); + let template = Template.fromStack(stack); + template.hasResourceProperties('AWS::EC2::SecurityGroup', { + Tags: [{ Key: 'Name', Value: 'Stack/Cluster/self-managed' }], + }); }); test('connect autoscaling group with imported cluster', () => { @@ -296,11 +292,12 @@ describe('cluster', () => { // WHEN importedCluster.connectAutoScalingGroupCapacity(selfManaged, {}); - const template = SynthUtils.toCloudFormation(stack); - expect(template.Resources.selfmanagedLaunchConfigD41289EB.Properties.SecurityGroups).toEqual([ - { 'Fn::GetAtt': ['selfmanagedInstanceSecurityGroupEA6D80C9', 'GroupId'] }, - { 'Fn::GetAtt': ['Cluster9EE0221C', 'ClusterSecurityGroupId'] }, - ]); + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { + SecurityGroups: [ + { 'Fn::GetAtt': ['selfmanagedInstanceSecurityGroupEA6D80C9', 'GroupId'] }, + { 'Fn::GetAtt': ['Cluster9EE0221C', 'ClusterSecurityGroupId'] }, + ], + }); }); test('cluster security group is attached when connecting self-managed nodes', () => { @@ -323,13 +320,12 @@ describe('cluster', () => { // WHEN cluster.connectAutoScalingGroupCapacity(selfManaged, {}); - const template = SynthUtils.toCloudFormation(stack); - expect(template.Resources.selfmanagedLaunchConfigD41289EB.Properties.SecurityGroups).toEqual([ - { 'Fn::GetAtt': ['selfmanagedInstanceSecurityGroupEA6D80C9', 'GroupId'] }, - { 'Fn::GetAtt': ['Cluster9EE0221C', 'ClusterSecurityGroupId'] }, - ]); - - + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { + SecurityGroups: [ + { 'Fn::GetAtt': ['selfmanagedInstanceSecurityGroupEA6D80C9', 'GroupId'] }, + { 'Fn::GetAtt': ['Cluster9EE0221C', 'ClusterSecurityGroupId'] }, + ], + }); }); test('spot interrupt handler is not added if spotInterruptHandler is false when connecting self-managed nodes', () => { @@ -370,8 +366,6 @@ describe('cluster', () => { const someConstruct = new constructs.Construct(stack, 'SomeConstruct'); expect(() => cluster.addCdk8sChart('chart', someConstruct)).toThrow(/Invalid cdk8s chart. Must contain a \'toJson\' method, but found undefined/); - - }); test('throws when a core construct is added as cdk8s chart', () => { @@ -387,8 +381,6 @@ describe('cluster', () => { const someConstruct = new cdk.Construct(stack, 'SomeConstruct'); expect(() => cluster.addCdk8sChart('chart', someConstruct)).toThrow(/Invalid cdk8s chart. Must contain a \'toJson\' method, but found undefined/); - - }); test('cdk8s chart can be added to cluster', () => { @@ -417,7 +409,7 @@ describe('cluster', () => { cluster.addCdk8sChart('cdk8s-chart', chart); - expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-KubernetesResource', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesResource', { Manifest: { 'Fn::Join': [ '', @@ -431,8 +423,6 @@ describe('cluster', () => { ], }, }); - - }); test('cluster connections include both control plane and cluster security group', () => { @@ -487,8 +477,6 @@ describe('cluster', () => { // make sure we can synth (no circular dependencies between the stacks) app.synth(); - - }); test('can declare a manifest with a token from a different stack than the cluster that depends on the cluster stack', () => { @@ -706,7 +694,7 @@ describe('cluster', () => { new eks.Cluster(stack, 'Cluster', { vpc, defaultCapacity: 0, version: CLUSTER_VERSION, prune: false }); // THEN - expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-Cluster', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-Cluster', { Config: { roleArn: { 'Fn::GetAtt': ['ClusterRoleFA261979', 'Arn'] }, version: '1.21', @@ -733,7 +721,7 @@ describe('cluster', () => { new eks.Cluster(stack, 'cluster', { version: CLUSTER_VERSION, prune: false }); // THEN - expect(stack).toHaveResource('AWS::EC2::VPC'); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPC', Match.anyValue()); }); @@ -748,7 +736,7 @@ describe('cluster', () => { // THEN expect(cluster.defaultNodegroup).toBeDefined(); - expect(stack).toHaveResource('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { InstanceTypes: [ 'm5.large', ], @@ -775,7 +763,7 @@ describe('cluster', () => { // THEN expect(cluster.defaultNodegroup).toBeDefined(); - expect(stack).toHaveResource('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { ScalingConfig: { DesiredSize: 10, MaxSize: 10, @@ -795,8 +783,8 @@ describe('cluster', () => { // THEN expect(cluster.defaultCapacity).toBeUndefined(); - expect(stack).not.toHaveResource('AWS::AutoScaling::AutoScalingGroup'); - expect(stack).not.toHaveResource('AWS::AutoScaling::LaunchConfiguration'); + Template.fromStack(stack).resourceCountIs('AWS::AutoScaling::AutoScalingGroup', 0); + Template.fromStack(stack).resourceCountIs('AWS::AutoScaling::LaunchConfiguration', 0); }); }); @@ -809,7 +797,7 @@ describe('cluster', () => { new eks.Cluster(stack, 'Cluster', { vpc, defaultCapacity: 0, version: CLUSTER_VERSION, prune: false }); // THEN - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { Tags: [ { Key: 'aws-cdk:subnet-name', Value: 'Private' }, { Key: 'aws-cdk:subnet-type', Value: 'Private' }, @@ -829,7 +817,7 @@ describe('cluster', () => { new eks.Cluster(stack, 'Cluster', { vpc, defaultCapacity: 0, version: CLUSTER_VERSION, prune: false }); // THEN - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { MapPublicIpOnLaunch: true, Tags: [ { Key: 'aws-cdk:subnet-name', Value: 'Public' }, @@ -857,9 +845,9 @@ describe('cluster', () => { instanceType: new ec2.InstanceType('t2.medium'), }); - const template = SynthUtils.toCloudFormation(stack); - expect(template.Resources.ClusterASG0E4BA723.UpdatePolicy).toEqual({ AutoScalingScheduledAction: { IgnoreUnmodifiedGroupSizeProperties: true } }); - + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { + UpdatePolicy: { AutoScalingScheduledAction: { IgnoreUnmodifiedGroupSizeProperties: true } }, + }); }); test('adding capacity creates an ASG with tags', () => { @@ -878,7 +866,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { Tags: [ { Key: { 'Fn::Join': ['', ['kubernetes.io/cluster/', { Ref: 'Cluster9EE0221C' }]] }, @@ -919,7 +907,7 @@ describe('cluster', () => { // THEN expect(cluster.defaultNodegroup).toBeDefined(); - expect(stack).toHaveResource('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { ScalingConfig: { DesiredSize: 10, MaxSize: 10, @@ -946,7 +934,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { Tags: [ { Key: { 'Fn::Join': ['', ['kubernetes.io/cluster/', { Ref: 'Cluster9EE0221C' }]] }, @@ -1000,67 +988,112 @@ describe('cluster', () => { expect(cluster.kubectlProvider).toEqual(kubectlProvider); }); - test('import cluster with existing kubectl provider function should work as expected with resources relying on kubectl getOrCreate', () => { + describe('import cluster with existing kubectl provider function should work as expected with resources relying on kubectl getOrCreate', () => { + test('creates helm chart', () => { + const { stack } = testFixture(); - const { stack } = testFixture(); + const handlerRole = iam.Role.fromRoleArn(stack, 'HandlerRole', 'arn:aws:iam::123456789012:role/lambda-role'); + const kubectlProvider = KubectlProvider.fromKubectlProviderAttributes(stack, 'KubectlProvider', { + functionArn: 'arn:aws:lambda:us-east-2:123456789012:function:my-function:1', + kubectlRoleArn: 'arn:aws:iam::123456789012:role/kubectl-role', + handlerRole: handlerRole, + }); - const handlerRole = iam.Role.fromRoleArn(stack, 'HandlerRole', 'arn:aws:iam::123456789012:role/lambda-role'); - const kubectlProvider = KubectlProvider.fromKubectlProviderAttributes(stack, 'KubectlProvider', { - functionArn: 'arn:aws:lambda:us-east-2:123456789012:function:my-function:1', - kubectlRoleArn: 'arn:aws:iam::123456789012:role/kubectl-role', - handlerRole: handlerRole, - }); + const cluster = eks.Cluster.fromClusterAttributes(stack, 'Cluster', { + clusterName: 'cluster', + kubectlProvider: kubectlProvider, + }); - const cluster = eks.Cluster.fromClusterAttributes(stack, 'Cluster', { - clusterName: 'cluster', - kubectlProvider: kubectlProvider, - }); + new eks.HelmChart(stack, 'Chart', { + cluster: cluster, + chart: 'chart', + }); - new eks.HelmChart(stack, 'Chart', { - cluster: cluster, - chart: 'chart', + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-HelmChart', { + ServiceToken: kubectlProvider.serviceToken, + RoleArn: kubectlProvider.roleArn, + }); }); - expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-HelmChart', { - ServiceToken: kubectlProvider.serviceToken, - RoleArn: kubectlProvider.roleArn, - }); + test('creates Kubernetes patch', () => { + const { stack } = testFixture(); - new eks.KubernetesPatch(stack, 'Patch', { - cluster: cluster, - applyPatch: {}, - restorePatch: {}, - resourceName: 'PatchResource', - }); + const handlerRole = iam.Role.fromRoleArn(stack, 'HandlerRole', 'arn:aws:iam::123456789012:role/lambda-role'); + const kubectlProvider = KubectlProvider.fromKubectlProviderAttributes(stack, 'KubectlProvider', { + functionArn: 'arn:aws:lambda:us-east-2:123456789012:function:my-function:1', + kubectlRoleArn: 'arn:aws:iam::123456789012:role/kubectl-role', + handlerRole: handlerRole, + }); - expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-KubernetesPatch', { - ServiceToken: kubectlProvider.serviceToken, - RoleArn: kubectlProvider.roleArn, - }); + const cluster = eks.Cluster.fromClusterAttributes(stack, 'Cluster', { + clusterName: 'cluster', + kubectlProvider: kubectlProvider, + }); - new eks.KubernetesManifest(stack, 'Manifest', { - cluster: cluster, - manifest: [], - }); + new eks.HelmChart(stack, 'Chart', { + cluster: cluster, + chart: 'chart', + }); - expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-KubernetesResource', { - ServiceToken: kubectlProvider.serviceToken, - RoleArn: kubectlProvider.roleArn, - }); + new eks.KubernetesPatch(stack, 'Patch', { + cluster: cluster, + applyPatch: {}, + restorePatch: {}, + resourceName: 'PatchResource', + }); - new eks.KubernetesObjectValue(stack, 'ObjectValue', { - cluster: cluster, - jsonPath: '', - objectName: 'name', - objectType: 'type', + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesPatch', { + ServiceToken: kubectlProvider.serviceToken, + RoleArn: kubectlProvider.roleArn, + }); }); - expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-KubernetesObjectValue', { - ServiceToken: kubectlProvider.serviceToken, - RoleArn: kubectlProvider.roleArn, - }); + test('creates Kubernetes object value', () => { + const { stack } = testFixture(); + + const handlerRole = iam.Role.fromRoleArn(stack, 'HandlerRole', 'arn:aws:iam::123456789012:role/lambda-role'); + const kubectlProvider = KubectlProvider.fromKubectlProviderAttributes(stack, 'KubectlProvider', { + functionArn: 'arn:aws:lambda:us-east-2:123456789012:function:my-function:1', + kubectlRoleArn: 'arn:aws:iam::123456789012:role/kubectl-role', + handlerRole: handlerRole, + }); + + const cluster = eks.Cluster.fromClusterAttributes(stack, 'Cluster', { + clusterName: 'cluster', + kubectlProvider: kubectlProvider, + }); + + new eks.HelmChart(stack, 'Chart', { + cluster: cluster, + chart: 'chart', + }); + + new eks.KubernetesPatch(stack, 'Patch', { + cluster: cluster, + applyPatch: {}, + restorePatch: {}, + resourceName: 'PatchResource', + }); + + new eks.KubernetesManifest(stack, 'Manifest', { + cluster: cluster, + manifest: [], + }); - expect(cluster.kubectlProvider).not.toBeInstanceOf(eks.KubectlProvider); + new eks.KubernetesObjectValue(stack, 'ObjectValue', { + cluster: cluster, + jsonPath: '', + objectName: 'name', + objectType: 'type', + }); + + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesObjectValue', { + ServiceToken: kubectlProvider.serviceToken, + RoleArn: kubectlProvider.roleArn, + }); + + expect(cluster.kubectlProvider).not.toBeInstanceOf(eks.KubectlProvider); + }); }); test('import cluster with new kubectl private subnets', () => { @@ -1111,7 +1144,7 @@ describe('cluster', () => { new cdk.CfnOutput(stack2, 'ClusterARN', { value: imported.clusterArn }); // THEN - expect(stack2).toMatchTemplate({ + Template.fromStack(stack2).templateMatches({ Outputs: { ClusterARN: { Value: { @@ -1154,7 +1187,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource(eks.KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesManifest.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -1197,11 +1230,11 @@ describe('cluster', () => { cluster.addManifest('manifest2', { bar: 123 }, { boor: [1, 2, 3] }); // THEN - expect(stack).toHaveResource(eks.KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesManifest.RESOURCE_TYPE, { Manifest: '[{"foo":123}]', }); - expect(stack).toHaveResource(eks.KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesManifest.RESOURCE_TYPE, { Manifest: '[{"bar":123},{"boor":[1,2,3]}]', }); @@ -1224,7 +1257,7 @@ describe('cluster', () => { app.synth(); // no cyclic dependency (see https://github.com/aws/aws-cdk/issues/7231) // expect a single resource in the 2nd stack - expect(stack2).toMatchTemplate({ + Template.fromStack(stack2).templateMatches({ Resources: { myresource49C6D325: { Type: 'Custom::AWSCDK-EKS-KubernetesResource', @@ -1261,7 +1294,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource(eks.KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesManifest.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -1313,7 +1346,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource(eks.KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesManifest.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -1531,7 +1564,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Release: 'stackclusterchartspotinterrupthandlerdec62e07', Chart: 'aws-node-termination-handler', Values: '{\"nodeSelector\":{\"lifecycle\":\"Ec2Spot\"}}', @@ -1575,7 +1608,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toCountResources(eks.HelmChart.RESOURCE_TYPE, 1); + Template.fromStack(stack).resourceCountIs(eks.HelmChart.RESOURCE_TYPE, 1); }); @@ -1653,7 +1686,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { AmiType: 'AL2_ARM_64', }); @@ -1674,7 +1707,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { AmiType: 'AL2_ARM_64', }); @@ -1695,7 +1728,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { AmiType: 'AL2_ARM_64', }); @@ -1801,7 +1834,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-Cluster', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-Cluster', { Config: { name: 'my-cluster-name', roleArn: { 'Fn::GetAtt': ['MyClusterRoleBA20FE72', 'Arn'] }, @@ -1823,7 +1856,7 @@ describe('cluster', () => { }); // role can be assumed by 3 lambda handlers (2 for the cluster resource and 1 for the kubernetes resource) - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -1844,7 +1877,7 @@ describe('cluster', () => { }); // policy allows creation role to pass the cluster role and to interact with the cluster (given we know the explicit cluster name) - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -1963,7 +1996,7 @@ describe('cluster', () => { new eks.Cluster(stack, 'MyCluster', { version: CLUSTER_VERSION, prune: false }); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2046,7 +2079,7 @@ describe('cluster', () => { // THEN const providerStack = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - expect(providerStack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(providerStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2074,7 +2107,41 @@ describe('cluster', () => { ], }); - + Template.fromStack(providerStack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { Service: 'lambda.amazonaws.com' }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + ]], + }, + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole', + ]], + }, + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/AmazonEC2ContainerRegistryReadOnly', + ]], + }, + ], + }); }); test('coreDnsComputeType will patch the coreDNS configuration to use a "fargate" compute type and restore to "ec2" upon removal', () => { @@ -2089,7 +2156,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-KubernetesPatch', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesPatch', { ResourceName: 'deployment/coredns', ResourceNamespace: 'kube-system', ApplyPatchJson: '{"spec":{"template":{"metadata":{"annotations":{"eks.amazonaws.com/compute-type":"fargate"}}}}}', @@ -2116,7 +2183,7 @@ describe('cluster', () => { // THEN expect(provider).toEqual(cluster.openIdConnectProvider); - expect(stack).toHaveResource('Custom::AWSCDKOpenIdConnectProvider', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDKOpenIdConnectProvider', { ServiceToken: { 'Fn::GetAtt': [ 'CustomAWSCDKOpenIdConnectProviderCustomResourceProviderHandlerF2C543E0', @@ -2152,7 +2219,7 @@ describe('cluster', () => { const sanitized = YAML.parse(fileContents); // THEN - expect(stack).toHaveResource(eks.KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesManifest.RESOURCE_TYPE, { Manifest: JSON.stringify([sanitized]), }); @@ -2219,7 +2286,7 @@ describe('cluster', () => { // THEN const providerStack = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - expect(providerStack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(providerStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2241,8 +2308,42 @@ describe('cluster', () => { }, }); + Template.fromStack(providerStack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { Service: 'lambda.amazonaws.com' }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + ]], + }, + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole', + ]], + }, + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/AmazonEC2ContainerRegistryReadOnly', + ]], + }, + ], + }); }); - }); test('kubectl provider passes security group to provider', () => { @@ -2260,11 +2361,11 @@ describe('cluster', () => { // the kubectl provider is inside a nested stack. const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); - expect(template.Resources.ProviderframeworkonEvent83C1D0A7.Properties.VpcConfig.SecurityGroupIds).toEqual( - [{ Ref: 'referencetoStackCluster18DFEAC17ClusterSecurityGroupId' }]); - - + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { + VpcConfig: { + SecurityGroupIds: [{ Ref: 'referencetoStackCluster18DFEAC17ClusterSecurityGroupId' }], + }, + }); }); test('kubectl provider passes environment to lambda', () => { @@ -2293,7 +2394,7 @@ describe('cluster', () => { // the kubectl provider is inside a nested stack. const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - expect(nested).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { Environment: { Variables: { Foo: 'Bar', @@ -2332,7 +2433,7 @@ describe('cluster', () => { // the kubectl provider is inside a nested stack. const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - expect(nested).toHaveResourceLike('AWS::Lambda::Function', { + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { Role: { Ref: 'referencetoStackKubectlIamRole02F8947EArn', }, @@ -2362,12 +2463,12 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); // we don't attach vpc config in case endpoint is public only, regardless of whether // the vpc has private subnets or not. - expect(template.Resources.Handler886CB40B.Properties.VpcConfig).toEqual(undefined); - + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { + VpcConfig: Match.absent(), + }); }); @@ -2382,13 +2483,12 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); // we don't attach vpc config in case endpoint is public only, regardless of whether // the vpc has private subnets or not. - expect(template.Resources.Handler886CB40B.Properties.VpcConfig).toEqual(undefined); - - + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { + VpcConfig: Match.absent(), + }); }); test('private without private subnets', () => { @@ -2417,13 +2517,10 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); - - // handler should have vpc config - expect(template.Resources.Handler886CB40B.Properties.VpcConfig.SubnetIds.length).not.toEqual(0); - expect(template.Resources.Handler886CB40B.Properties.VpcConfig.SecurityGroupIds.length).not.toEqual(0); - + const functions = Template.fromStack(nested).findResources('AWS::Lambda::Function'); + expect(functions.Handler886CB40B.Properties.VpcConfig.SubnetIds.length).not.toEqual(0); + expect(functions.Handler886CB40B.Properties.VpcConfig.SecurityGroupIds.length).not.toEqual(0); }); test('private and non restricted public without private subnets', () => { @@ -2437,12 +2534,12 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); // we don't have private subnets, but we don't need them since public access // is not restricted. - expect(template.Resources.Handler886CB40B.Properties.VpcConfig).toEqual(undefined); - + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { + VpcConfig: Match.absent(), + }); }); @@ -2456,13 +2553,11 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); // we have private subnets so we should use them. - expect(template.Resources.Handler886CB40B.Properties.VpcConfig.SubnetIds.length).not.toEqual(0); - expect(template.Resources.Handler886CB40B.Properties.VpcConfig.SecurityGroupIds.length).not.toEqual(0); - - + const functions = Template.fromStack(nested).findResources('AWS::Lambda::Function'); + expect(functions.Handler886CB40B.Properties.VpcConfig.SubnetIds.length).not.toEqual(0); + expect(functions.Handler886CB40B.Properties.VpcConfig.SecurityGroupIds.length).not.toEqual(0); }); test('private and restricted public without private subnets', () => { @@ -2490,12 +2585,11 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); // we have private subnets so we should use them. - expect(template.Resources.Handler886CB40B.Properties.VpcConfig.SubnetIds.length).not.toEqual(0); - expect(template.Resources.Handler886CB40B.Properties.VpcConfig.SecurityGroupIds.length).not.toEqual(0); - + const functions = Template.fromStack(nested).findResources('AWS::Lambda::Function'); + expect(functions.Handler886CB40B.Properties.VpcConfig.SubnetIds.length).not.toEqual(0); + expect(functions.Handler886CB40B.Properties.VpcConfig.SecurityGroupIds.length).not.toEqual(0); }); @@ -2552,13 +2646,9 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); - - expect(template.Resources.Handler886CB40B.Properties.VpcConfig.SubnetIds).toEqual([ - 'subnet-private-in-us-east-1a', - ]); - - + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { + VpcConfig: { SubnetIds: ['subnet-private-in-us-east-1a'] }, + }); }); test('private endpoint access selects only private subnets from looked up vpc with concrete subnet selection', () => { @@ -2620,13 +2710,9 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); - - expect(template.Resources.Handler886CB40B.Properties.VpcConfig.SubnetIds).toEqual([ - 'subnet-private-in-us-east-1a', - ]); - - + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { + VpcConfig: { SubnetIds: ['subnet-private-in-us-east-1a'] }, + }); }); test('private endpoint access selects only private subnets from managed vpc with concrete subnet selection', () => { @@ -2650,14 +2736,14 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); - - expect(template.Resources.Handler886CB40B.Properties.VpcConfig.SubnetIds).toEqual([ - { Ref: 'referencetoStackVpcPrivateSubnet1Subnet8E6A14CBRef' }, - 'subnet-unknown', - ]); - - + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { + VpcConfig: { + SubnetIds: [ + { Ref: 'referencetoStackVpcPrivateSubnet1Subnet8E6A14CBRef' }, + 'subnet-unknown', + ], + }, + }); }); test('private endpoint access considers specific subnet selection', () => { @@ -2676,13 +2762,9 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); - - expect(template.Resources.Handler886CB40B.Properties.VpcConfig.SubnetIds).toEqual([ - 'subnet1', - ]); - - + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { + VpcConfig: { SubnetIds: ['subnet1'] }, + }); }); test('can configure private endpoint access', () => { @@ -2737,7 +2819,7 @@ describe('cluster', () => { // the kubectl provider is inside a nested stack. const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - expect(nested).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { VpcConfig: { SecurityGroupIds: [ { @@ -2803,10 +2885,8 @@ describe('cluster', () => { // the kubectl provider is inside a nested stack. const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); - expect(16).toEqual(template.Resources.Handler886CB40B.Properties.VpcConfig.SubnetIds.length); - - + const functions = Template.fromStack(nested).findResources('AWS::Lambda::Function'); + expect(functions.Handler886CB40B.Properties.VpcConfig.SubnetIds.length).toEqual(16); }); test('kubectl provider considers vpc subnet selection', () => { @@ -2855,7 +2935,7 @@ describe('cluster', () => { // the kubectl provider is inside a nested stack. const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - expect(nested).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { VpcConfig: { SecurityGroupIds: [ { @@ -2937,10 +3017,11 @@ describe('cluster', () => { const expectedKubernetesGetId = 'Cluster1myserviceLoadBalancerAddress198CCB03'; - const template = SynthUtils.toCloudFormation(stack); + let template = Template.fromStack(stack); + const resources = template.findResources('Custom::AWSCDK-EKS-KubernetesObjectValue'); // make sure the custom resource is created correctly - expect(template.Resources[expectedKubernetesGetId].Properties).toEqual({ + expect(resources[expectedKubernetesGetId].Properties).toEqual({ ServiceToken: { 'Fn::GetAtt': [ 'awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B', @@ -2964,8 +3045,9 @@ describe('cluster', () => { }); // make sure the attribute points to the expected custom resource and extracts the correct attribute - expect(template.Outputs.LoadBalancerAddress.Value).toEqual({ 'Fn::GetAtt': [expectedKubernetesGetId, 'Value'] }); - + template.hasOutput('LoadBalancerAddress', { + Value: { 'Fn::GetAtt': [expectedKubernetesGetId, 'Value'] }, + }); }); test('custom kubectl layer can be provided', () => { @@ -2982,7 +3064,7 @@ describe('cluster', () => { // THEN const providerStack = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - expect(providerStack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(providerStack).hasResourceProperties('AWS::Lambda::Function', { Layers: ['arn:of:layer'], }); @@ -3002,7 +3084,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-Cluster', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-Cluster', { Config: { encryptionConfig: [{ provider: { @@ -3070,7 +3152,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-Cluster', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-Cluster', { Config: { kubernetesNetworkConfig: { serviceIpv4Cidr: customCidr, diff --git a/packages/@aws-cdk/aws-eks/test/fargate.test.ts b/packages/@aws-cdk/aws-eks/test/fargate.test.ts index 776da61d1561a..ddd195c7574d4 100644 --- a/packages/@aws-cdk/aws-eks/test/fargate.test.ts +++ b/packages/@aws-cdk/aws-eks/test/fargate.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; @@ -21,7 +20,7 @@ describe('fargate', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-FargateProfile', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-FargateProfile', { Config: { clusterName: { Ref: 'MyCluster8AD82BF8' }, podExecutionRoleArn: { 'Fn::GetAtt': ['MyClusterfargateprofileMyProfilePodExecutionRole4795C054', 'Arn'] }, @@ -43,7 +42,7 @@ describe('fargate', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-FargateProfile', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-FargateProfile', { Config: { clusterName: { Ref: 'MyCluster8AD82BF8' }, podExecutionRoleArn: { 'Fn::GetAtt': ['MyClusterfargateprofileMyProfilePodExecutionRole4795C054', 'Arn'] }, @@ -67,7 +66,7 @@ describe('fargate', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-FargateProfile', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-FargateProfile', { Config: { clusterName: { Ref: 'MyCluster8AD82BF8' }, podExecutionRoleArn: { 'Fn::GetAtt': ['MyRoleF48FFE04', 'Arn'] }, @@ -91,7 +90,7 @@ describe('fargate', () => { Tags.of(cluster).add('propTag', '123'); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-FargateProfile', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-FargateProfile', { Config: { selectors: [{ namespace: 'default' }], clusterName: { Ref: 'MyCluster8AD82BF8' }, @@ -122,7 +121,7 @@ describe('fargate', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-FargateProfile', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-FargateProfile', { Config: { clusterName: { Ref: 'MyCluster8AD82BF8' }, podExecutionRoleArn: { 'Fn::GetAtt': ['MyClusterfargateprofileMyProfilePodExecutionRole4795C054', 'Arn'] }, @@ -161,7 +160,7 @@ describe('fargate', () => { new eks.FargateCluster(stack, 'FargateCluster', { version: CLUSTER_VERSION }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-KubernetesPatch', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesPatch', { ResourceName: 'deployment/coredns', ResourceNamespace: 'kube-system', ApplyPatchJson: '{"spec":{"template":{"metadata":{"annotations":{"eks.amazonaws.com/compute-type":"fargate"}}}}}', @@ -171,7 +170,7 @@ describe('fargate', () => { }, }); - expect(stack).toHaveResource('Custom::AWSCDK-EKS-FargateProfile', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-FargateProfile', { Config: { clusterName: { Ref: 'FargateCluster019F03E8', @@ -204,7 +203,7 @@ describe('fargate', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-FargateProfile', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-FargateProfile', { Config: { clusterName: { Ref: 'FargateCluster019F03E8', @@ -238,7 +237,7 @@ describe('fargate', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-FargateProfile', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-FargateProfile', { Config: { clusterName: { Ref: 'FargateCluster019F03E8', @@ -272,14 +271,14 @@ describe('fargate', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-FargateProfile', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-FargateProfile', { Config: { clusterName: { Ref: 'MyCluster8AD82BF8' }, podExecutionRoleArn: { 'Fn::GetAtt': ['MyClusterfargateprofileMyProfile1PodExecutionRole794E9E37', 'Arn'] }, selectors: [{ namespace: 'namespace1' }], }, }); - expect(stack).toHaveResource('Custom::AWSCDK-EKS-FargateProfile', { + Template.fromStack(stack).hasResource('Custom::AWSCDK-EKS-FargateProfile', { Properties: { ServiceToken: { 'Fn::GetAtt': [ @@ -298,7 +297,7 @@ describe('fargate', () => { 'MyClusterfargateprofileMyProfile1PodExecutionRole794E9E37', 'MyClusterfargateprofileMyProfile1879D501A', ], - }, ResourcePart.CompleteDefinition); + }); }); @@ -311,7 +310,7 @@ describe('fargate', () => { new eks.FargateCluster(stack, 'FargateCluster', { version: CLUSTER_VERSION }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-KubernetesResource', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesResource', { Manifest: { 'Fn::Join': [ '', @@ -353,7 +352,7 @@ describe('fargate', () => { new eks.FargateCluster(stack, 'FargateCluster', { version: CLUSTER_VERSION }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -442,7 +441,7 @@ describe('fargate', () => { }); // THEN - expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-Cluster', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-Cluster', { Config: { encryptionConfig: [{ provider: { diff --git a/packages/@aws-cdk/aws-eks/test/helm-chart.test.ts b/packages/@aws-cdk/aws-eks/test/helm-chart.test.ts index cfc7961f5e5e5..5f0c7536a0872 100644 --- a/packages/@aws-cdk/aws-eks/test/helm-chart.test.ts +++ b/packages/@aws-cdk/aws-eks/test/helm-chart.test.ts @@ -1,5 +1,5 @@ -import '@aws-cdk/assert-internal/jest'; import * as path from 'path'; +import { Template } from '@aws-cdk/assertions'; import { Asset } from '@aws-cdk/aws-s3-assets'; import { Duration } from '@aws-cdk/core'; import * as eks from '../lib'; @@ -17,7 +17,7 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Namespace: 'default' }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Namespace: 'default' }); }); test('should have a lowercase default release name', () => { // GIVEN @@ -27,7 +27,7 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Release: 'stackmychartff398361', }); }); @@ -92,7 +92,7 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyChart', { cluster, chartAsset }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { ChartAssetURL: { 'Fn::Join': [ '', @@ -144,7 +144,7 @@ describe('helm chart', () => { }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Release: 'hismostprobablylongerthanfiftythreecharacterscaf15d09', }); }); @@ -156,7 +156,7 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart', values: { foo: 123 } }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Values: '{"foo":123}' }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Values: '{"foo":123}' }); }); test('should support create namespaces by default', () => { // GIVEN @@ -166,7 +166,7 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { CreateNamespace: true }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { CreateNamespace: true }); }); test('should support create namespaces when explicitly specified', () => { // GIVEN @@ -176,7 +176,7 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart', createNamespace: true }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { CreateNamespace: true }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { CreateNamespace: true }); }); test('should not create namespaces when disabled', () => { // GIVEN @@ -186,7 +186,8 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart', createNamespace: false }); // THEN - expect(stack).not.toHaveResource(eks.HelmChart.RESOURCE_TYPE, { CreateNamespace: true }); + const charts = Template.fromStack(stack).findResources(eks.HelmChart.RESOURCE_TYPE, { CreateNamespace: true }); + expect(Object.keys(charts).length).toEqual(0); }); test('should support waiting until everything is completed before marking release as successful', () => { // GIVEN @@ -196,7 +197,7 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyWaitingChart', { cluster, chart: 'chart', wait: true }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Wait: true }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Wait: true }); }); test('should default to not waiting before marking release as successful', () => { // GIVEN @@ -206,7 +207,9 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyWaitingChart', { cluster, chart: 'chart' }); // THEN - expect(stack).not.toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Wait: true }); + const charts = Template.fromStack(stack).findResources(eks.HelmChart.RESOURCE_TYPE, { Wait: true }); + expect(Object.keys(charts).length).toEqual(0); + }); test('should enable waiting when specified', () => { // GIVEN @@ -216,7 +219,7 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyWaitingChart', { cluster, chart: 'chart', wait: true }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Wait: true }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Wait: true }); }); test('should disable waiting when specified as false', () => { // GIVEN @@ -226,7 +229,8 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyWaitingChart', { cluster, chart: 'chart', wait: false }); // THEN - expect(stack).not.toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Wait: true }); + const charts = Template.fromStack(stack).findResources(eks.HelmChart.RESOURCE_TYPE, { Wait: true }); + expect(Object.keys(charts).length).toEqual(0); }); test('should timeout only after 10 minutes', () => { @@ -241,7 +245,7 @@ describe('helm chart', () => { }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Timeout: '600s' }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Timeout: '600s' }); }); }); }); diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json index 00ab6f9f6fe3c..b38b7937618fc 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json @@ -951,6 +951,17 @@ ], "endpointPublicAccess": true, "endpointPrivateAccess": true + }, + "tags": { + "foo": "bar" + }, + "logging": { + "clusterLogging": [ + { + "enabled": true, + "types": [ "api", "authenticator", "scheduler" ] + } + ] } }, "AssumeRoleArn": { @@ -2701,7 +2712,7 @@ }, "Release": "ksclustertestclusterchartspotinterrupthandlerf41ba997", "Chart": "aws-node-termination-handler", - "Version": "0.13.2", + "Version": "1.14.1", "Values": "{\"nodeSelector\":{\"lifecycle\":\"Ec2Spot\"}}", "Namespace": "kube-system", "Repository": "https://aws.github.io/eks-charts", diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts index 5cbc0e384eb17..1ae9a97e5bb37 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts @@ -38,6 +38,14 @@ class EksClusterStack extends TestStack { defaultCapacity: 2, version: eks.KubernetesVersion.V1_21, secretsEncryptionKey, + tags: { + foo: 'bar', + }, + clusterLogging: [ + eks.ClusterLoggingTypes.API, + eks.ClusterLoggingTypes.AUTHENTICATOR, + eks.ClusterLoggingTypes.SCHEDULER, + ], }); this.assertFargateProfile(); diff --git a/packages/@aws-cdk/aws-eks/test/k8s-manifest.test.ts b/packages/@aws-cdk/aws-eks/test/k8s-manifest.test.ts index 9b0295c23efff..34b2a47cc6d14 100644 --- a/packages/@aws-cdk/aws-eks/test/k8s-manifest.test.ts +++ b/packages/@aws-cdk/aws-eks/test/k8s-manifest.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { SynthUtils } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import { CfnResource, Stack } from '@aws-cdk/core'; import { Cluster, KubernetesManifest, KubernetesVersion, HelmChart } from '../lib'; import { testFixtureNoVpc, testFixtureCluster } from './util'; @@ -72,7 +71,7 @@ describe('k8s manifest', () => { manifest, }); - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: JSON.stringify(manifest), }); @@ -91,13 +90,13 @@ describe('k8s manifest', () => { cluster.addHelmChart('helm', { chart: 'hello-world' }); // THEN - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: '[{"bar":2334}]', ClusterName: 'my-cluster-name', RoleArn: 'arn:aws:iam::1111111:role/iam-role-that-has-masters-access', }); - expect(stack).toHaveResource(HelmChart.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(HelmChart.RESOURCE_TYPE, { ClusterName: 'my-cluster-name', RoleArn: 'arn:aws:iam::1111111:role/iam-role-that-has-masters-access', Release: 'myclustercharthelm78d2c26a', @@ -140,7 +139,7 @@ describe('k8s manifest', () => { }); // THEN - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: JSON.stringify([{ apiVersion: 'v1beta1', kind: 'Foo', @@ -182,7 +181,7 @@ describe('k8s manifest', () => { ); // THEN - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: JSON.stringify([ { apiVersion: 'v1beta', @@ -244,7 +243,7 @@ describe('k8s manifest', () => { }); // THEN - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: JSON.stringify([ { apiVersion: 'v1beta', @@ -259,7 +258,7 @@ describe('k8s manifest', () => { PruneLabel: 'aws.cdk.eks/prune-c89a5983505f58231ac2a9a86fd82735ccf2308eac', }); - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: JSON.stringify([ { apiVersion: 'v1', @@ -297,7 +296,7 @@ describe('k8s manifest', () => { }); // THEN - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: JSON.stringify([{ malformed: { resource: 'yes' } }]), PruneLabel: 'aws.cdk.eks/prune-c89a5983505f58231ac2a9a86fd82735ccf2308eac', }); @@ -314,7 +313,7 @@ describe('k8s manifest', () => { cluster.addManifest('m1', ['foo']); // THEN - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: JSON.stringify([['foo']]), PruneLabel: 'aws.cdk.eks/prune-c89a5983505f58231ac2a9a86fd82735ccf2308eac', }); @@ -347,7 +346,7 @@ describe('k8s manifest', () => { }); // THEN - const template = SynthUtils.synthesize(stack).template; + const template = Template.fromStack(stack).toJSON(); const m1 = template.Resources.Clustermanifestm1E5FBE3C1.Properties; const m2 = template.Resources.m201F909C5.Properties; diff --git a/packages/@aws-cdk/aws-eks/test/k8s-object-value.test.ts b/packages/@aws-cdk/aws-eks/test/k8s-object-value.test.ts index 8bc87ba294763..8d29fa19d6471 100644 --- a/packages/@aws-cdk/aws-eks/test/k8s-object-value.test.ts +++ b/packages/@aws-cdk/aws-eks/test/k8s-object-value.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import { App, Stack, Duration } from '@aws-cdk/core'; import * as eks from '../lib'; import { KubernetesObjectValue } from '../lib/k8s-object-value'; diff --git a/packages/@aws-cdk/aws-eks/test/k8s-patch.test.ts b/packages/@aws-cdk/aws-eks/test/k8s-patch.test.ts index c59851500eff0..4040a070c15a0 100644 --- a/packages/@aws-cdk/aws-eks/test/k8s-patch.test.ts +++ b/packages/@aws-cdk/aws-eks/test/k8s-patch.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Names, Stack } from '@aws-cdk/core'; import * as eks from '../lib'; import { KubernetesPatch, PatchType } from '../lib/k8s-patch'; @@ -20,7 +20,7 @@ describe('k8s patch', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-KubernetesPatch', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesPatch', { ServiceToken: { 'Fn::GetAtt': [ 'awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B', @@ -59,7 +59,7 @@ describe('k8s patch', () => { restorePatch: { restore: { patch: 123 } }, resourceName: 'myResourceName', }); - expect(stack).toHaveResource('Custom::AWSCDK-EKS-KubernetesPatch', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesPatch', { PatchType: 'strategic', }); @@ -92,15 +92,15 @@ describe('k8s patch', () => { patchType: PatchType.STRATEGIC, }); - expect(stack).toHaveResource('Custom::AWSCDK-EKS-KubernetesPatch', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesPatch', { ResourceName: 'jsonPatchResource', PatchType: 'json', }); - expect(stack).toHaveResource('Custom::AWSCDK-EKS-KubernetesPatch', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesPatch', { ResourceName: 'mergePatchResource', PatchType: 'merge', }); - expect(stack).toHaveResource('Custom::AWSCDK-EKS-KubernetesPatch', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesPatch', { ResourceName: 'strategicPatchResource', PatchType: 'strategic', }); diff --git a/packages/@aws-cdk/aws-eks/test/nodegroup.test.ts b/packages/@aws-cdk/aws-eks/test/nodegroup.test.ts index 717b091acf2aa..dd8d0aa1274cd 100644 --- a/packages/@aws-cdk/aws-eks/test/nodegroup.test.ts +++ b/packages/@aws-cdk/aws-eks/test/nodegroup.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import * as cdk from '@aws-cdk/core'; @@ -95,7 +95,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { AmiType: 'AL2_x86_64_GPU', }); @@ -114,7 +114,7 @@ describe('node group', () => { new eks.Nodegroup(stack, 'Nodegroup', { cluster }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { ClusterName: { Ref: 'Cluster9EE0221C', }, @@ -159,7 +159,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { ClusterName: { Ref: 'Cluster9EE0221C', }, @@ -204,7 +204,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { ClusterName: { Ref: 'Cluster9EE0221C', }, @@ -254,7 +254,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { AmiType: 'AL2_x86_64', }); }); @@ -281,7 +281,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { AmiType: 'AL2_ARM_64', }); }); @@ -309,7 +309,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { AmiType: 'AL2_x86_64_GPU', }); }); @@ -463,9 +463,9 @@ describe('node group', () => { }); /** - * BOTTOEROCKET_X86_64 with defined instance types w/o launchTemplateSpec should deploy correctly. + * BOTTLEROCKET_X86_64 with defined instance types w/o launchTemplateSpec should deploy correctly. */ - test('BOTTOEROCKET_X86_64 with defined instance types w/o launchTemplateSpec should deploy correctly', () => { + test('BOTTLEROCKET_X86_64 with defined instance types w/o launchTemplateSpec should deploy correctly', () => { // GIVEN const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { @@ -480,15 +480,15 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { AmiType: 'BOTTLEROCKET_x86_64', }); }); /** - * BOTTOEROCKET_ARM_64 with defined instance types w/o launchTemplateSpec should deploy correctly. + * BOTTLEROCKET_ARM_64 with defined instance types w/o launchTemplateSpec should deploy correctly. */ - test('BOTTOEROCKET_ARM_64 with defined instance types w/o launchTemplateSpec should deploy correctly', () => { + test('BOTTLEROCKET_ARM_64 with defined instance types w/o launchTemplateSpec should deploy correctly', () => { // GIVEN const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { @@ -503,7 +503,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { AmiType: 'BOTTLEROCKET_ARM_64', }); }); @@ -521,7 +521,7 @@ describe('node group', () => { new eks.Nodegroup(stack, 'Nodegroup', { cluster }); // THEN - expect(stack).toHaveResource(eks.KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesManifest.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -582,7 +582,7 @@ describe('node group', () => { }, }); // THEN - expect(stack).toHaveResource('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { RemoteAccess: { Ec2SshKey: 'foo', SourceSecurityGroups: [ @@ -611,7 +611,7 @@ describe('node group', () => { new eks.Nodegroup(stack, 'Nodegroup', { cluster, forceUpdate: false }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { ForceUpdateEnabled: false, }); @@ -633,7 +633,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { InstanceTypes: [ 'm5.large', ], @@ -658,7 +658,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { InstanceTypes: [ 'm5.large', ], @@ -687,7 +687,7 @@ describe('node group', () => { capacityType: eks.CapacityType.SPOT, }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { InstanceTypes: [ 'm5.large', 't3.large', @@ -718,7 +718,7 @@ describe('node group', () => { capacityType: eks.CapacityType.ON_DEMAND, }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { InstanceTypes: [ 'm5.large', 't3.large', @@ -765,7 +765,7 @@ describe('node group', () => { capacityType: eks.CapacityType.SPOT, }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { CapacityType: 'SPOT', }); @@ -830,7 +830,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { RemoteAccess: { Ec2SshKey: 'foo', }, @@ -856,7 +856,7 @@ describe('node group', () => { new cdk.CfnOutput(stack2, 'NodegroupName', { value: imported.nodegroupName }); // THEN - expect(stack2).toMatchTemplate({ + Template.fromStack(stack2).templateMatches({ Outputs: { NodegroupName: { Value: { @@ -880,7 +880,7 @@ describe('node group', () => { cluster.addNodegroupCapacity('ng'); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { ClusterName: { Ref: 'Cluster9EE0221C', }, @@ -929,7 +929,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { ClusterName: { Ref: 'Cluster9EE0221C', }, @@ -984,7 +984,7 @@ describe('node group', () => { desiredSize: 4, }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { ScalingConfig: { MinSize: 2, MaxSize: 6, @@ -1010,7 +1010,7 @@ describe('node group', () => { desiredSize: cdk.Lazy.number({ produce: () => 20 }), }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { ScalingConfig: { MinSize: 5, MaxSize: 1, @@ -1050,7 +1050,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { LaunchTemplate: { Id: { Ref: 'LaunchTemplate', diff --git a/packages/@aws-cdk/aws-eks/test/service-account.test.ts b/packages/@aws-cdk/aws-eks/test/service-account.test.ts index e457598ccaa4f..e4db2f6680a97 100644 --- a/packages/@aws-cdk/aws-eks/test/service-account.test.ts +++ b/packages/@aws-cdk/aws-eks/test/service-account.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as eks from '../lib'; import { testFixture, testFixtureCluster } from './util'; @@ -15,7 +15,7 @@ describe('service account', () => { new eks.ServiceAccount(stack, 'MyServiceAccount', { cluster }); // THEN - expect(stack).toHaveResource(eks.KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesManifest.RESOURCE_TYPE, { ServiceToken: { 'Fn::GetAtt': [ 'awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B', @@ -38,7 +38,7 @@ describe('service account', () => { ], }, }); - expect(stack).toHaveResource(iam.CfnRole.CFN_RESOURCE_TYPE_NAME, { + Template.fromStack(stack).hasResourceProperties(iam.CfnRole.CFN_RESOURCE_TYPE_NAME, { AssumeRolePolicyDocument: { Statement: [ { @@ -73,7 +73,7 @@ describe('service account', () => { cluster.addServiceAccount('MyOtherServiceAccount'); // THEN - expect(stack).toHaveResource(eks.KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesManifest.RESOURCE_TYPE, { ServiceToken: { 'Fn::GetAtt': [ 'awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B', @@ -122,7 +122,7 @@ describe('service account', () => { cluster.addServiceAccount('MyServiceAccount'); - expect(stack).toHaveResource(eks.KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesManifest.RESOURCE_TYPE, { ServiceToken: { 'Fn::GetAtt': [ 'StackClusterF0EB02FAKubectlProviderNestedStackStackClusterF0EB02FAKubectlProviderNestedStackResource739D12C4', @@ -147,7 +147,7 @@ describe('service account', () => { }, }); - expect(stack).toHaveResource(iam.CfnRole.CFN_RESOURCE_TYPE_NAME, { + Template.fromStack(stack).hasResourceProperties(iam.CfnRole.CFN_RESOURCE_TYPE_NAME, { AssumeRolePolicyDocument: { Statement: [ { diff --git a/packages/@aws-cdk/aws-elasticache/package.json b/packages/@aws-cdk/aws-elasticache/package.json index e6ed98edbb9a3..50e6f1e715664 100644 --- a/packages/@aws-cdk/aws-elasticache/package.json +++ b/packages/@aws-cdk/aws-elasticache/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -76,7 +83,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-elasticbeanstalk/package.json b/packages/@aws-cdk/aws-elasticbeanstalk/package.json index 5752d05303360..c3813016c4e6a 100644 --- a/packages/@aws-cdk/aws-elasticbeanstalk/package.json +++ b/packages/@aws-cdk/aws-elasticbeanstalk/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -76,7 +83,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/README.md b/packages/@aws-cdk/aws-elasticloadbalancing/README.md index 6d66ca5965c69..45ec1828bb466 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/README.md +++ b/packages/@aws-cdk/aws-elasticloadbalancing/README.md @@ -21,17 +21,19 @@ balancer, set up listeners and a health check, and supply the fleet(s) you want to load balance to in the `targets` property. ```ts +declare const vpc: ec2.IVpc; const lb = new elb.LoadBalancer(this, 'LB', { - vpc, - internetFacing: true, - healthCheck: { - port: 80 - }, + vpc, + internetFacing: true, + healthCheck: { + port: 80, + }, }); +declare const myAutoScalingGroup: autoscaling.AutoScalingGroup; lb.addTarget(myAutoScalingGroup); lb.addListener({ - externalPort: 80, + externalPort: 80, }); ``` @@ -39,8 +41,10 @@ The load balancer allows all connections by default. If you want to change that, pass the `allowConnectionsFrom` property while setting up the listener: ```ts +declare const mySecurityGroup: ec2.SecurityGroup; +declare const lb: elb.LoadBalancer; lb.addListener({ - externalPort: 80, - allowConnectionsFrom: [mySecurityGroup] + externalPort: 80, + allowConnectionsFrom: [mySecurityGroup], }); ``` diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/package.json b/packages/@aws-cdk/aws-elasticloadbalancing/package.json index fb6b19aeb74dc..be344c056d66e 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancing/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -72,12 +79,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-ec2": "0.0.0", diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-elasticloadbalancing/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..036e5ddf38c1b --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancing/rosetta/default.ts-fixture @@ -0,0 +1,13 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as elb from '@aws-cdk/aws-elasticloadbalancing'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as autoscaling from '@aws-cdk/aws-autoscaling'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/test/loadbalancer.test.ts b/packages/@aws-cdk/aws-elasticloadbalancing/test/loadbalancer.test.ts index 457742ff5db70..5497f380c5f76 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/test/loadbalancer.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancing/test/loadbalancer.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Connections, Peer, SubnetType, Vpc } from '@aws-cdk/aws-ec2'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import { Duration, Stack } from '@aws-cdk/core'; @@ -19,7 +19,7 @@ describe('tests', () => { internalPort: 8080, }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancing::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancing::LoadBalancer', { Listeners: [{ InstancePort: '8080', InstanceProtocol: 'http', @@ -46,7 +46,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancing::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancing::LoadBalancer', { HealthCheck: { HealthyThreshold: '2', Interval: '60', @@ -76,7 +76,7 @@ describe('tests', () => { elb.addTarget(new FakeTarget()); // THEN: at the very least it added a security group rule for the backend - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { SecurityGroupEgress: [ { Description: 'Port 8080 LB to fleet', @@ -101,7 +101,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancing::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancing::LoadBalancer', { CrossZone: true, }); }); @@ -118,7 +118,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancing::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancing::LoadBalancer', { CrossZone: false, }); }); @@ -134,7 +134,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancing::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancing::LoadBalancer', { CrossZone: true, }); }); @@ -171,7 +171,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancing::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancing::LoadBalancer', { Subnets: vpc.selectSubnets({ subnetGroupName: 'private1', }).subnetIds.map((subnetId: string) => stack.resolve(subnetId)), @@ -194,7 +194,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancing::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancing::LoadBalancer', { Listeners: [{ InstancePort: '8080', InstanceProtocol: 'http', @@ -221,7 +221,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancing::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancing::LoadBalancer', { Listeners: [{ InstancePort: '8080', InstanceProtocol: 'http', @@ -266,7 +266,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancing::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancing::LoadBalancer', { AccessLoggingPolicy: { Enabled: true, S3BucketName: 'fakeBucket', @@ -289,7 +289,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancing::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancing::LoadBalancer', { AccessLoggingPolicy: { Enabled: false, S3BucketName: 'fakeBucket', diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json index a232e131684f7..bc600f17e2974 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -65,12 +72,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-cognito": "0.0.0", diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/cognito.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/cognito.test.ts index dad0adda17691..e747c60980660 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/cognito.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/cognito.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as cognito from '@aws-cdk/aws-cognito'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; @@ -35,7 +35,7 @@ test('Cognito Action', () => { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { AuthenticateCognitoConfig: { @@ -56,5 +56,5 @@ test('Cognito Action', () => { Type: 'fixed-response', }, ], - })); + }); }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json index 9b1d6611c9678..b00b793851348 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -65,12 +72,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5", + "@types/jest": "^27.4.1", + "jest": "^27.5.1", "@aws-cdk/aws-ecs": "0.0.0", "@aws-cdk/aws-ecs-patterns": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/alb-target.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/alb-target.test.ts index 1ccea0b91d1b2..b8c30af1f332a 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/alb-target.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/alb-target.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import { Stack } from '@aws-cdk/core'; @@ -19,7 +19,7 @@ test('Can create target groups with alb target', () => { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 80, Protocol: 'TCP', Targets: [ @@ -34,7 +34,7 @@ test('Can create target groups with alb target', () => { VpcId: { Ref: 'Stack8A423254', }, - })); + }); }); test('Can create target groups with alb arn target', () => { @@ -51,7 +51,7 @@ test('Can create target groups with alb arn target', () => { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 80, Protocol: 'TCP', Targets: [ @@ -64,5 +64,5 @@ test('Can create target groups with alb arn target', () => { VpcId: { Ref: 'Stack8A423254', }, - })); + }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/instance-target.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/instance-target.test.ts index 874adeffe2c36..55a731ac51dab 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/instance-target.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/instance-target.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import { Stack } from '@aws-cdk/core'; @@ -18,14 +18,14 @@ test('Can create target groups with instance id target', () => { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 80, Protocol: 'HTTP', Targets: [ { Id: 'i-1234' }, ], TargetType: 'instance', - })); + }); }); test('Can create target groups with instance target', () => { @@ -48,12 +48,12 @@ test('Can create target groups with instance target', () => { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 80, Protocol: 'HTTP', Targets: [ { Id: { Ref: 'InstanceC1063A87' } }, ], TargetType: 'instance', - })); + }); }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/ip-target.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/ip-target.test.ts index a1ab3b9f2896e..185ad0c877574 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/ip-target.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/ip-target.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import { Stack } from '@aws-cdk/core'; @@ -18,12 +18,12 @@ test('Can create target groups with lambda targets', () => { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 80, Protocol: 'HTTP', Targets: [ { Id: '1.2.3.4' }, ], TargetType: 'ip', - })); + }); }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/lambda-target.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/lambda-target.test.ts index b22584404fd09..2e757a0028aef 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/lambda-target.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/lambda-target.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, ResourcePart } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as lambda from '@aws-cdk/aws-lambda'; @@ -29,12 +29,12 @@ test('Can create target groups with lambda targets', () => { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { TargetType: 'lambda', Targets: [ { Id: { 'Fn::GetAtt': ['FunA2CCED21', 'Arn'] } }, ], - })); + }); }); test('Lambda targets create dependency on Invoke permission', () => { @@ -44,7 +44,7 @@ test('Lambda targets create dependency on Invoke permission', () => { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', (def: any) => { + Template.fromStack(stack).hasResource('AWS::ElasticLoadBalancingV2::TargetGroup', (def: any) => { return (def.DependsOn ?? []).includes('FunInvokeServicePrincipalelasticloadbalancingamazonawscomD2CAC0C4'); - }, ResourcePart.CompleteDefinition)); + }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index 2994a437b3929..79caedf9e734d 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -83,15 +83,15 @@ export interface BaseApplicationListenerProps { readonly defaultAction?: ListenerAction; /** - * Allow anyone to connect to this listener + * Allow anyone to connect to the load balancer on the listener port * - * If this is specified, the listener will be opened up to anyone who can reach it. + * If this is specified, the load balancer will be opened up to anyone who can reach it. * For internal load balancers this is anyone in the same VPC. For public load * balancers, this is anyone on the internet. * * If you want to be more selective about who can access this load * balancer, set this to `false` and use the listener's `connections` - * object to selectively grant access to the listener. + * object to selectively grant access to the load balancer on the listener port. * * @default true */ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts index cf34ee5a7857e..98bfdc90796bb 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts @@ -134,6 +134,17 @@ export class ApplicationTargetGroup extends TargetGroupBase implements IApplicat this.protocol = protocol; this.port = port; + // this.targetType is lazy + this.node.addValidation({ + validate: () => { + if (this.targetType === TargetType.LAMBDA && (this.port || this.protocol)) { + return ['port/protocol should not be specified for Lambda targets']; + } else { + return []; + } + }, + }); + this.connectableMembers = []; this.listeners = []; diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts index b54ae026b86fb..93e4e7cdd9b7a 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts @@ -1,7 +1,5 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; -import { PolicyStatement, ServicePrincipal } from '@aws-cdk/aws-iam'; -import { IBucket } from '@aws-cdk/aws-s3'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { Resource } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; @@ -125,41 +123,6 @@ export class NetworkLoadBalancer extends BaseLoadBalancer implements INetworkLoa }); } - /** - * Enable access logging for this load balancer. - * - * A region must be specified on the stack containing the load balancer; you cannot enable logging on - * environment-agnostic stacks. See https://docs.aws.amazon.com/cdk/latest/guide/environments.html - * - * This is extending the BaseLoadBalancer.logAccessLogs method to match the bucket permissions described - * at https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-access-logs.html#access-logging-bucket-requirements - */ - public logAccessLogs(bucket: IBucket, prefix?: string) { - super.logAccessLogs(bucket, prefix); - - const logsDeliveryServicePrincipal = new ServicePrincipal('delivery.logs.amazonaws.com'); - - bucket.addToResourcePolicy( - new PolicyStatement({ - actions: ['s3:PutObject'], - principals: [logsDeliveryServicePrincipal], - resources: [ - bucket.arnForObjects(`${prefix ? prefix + '/' : ''}AWSLogs/${this.stack.account}/*`), - ], - conditions: { - StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' }, - }, - }), - ); - bucket.addToResourcePolicy( - new PolicyStatement({ - actions: ['s3:GetBucketAcl'], - principals: [logsDeliveryServicePrincipal], - resources: [bucket.bucketArn], - }), - ); - } - /** * Return the given named metric for this Network Load Balancer * @@ -326,4 +289,4 @@ class LookedUpNetworkLoadBalancer extends Resource implements INetworkLoadBalanc ...props, }); } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts index 85ae9d143f0e2..0fcb0f4916c87 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts @@ -1,5 +1,6 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; +import { PolicyStatement, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { ContextProvider, IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; @@ -192,8 +193,11 @@ export abstract class BaseLoadBalancer extends Resource { /** * The VPC this load balancer has been created in. + * + * This property is always defined (not `null` or `undefined`) for sub-classes of `BaseLoadBalancer`. */ - public readonly vpc: ec2.IVpc; + public readonly vpc?: ec2.IVpc; + /** * Attributes set on this load balancer */ @@ -255,7 +259,27 @@ export abstract class BaseLoadBalancer extends Resource { throw new Error(`Cannot enable access logging; don't know ELBv2 account for region ${region}`); } + const logsDeliveryServicePrincipal = new ServicePrincipal('delivery.logs.amazonaws.com'); bucket.grantPut(new iam.AccountPrincipal(account), `${(prefix ? prefix + '/' : '')}AWSLogs/${Stack.of(this).account}/*`); + bucket.addToResourcePolicy( + new PolicyStatement({ + actions: ['s3:PutObject'], + principals: [logsDeliveryServicePrincipal], + resources: [ + bucket.arnForObjects(`${prefix ? prefix + '/' : ''}AWSLogs/${this.stack.account}/*`), + ], + conditions: { + StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' }, + }, + }), + ); + bucket.addToResourcePolicy( + new PolicyStatement({ + actions: ['s3:GetBucketAcl'], + principals: [logsDeliveryServicePrincipal], + resources: [bucket.bucketArn], + }), + ); // make sure the bucket's policy is created before the ALB (see https://github.com/aws/aws-cdk/issues/1633) this.node.addDependency(bucket); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json index b20b9b8dc9534..1668c5ffc511f 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json @@ -79,12 +79,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-certificatemanager": "0.0.0", diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/actions.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/actions.test.ts index b3a6397ba6eaa..5df99ef270b0a 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/actions.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/actions.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as elbv2 from '../../lib'; @@ -25,7 +25,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { TargetGroupArn: { Ref: 'TargetGroup1E5480F51' }, @@ -45,7 +45,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { ForwardConfig: { @@ -81,7 +81,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { ForwardConfig: { @@ -122,7 +122,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { AuthenticateOidcConfig: { @@ -161,7 +161,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerRule', { Actions: [ { TargetGroupArn: { Ref: 'TargetGroup2D571E5D7' }, @@ -190,7 +190,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerRule', { Actions: [ { TargetGroupArn: { Ref: 'TargetGroup2D571E5D7' }, diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/conditions.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/conditions.test.ts index 6864ecaa80c2b..e7d1bccab934b 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/conditions.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/conditions.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import * as elbv2 from '../../lib'; describe('tests', () => { 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 14f9bd51bee61..5caf1f756dd80 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts @@ -1,5 +1,4 @@ -import { MatchStyle } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as acm from '@aws-cdk/aws-certificatemanager'; import { Metric } from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; @@ -24,7 +23,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { Protocol: 'HTTPS', }); }); @@ -42,7 +41,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { Port: 80, }); }); @@ -60,7 +59,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { SecurityGroupIngress: [ { Description: 'Allow from anyone on port 80', @@ -86,7 +85,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { SecurityGroupIngress: [ { Description: 'Allow from anyone on port 80', @@ -138,7 +137,7 @@ describe('tests', () => { listener.addCertificates('Certs', [importedCertificate(stack, 'cert')]); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { Certificates: [ { CertificateArn: 'cert' }, ], @@ -169,15 +168,15 @@ describe('tests', () => { expect(listener.node.tryFindChild('DefaultCertificates3')).not.toBeDefined(); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { Certificates: [{ CertificateArn: 'cert1' }], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerCertificate', { Certificates: [{ CertificateArn: 'cert2' }], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerCertificate', { Certificates: [{ CertificateArn: 'cert3' }], }); }); @@ -195,7 +194,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { TargetType: 'ip', }); }); @@ -213,7 +212,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { Name: 'foo', }); }); @@ -237,7 +236,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { TargetGroupArn: { Ref: 'TargetGroup3D7CD9B8' }, @@ -245,7 +244,7 @@ describe('tests', () => { }, ], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 10, Conditions: [ { @@ -282,7 +281,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { TargetGroupArn: { Ref: 'LBListenerTargetsGroup76EF81E8' }, @@ -290,7 +289,7 @@ describe('tests', () => { }, ], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { VpcId: { Ref: 'Stack8A423254' }, Port: 80, Protocol: 'HTTP', @@ -298,7 +297,7 @@ describe('tests', () => { { Id: 'i-12345' }, ], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerRule', { Actions: [ { TargetGroupArn: { Ref: 'LBListenerWithPathGroupE889F9E5' }, @@ -306,7 +305,7 @@ describe('tests', () => { }, ], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { VpcId: { Ref: 'Stack8A423254' }, Port: 80, Protocol: 'HTTP', @@ -328,7 +327,7 @@ describe('tests', () => { listener.addTargets('Targets', { port: 8080, targets: [new elbv2.IpTarget('1.2.3.4')] }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { Certificates: [ { CertificateArn: 'cert' }, ], @@ -348,7 +347,7 @@ describe('tests', () => { listener2.addCertificates('Certs', [importedCertificate(stack2, 'cert')]); // THEN - expect(stack2).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { + Template.fromStack(stack2).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerCertificate', { Certificates: [ { CertificateArn: 'cert' }, ], @@ -370,7 +369,7 @@ describe('tests', () => { group.enableCookieStickiness(cdk.Duration.hours(1)); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { TargetGroupAttributes: [ { Key: 'stickiness.enabled', @@ -403,7 +402,7 @@ describe('tests', () => { group.enableCookieStickiness(cdk.Duration.hours(1), 'MyDeliciousCookie'); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { TargetGroupAttributes: [ { Key: 'stickiness.enabled', @@ -445,7 +444,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { UnhealthyThresholdCount: 3, HealthCheckIntervalSeconds: 60, HealthCheckPath: '/test', @@ -494,7 +493,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { ProtocolVersion: 'GRPC', }); }); @@ -517,7 +516,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerRule', { ListenerArn: 'ieks', Priority: 30, Actions: [ @@ -547,7 +546,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerRule', { ListenerArn: 'ieks', Priority: 30, Actions: [ @@ -575,14 +574,14 @@ describe('tests', () => { }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches(Match.objectLike({ Resources: { SomeResource: { Type: 'Test::Resource', DependsOn: ['LoadBalancerListenerE1A099B9'], }, }, - }, MatchStyle.SUPERSET); + })); }); test('Exercise metrics', () => { @@ -648,14 +647,14 @@ describe('tests', () => { }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches(Match.objectLike({ Resources: { SomeResource: { Type: 'Test::Resource', DependsOn: ['LoadBalancerListenerSecondGroupRuleF5FDC196'], }, }, - }, MatchStyle.SUPERSET); + })); }); test('Can add fixed responses', () => { @@ -683,7 +682,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { FixedResponseConfig: { @@ -696,7 +695,7 @@ describe('tests', () => { ], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerRule', { Actions: [ { FixedResponseConfig: { @@ -733,7 +732,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { RedirectConfig: { @@ -746,7 +745,7 @@ describe('tests', () => { ], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerRule', { Actions: [ { RedirectConfig: { @@ -771,7 +770,7 @@ describe('tests', () => { lb.addRedirect(); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { Port: 80, Protocol: 'HTTP', DefaultActions: [ @@ -800,7 +799,7 @@ describe('tests', () => { loadBalancer.addRedirect({ open: false }); // THEN - expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroup', { + const matchingGroups = Template.fromStack(stack).findResources('AWS::EC2::SecurityGroup', { SecurityGroupIngress: [ { CidrIp: '0.0.0.0/0', @@ -809,7 +808,7 @@ describe('tests', () => { }, ], }); - + expect(Object.keys(matchingGroups).length).toBe(0); }); test('Can add simple redirect responses with custom values', () => { @@ -830,7 +829,7 @@ describe('tests', () => { listener.addCertificates('ListenerCertificateX', [importedCertificate(stack, 'cert3')]); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { Port: 8443, Protocol: 'HTTPS', DefaultActions: [ @@ -859,7 +858,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { TargetGroupAttributes: [ { Key: 'deregistration_delay.timeout_seconds', @@ -888,7 +887,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { TargetGroupAttributes: [ { Key: 'stickiness.enabled', @@ -1092,7 +1091,7 @@ describe('tests', () => { listener.connections.allowToAnyIpv4(ec2.Port.tcp(443)); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: 'security-group-id', }); }); @@ -1114,11 +1113,11 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { Protocol: 'HTTPS', }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerCertificate', { Certificates: [{ CertificateArn: 'cert2' }], }); }); @@ -1137,11 +1136,11 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { Protocol: 'HTTPS', }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerCertificate', { Certificates: [{ CertificateArn: 'cert2' }], }); }); @@ -1162,15 +1161,15 @@ describe('tests', () => { listener.addCertificateArns('ListenerCertificateX', ['cert3']); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { Protocol: 'HTTPS', }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerCertificate', { Certificates: [{ CertificateArn: 'cert2' }], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerCertificate', { Certificates: [{ CertificateArn: 'cert3' }], }); }); @@ -1194,7 +1193,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 10, Conditions: [ { @@ -1257,7 +1256,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 10, Conditions: [ { @@ -1276,7 +1275,7 @@ describe('tests', () => { ], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 20, Conditions: [ { @@ -1338,7 +1337,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 10, Conditions: [ { @@ -1370,7 +1369,7 @@ describe('tests', () => { ], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 20, Conditions: [ { @@ -1389,7 +1388,7 @@ describe('tests', () => { ], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 30, Conditions: [ { @@ -1407,7 +1406,7 @@ describe('tests', () => { ], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 40, Conditions: [ { @@ -1445,7 +1444,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 10, Conditions: [ { @@ -1485,7 +1484,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 1, Conditions: [ { @@ -1560,7 +1559,7 @@ describe('tests', () => { }); // THEN - expect(stack).not.toHaveResource('AWS::ElasticLoadBalancingV2::Listener'); + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::Listener', 0); expect(listener.listenerArn).toEqual('arn:aws:elasticloadbalancing:us-west-2:123456789012:listener/application/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2'); expect(listener.connections.securityGroups[0].securityGroupId).toEqual('sg-12345'); }); @@ -1592,7 +1591,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 5, }); }); @@ -1619,7 +1618,7 @@ describe('tests', () => { ]); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerCertificate', { Certificates: [ { CertificateArn: 'arn:something' }, ], diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts index d935a915d55e7..5bb54df75ac24 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts @@ -1,11 +1,10 @@ -import { ResourcePart, arrayWith } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import { Metric } from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as s3 from '@aws-cdk/aws-s3'; +import { testFutureBehavior, testLegacyBehavior } from '@aws-cdk/cdk-build-tools/lib/feature-flag'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import { testFutureBehavior } from '@aws-cdk/cdk-build-tools/lib/feature-flag'; import * as elbv2 from '../../lib'; const s3GrantWriteCtx = { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }; @@ -23,7 +22,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { Scheme: 'internet-facing', Subnets: [ { Ref: 'StackPublicSubnet1Subnet0AD81D22' }, @@ -45,12 +44,12 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { DependsOn: [ 'StackPublicSubnet1DefaultRoute16154E3D', 'StackPublicSubnet2DefaultRoute0319539B', ], - }, ResourcePart.CompleteDefinition); + }); }); test('Trivial construction: internal', () => { @@ -62,7 +61,7 @@ describe('tests', () => { new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { Scheme: 'internal', Subnets: [ { Ref: 'StackPrivateSubnet1Subnet47AC2BC7' }, @@ -86,7 +85,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { LoadBalancerAttributes: [ { Key: 'deletion_protection.enabled', @@ -116,13 +115,13 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { - LoadBalancerAttributes: arrayWith( + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { + LoadBalancerAttributes: Match.arrayWith([ { Key: 'deletion_protection.enabled', Value: 'false', }, - ), + ]), }); }); @@ -143,109 +142,226 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener'); + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::Listener', 1); expect(loadBalancer.listeners).toContain(listener); }); - testFutureBehavior('Access logging', s3GrantWriteCtx, cdk.App, (app) => { - // GIVEN - const stack = new cdk.Stack(app, undefined, { env: { region: 'us-east-1' } }); - const vpc = new ec2.Vpc(stack, 'Stack'); - const bucket = new s3.Bucket(stack, 'AccessLoggingBucket'); - const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + describe('logAccessLogs', () => { - // WHEN - lb.logAccessLogs(bucket); + function loggingSetup(app: cdk.App): { stack: cdk.Stack, bucket: s3.Bucket, lb: elbv2.ApplicationLoadBalancer } { + const stack = new cdk.Stack(app, undefined, { env: { region: 'us-east-1' } }); + const vpc = new ec2.Vpc(stack, 'Stack'); + const bucket = new s3.Bucket(stack, 'AccessLoggingBucket'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + return { stack, bucket, lb }; + } - // THEN + test('sets load balancer attributes', () => { + // GIVEN + const app = new cdk.App(); + const { stack, bucket, lb } = loggingSetup(app); - // verify that the LB attributes reference the bucket - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { - LoadBalancerAttributes: arrayWith( - { - Key: 'access_logs.s3.enabled', - Value: 'true', - }, - { - Key: 'access_logs.s3.bucket', - Value: { Ref: 'AccessLoggingBucketA6D88F29' }, - }, - { - Key: 'access_logs.s3.prefix', - Value: '', - }, - ), - }); + // WHEN + lb.logAccessLogs(bucket); - // verify the bucket policy allows the ALB to put objects in the bucket - expect(stack).toHaveResource('AWS::S3::BucketPolicy', { - PolicyDocument: { - Version: '2012-10-17', - Statement: [ + //THEN + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { + LoadBalancerAttributes: Match.arrayWith([ { - Action: ['s3:PutObject', 's3:Abort*'], - Effect: 'Allow', - Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } }, - Resource: { - 'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/AWSLogs/', - { Ref: 'AWS::AccountId' }, '/*']], - }, + Key: 'access_logs.s3.enabled', + Value: 'true', }, - ], - }, + { + Key: 'access_logs.s3.bucket', + Value: { Ref: 'AccessLoggingBucketA6D88F29' }, + }, + { + Key: 'access_logs.s3.prefix', + Value: '', + }, + ]), + }); }); - // verify the ALB depends on the bucket *and* the bucket policy - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { - DependsOn: ['AccessLoggingBucketPolicy700D7CC6', 'AccessLoggingBucketA6D88F29'], - }, ResourcePart.CompleteDefinition); - }); + test('adds a dependency on the bucket', () => { + // GIVEN + const app = new cdk.App(); + const { stack, bucket, lb } = loggingSetup(app); - testFutureBehavior('access logging with prefix', s3GrantWriteCtx, cdk.App, (app) => { - // GIVEN - const stack = new cdk.Stack(app, undefined, { env: { region: 'us-east-1' } }); - const vpc = new ec2.Vpc(stack, 'Stack'); - const bucket = new s3.Bucket(stack, 'AccessLoggingBucket'); - const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + // WHEN + lb.logAccessLogs(bucket); - // WHEN - lb.logAccessLogs(bucket, 'prefix-of-access-logs'); + // THEN + // verify the ALB depends on the bucket *and* the bucket policy + Template.fromStack(stack).hasResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + DependsOn: ['AccessLoggingBucketPolicy700D7CC6', 'AccessLoggingBucketA6D88F29'], + }); + }); - // THEN - // verify that the LB attributes reference the bucket - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { - LoadBalancerAttributes: arrayWith( - { - Key: 'access_logs.s3.enabled', - Value: 'true', - }, - { - Key: 'access_logs.s3.bucket', - Value: { Ref: 'AccessLoggingBucketA6D88F29' }, + testLegacyBehavior('legacy bucket permissions', cdk.App, (app) => { + const { stack, bucket, lb } = loggingSetup(app); + + // WHEN + lb.logAccessLogs(bucket); + + // THEN + // verify the bucket policy allows the ALB to put objects in the bucket + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: ['s3:PutObject*', 's3:Abort*'], + Effect: 'Allow', + Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } }, + Resource: { + 'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/AWSLogs/', + { Ref: 'AWS::AccountId' }, '/*']], + }, + }, + { + Action: 's3:PutObject', + Effect: 'Allow', + Principal: { Service: 'delivery.logs.amazonaws.com' }, + Resource: { + 'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/AWSLogs/', + { Ref: 'AWS::AccountId' }, '/*']], + }, + Condition: { StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' } }, + }, + { + Action: 's3:GetBucketAcl', + Effect: 'Allow', + Principal: { Service: 'delivery.logs.amazonaws.com' }, + Resource: { + 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'], + }, + }, + ], }, - { - Key: 'access_logs.s3.prefix', - Value: 'prefix-of-access-logs', + }); + }); + + testFutureBehavior('logging bucket permissions', s3GrantWriteCtx, cdk.App, (app) => { + // GIVEN + const { stack, bucket, lb } = loggingSetup(app); + + // WHEN + lb.logAccessLogs(bucket); + + // THEN + // verify the bucket policy allows the ALB to put objects in the bucket + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: [ + 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', + 's3:Abort*', + ], + Effect: 'Allow', + Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } }, + Resource: { + 'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/AWSLogs/', + { Ref: 'AWS::AccountId' }, '/*']], + }, + }, + { + Action: 's3:PutObject', + Effect: 'Allow', + Principal: { Service: 'delivery.logs.amazonaws.com' }, + Resource: { + 'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/AWSLogs/', + { Ref: 'AWS::AccountId' }, '/*']], + }, + Condition: { StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' } }, + }, + { + Action: 's3:GetBucketAcl', + Effect: 'Allow', + Principal: { Service: 'delivery.logs.amazonaws.com' }, + Resource: { + 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'], + }, + }, + ], }, - ), + }); }); - // verify the bucket policy allows the ALB to put objects in the bucket - expect(stack).toHaveResource('AWS::S3::BucketPolicy', { - PolicyDocument: { - Version: '2012-10-17', - Statement: [ + testFutureBehavior('access logging with prefix', s3GrantWriteCtx, cdk.App, (app) => { + // GIVEN + const { stack, bucket, lb } = loggingSetup(app); + + // WHEN + lb.logAccessLogs(bucket, 'prefix-of-access-logs'); + + // THEN + // verify that the LB attributes reference the bucket + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { + LoadBalancerAttributes: Match.arrayWith([ { - Action: ['s3:PutObject', 's3:Abort*'], - Effect: 'Allow', - Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } }, - Resource: { - 'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/prefix-of-access-logs/AWSLogs/', - { Ref: 'AWS::AccountId' }, '/*']], - }, + Key: 'access_logs.s3.enabled', + Value: 'true', + }, + { + Key: 'access_logs.s3.bucket', + Value: { Ref: 'AccessLoggingBucketA6D88F29' }, }, - ], - }, + { + Key: 'access_logs.s3.prefix', + Value: 'prefix-of-access-logs', + }, + ]), + }); + + // verify the bucket policy allows the ALB to put objects in the bucket + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: [ + 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', + 's3:Abort*', + ], + Effect: 'Allow', + Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } }, + Resource: { + 'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/prefix-of-access-logs/AWSLogs/', + { Ref: 'AWS::AccountId' }, '/*']], + }, + }, + { + Action: 's3:PutObject', + Effect: 'Allow', + Principal: { Service: 'delivery.logs.amazonaws.com' }, + Resource: { + 'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/prefix-of-access-logs/AWSLogs/', + { Ref: 'AWS::AccountId' }, '/*']], + }, + Condition: { StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' } }, + }, + { + Action: 's3:GetBucketAcl', + Effect: 'Allow', + Principal: { Service: 'delivery.logs.amazonaws.com' }, + Resource: { + 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'], + }, + }, + ], + }, + }); }); }); @@ -300,7 +416,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { Name: 'myLoadBalancer', }); }); @@ -364,7 +480,7 @@ describe('tests', () => { listener.addTargets('Targets', { port: 8080 }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener'); + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::Listener', 1); expect(() => alb.listeners).toThrow(); }); @@ -393,7 +509,7 @@ describe('tests', () => { alb.addSecurityGroup(new ec2.SecurityGroup(stack, 'SecurityGroup2', { vpc })); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { SecurityGroups: [ { 'Fn::GetAtt': ['SecurityGroup1F554B36F', 'GroupId'] }, { 'Fn::GetAtt': ['SecurityGroup23BE86BB7', 'GroupId'] }, @@ -421,7 +537,7 @@ describe('tests', () => { }); // THEN - expect(stack).not.toHaveResource('AWS::ElasticLoadBalancingV2::ApplicationLoadBalancer'); + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::ApplicationLoadBalancer', 0); expect(loadBalancer.loadBalancerArn).toEqual('arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/application/my-load-balancer/50dc6c495c0c9188'); expect(loadBalancer.loadBalancerCanonicalHostedZoneId).toEqual('Z3DZXE0EXAMPLE'); expect(loadBalancer.loadBalancerDnsName).toEqual('my-load-balancer-1234567890.us-west-2.elb.amazonaws.com'); @@ -453,7 +569,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener'); + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::Listener', 1); expect(() => loadBalancer.listeners).toThrow(); }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/security-group.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/security-group.test.ts index 03d17a207f16a..e0aeafb44ee75 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/security-group.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/security-group.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import * as cdk from '@aws-cdk/core'; @@ -193,7 +193,7 @@ describe('tests', () => { fixture.listener.connections.allowDefaultPortFromAnyIpv4('Open to the world'); // THEN - expect(fixture.stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(fixture.stack).hasResourceProperties('AWS::EC2::SecurityGroup', { SecurityGroupIngress: [ { CidrIp: '0.0.0.0/0', @@ -220,7 +220,7 @@ describe('tests', () => { listener2.connections.allowDefaultPortFromAnyIpv4('Open to the world'); // THEN - expect(stack2).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack2).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { CidrIp: '0.0.0.0/0', Description: 'Open to the world', IpProtocol: 'tcp', @@ -243,7 +243,7 @@ function expectedImportedSGRules(stack: cdk.Stack) { } function expectSGRules(stack: cdk.Stack, lbGroup: any) { - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: lbGroup, IpProtocol: 'tcp', Description: 'Load balancer to target', @@ -251,7 +251,7 @@ function expectSGRules(stack: cdk.Stack, lbGroup: any) { FromPort: 8008, ToPort: 8008, }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { IpProtocol: 'tcp', Description: 'Load balancer to target', FromPort: 8008, 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 54848cae02679..57a3aabe6a11b 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 @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import * as cdk from '@aws-cdk/core'; @@ -38,13 +38,43 @@ describe('tests', () => { }, }); - expect(stack).not.toHaveResourceLike('AWS::ElasticLoadBalancingV2::TargetGroup', { + const matches = Template.fromStack(stack).findResources('AWS::ElasticLoadBalancingV2::TargetGroup', { TargetGroupAttributes: [ { Key: 'stickiness.enabled', }, ], }); + expect(Object.keys(matches).length).toBe(0); + }); + + test('Lambda target should not have port set', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + + const tg = new elbv2.ApplicationTargetGroup(stack, 'TG2', { + protocol: elbv2.ApplicationProtocol.HTTPS, + }); + tg.addTarget({ + attachToApplicationTargetGroup(_targetGroup: elbv2.IApplicationTargetGroup): elbv2.LoadBalancerTargetProps { + return { + targetType: elbv2.TargetType.LAMBDA, + targetJson: { id: 'arn:aws:lambda:eu-west-1:123456789012:function:myFn' }, + }; + }, + }); + expect(() => app.synth()).toThrow(/port\/protocol should not be specified for Lambda targets/); + }); + + test('Lambda target should not have protocol set', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + + new elbv2.ApplicationTargetGroup(stack, 'TG', { + port: 443, + targetType: elbv2.TargetType.LAMBDA, + }); + expect(() => app.synth()).toThrow(/port\/protocol should not be specified for Lambda targets/); }); test('Can add self-registering target to imported TargetGroup', () => { @@ -103,7 +133,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { HealthCheckEnabled: true, HealthCheckIntervalSeconds: 255, HealthCheckPath: '/arbitrary', @@ -130,7 +160,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { TargetGroupAttributes: [ { Key: 'stickiness.enabled', @@ -162,7 +192,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { TargetGroupAttributes: [ { Key: 'stickiness.enabled', @@ -197,7 +227,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { TargetGroupAttributes: [ { Key: 'stickiness.enabled', @@ -233,7 +263,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { ProtocolVersion: 'GRPC', HealthCheckEnabled: true, HealthCheckIntervalSeconds: 255, diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/actions.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/actions.test.ts index b6f53fa53d584..d01b0cd666db0 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/actions.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/actions.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as elbv2 from '../../lib'; @@ -27,7 +27,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { ForwardConfig: { @@ -63,7 +63,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { ForwardConfig: { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts index a5b3d5aea1e5c..b92df26e76bb7 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts @@ -1,5 +1,4 @@ -import { MatchStyle } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as acm from '@aws-cdk/aws-certificatemanager'; import * as ec2 from '@aws-cdk/aws-ec2'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; @@ -22,7 +21,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { Protocol: 'TCP', Port: 443, }); @@ -40,7 +39,7 @@ describe('tests', () => { listener.addTargetGroups('Default', group); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { TargetGroupArn: { Ref: 'TargetGroup3D7CD9B8' }, @@ -64,7 +63,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { TargetGroupArn: { Ref: 'LBListenerTargetsGroup76EF81E8' }, @@ -72,7 +71,7 @@ describe('tests', () => { }, ], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { VpcId: { Ref: 'Stack8A423254' }, Port: 80, Protocol: 'TCP', @@ -96,7 +95,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { TargetGroupArn: { Ref: 'LBListenerTargetsGroup76EF81E8' }, @@ -104,7 +103,7 @@ describe('tests', () => { }, ], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { VpcId: { Ref: 'Stack8A423254' }, Port: 9700, Protocol: 'TCP_UDP', @@ -139,7 +138,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { Protocol: 'TLS', Port: 443, Certificates: [ @@ -153,7 +152,7 @@ describe('tests', () => { }, ], }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { VpcId: { Ref: 'Stack8A423254' }, Port: 80, Protocol: 'TCP', @@ -180,7 +179,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { HealthCheckIntervalSeconds: 30, }); }); @@ -200,7 +199,7 @@ describe('tests', () => { new ResourceWithLBDependency(stack, 'MyResource', group); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches(Match.objectLike({ Resources: { MyResource: { Type: 'Test::Resource', @@ -212,7 +211,7 @@ describe('tests', () => { ], }, }, - }, MatchStyle.SUPERSET); + })); }); test('Trivial add TLS listener', () => { @@ -234,7 +233,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { Protocol: 'TLS', Port: 443, Certificates: [ @@ -266,7 +265,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { Protocol: 'TLS', Port: 443, AlpnPolicy: ['HTTP2Only'], @@ -452,7 +451,7 @@ describe('tests', () => { }); // THEN - expect(stack).not.toHaveResource('AWS::ElasticLoadBalancingV2::Listener'); + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::Listener', 0); expect(listener.listenerArn).toEqual('arn:aws:elasticloadbalancing:us-west-2:123456789012:listener/network/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2'); }); }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts index ccfb2924e211e..2454cd1ccfb01 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts @@ -1,10 +1,9 @@ -import { ResourcePart, arrayWith } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as s3 from '@aws-cdk/aws-s3'; +import { testFutureBehavior } from '@aws-cdk/cdk-build-tools/lib/feature-flag'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import { testFutureBehavior } from '@aws-cdk/cdk-build-tools/lib/feature-flag'; import * as elbv2 from '../../lib'; const s3GrantWriteCtx = { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }; @@ -22,7 +21,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { Scheme: 'internet-facing', Subnets: [ { Ref: 'StackPublicSubnet1Subnet0AD81D22' }, @@ -41,7 +40,7 @@ describe('tests', () => { new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { Scheme: 'internal', Subnets: [ { Ref: 'StackPrivateSubnet1Subnet47AC2BC7' }, @@ -63,13 +62,13 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { - LoadBalancerAttributes: arrayWith( + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { + LoadBalancerAttributes: Match.arrayWith([ { Key: 'load_balancing.cross_zone.enabled', Value: 'true', }, - ), + ]), }); }); @@ -86,8 +85,8 @@ describe('tests', () => { // THEN // verify that the LB attributes reference the bucket - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { - LoadBalancerAttributes: arrayWith( + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { + LoadBalancerAttributes: Match.arrayWith([ { Key: 'access_logs.s3.enabled', Value: 'true', @@ -100,16 +99,23 @@ describe('tests', () => { Key: 'access_logs.s3.prefix', Value: '', }, - ), + ]), }); // verify the bucket policy allows the ALB to put objects in the bucket - expect(stack).toHaveResource('AWS::S3::BucketPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { PolicyDocument: { Version: '2012-10-17', Statement: [ { - Action: ['s3:PutObject', 's3:Abort*'], + Action: [ + 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', + 's3:Abort*', + ], Effect: 'Allow', Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } }, Resource: { @@ -140,9 +146,9 @@ describe('tests', () => { }); // verify the ALB depends on the bucket *and* the bucket policy - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { DependsOn: ['AccessLoggingBucketPolicy700D7CC6', 'AccessLoggingBucketA6D88F29'], - }, ResourcePart.CompleteDefinition); + }); }); testFutureBehavior('access logging with prefix', s3GrantWriteCtx, cdk.App, (app) => { @@ -157,8 +163,8 @@ describe('tests', () => { // THEN // verify that the LB attributes reference the bucket - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { - LoadBalancerAttributes: arrayWith( + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { + LoadBalancerAttributes: Match.arrayWith([ { Key: 'access_logs.s3.enabled', Value: 'true', @@ -171,16 +177,23 @@ describe('tests', () => { Key: 'access_logs.s3.prefix', Value: 'prefix-of-access-logs', }, - ), + ]), }); // verify the bucket policy allows the ALB to put objects in the bucket - expect(stack).toHaveResource('AWS::S3::BucketPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { PolicyDocument: { Version: '2012-10-17', Statement: [ { - Action: ['s3:PutObject', 's3:Abort*'], + Action: [ + 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', + 's3:Abort*', + ], Effect: 'Allow', Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } }, Resource: { @@ -223,7 +236,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { Name: 'myLoadBalancer', }); }); @@ -285,7 +298,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { Scheme: 'internal', Subnets: [ { Ref: 'VPCIsolatedSubnet1SubnetEBD00FC6' }, @@ -320,7 +333,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { Scheme: 'internal', Subnets: [ { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, @@ -355,7 +368,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { Scheme: 'internet-facing', Subnets: [ { Ref: 'VPCPublicSubnet1SubnetB4246D30' }, @@ -377,7 +390,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { Scheme: 'internal', Subnets: [ { Ref: 'VPCPublicSubnet1SubnetB4246D30' }, @@ -413,7 +426,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { Scheme: 'internal', Subnets: [ { Ref: 'VPCIsolatedSubnet1SubnetEBD00FC6' }, @@ -442,7 +455,7 @@ describe('tests', () => { }); // THEN - expect(stack).not.toHaveResource('AWS::ElasticLoadBalancingV2::NetworkLoadBalancer'); + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::NetworkLoadBalancer', 0); expect(loadBalancer.loadBalancerArn).toEqual('arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/network/my-load-balancer/50dc6c495c0c9188'); expect(loadBalancer.loadBalancerCanonicalHostedZoneId).toEqual('Z3DZXE0EXAMPLE'); expect(loadBalancer.loadBalancerDnsName).toEqual('my-load-balancer-1234567890.us-west-2.elb.amazonaws.com'); @@ -478,8 +491,8 @@ describe('tests', () => { }); // THEN - expect(stack).not.toHaveResource('AWS::ElasticLoadBalancingV2::NetworkLoadBalancer'); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener'); + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::NetworkLoadBalancer', 0); + Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::Listener', 1); }); }); }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/target-group.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/target-group.test.ts index d1d516ea83665..7caaf465b045d 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/target-group.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/target-group.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; @@ -18,7 +18,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { TargetGroupAttributes: [ { Key: 'proxy_protocol_v2.enabled', @@ -41,7 +41,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { TargetGroupAttributes: [ { Key: 'preserve_client_ip.enabled', @@ -64,7 +64,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { TargetGroupAttributes: [ { Key: 'proxy_protocol_v2.enabled', @@ -87,7 +87,7 @@ describe('tests', () => { }); // THEN - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { TargetGroupAttributes: [ { Key: 'preserve_client_ip.enabled', @@ -107,7 +107,7 @@ describe('tests', () => { protocol: elbv2.Protocol.UDP, }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { Protocol: 'UDP', }); }); @@ -121,7 +121,7 @@ describe('tests', () => { port: 80, }); - expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { Protocol: 'TCP', }); }); diff --git a/packages/@aws-cdk/aws-elasticsearch/package.json b/packages/@aws-cdk/aws-elasticsearch/package.json index 73b7bafbe8b41..bbcdd5dbe8061 100644 --- a/packages/@aws-cdk/aws-elasticsearch/package.json +++ b/packages/@aws-cdk/aws-elasticsearch/package.json @@ -79,12 +79,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-certificatemanager": "0.0.0", diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index 5fc9035dc92f7..3242e75d2c253 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -1,6 +1,5 @@ /* eslint-disable jest/expect-expect */ -import '@aws-cdk/assert-internal/jest'; -import * as assert from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as acm from '@aws-cdk/aws-certificatemanager'; import { Metric, Statistic } from '@aws-cdk/aws-cloudwatch'; import { Vpc, EbsDeviceVolumeType, SecurityGroup } from '@aws-cdk/aws-ec2'; @@ -53,7 +52,7 @@ test('subnets and security groups can be provided when vpc is used', () => { }); expect(domain.connections.securityGroups[0].securityGroupId).toEqual(securityGroup.securityGroupId); - expect(stack).toHaveResource('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { VPCOptions: { SecurityGroupIds: [ { @@ -70,7 +69,6 @@ test('subnets and security groups can be provided when vpc is used', () => { ], }, }); - }); test('default subnets and security group when vpc is used', () => { @@ -82,7 +80,7 @@ test('default subnets and security group when vpc is used', () => { }); expect(stack.resolve(domain.connections.securityGroups[0].securityGroupId)).toEqual({ 'Fn::GetAtt': ['DomainSecurityGroup48AA5FD6', 'GroupId'] }); - expect(stack).toHaveResource('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { VPCOptions: { SecurityGroupIds: [ { @@ -105,7 +103,6 @@ test('default subnets and security group when vpc is used', () => { ], }, }); - }); test('default removalpolicy is retain', () => { @@ -113,9 +110,9 @@ test('default removalpolicy is retain', () => { version: ElasticsearchVersion.V7_1, }); - expect(stack).toHaveResource('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResource('AWS::Elasticsearch::Domain', { DeletionPolicy: 'Retain', - }, assert.ResourcePart.CompleteDefinition); + }); }); test('grants kms permissions if needed', () => { @@ -151,7 +148,7 @@ test('grants kms permissions if needed', () => { Version: '2012-10-17', }; - const resources = assert.expect(stack).value.Resources; + const resources = Template.fromStack(stack).toJSON().Resources; expect(resources.AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E.Properties.PolicyDocument).toStrictEqual(expectedPolicy); }); @@ -159,7 +156,7 @@ test('grants kms permissions if needed', () => { test('minimal example renders correctly', () => { new Domain(stack, 'Domain', { version: ElasticsearchVersion.V7_1 }); - expect(stack).toHaveResource('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { CognitoOptions: { Enabled: false, }, @@ -179,10 +176,10 @@ test('minimal example renders correctly', () => { Enabled: false, }, LogPublishingOptions: { - AUDIT_LOGS: assert.ABSENT, - ES_APPLICATION_LOGS: assert.ABSENT, - SEARCH_SLOW_LOGS: assert.ABSENT, - INDEX_SLOW_LOGS: assert.ABSENT, + AUDIT_LOGS: Match.absent(), + ES_APPLICATION_LOGS: Match.absent(), + SEARCH_SLOW_LOGS: Match.absent(), + INDEX_SLOW_LOGS: Match.absent(), }, NodeToNodeEncryptionOptions: { Enabled: false, @@ -196,11 +193,11 @@ test('can enable version upgrade update policy', () => { enableVersionUpgrade: true, }); - expect(stack).toHaveResource('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResource('AWS::Elasticsearch::Domain', { UpdatePolicy: { EnableVersionUpgrade: true, }, - }, assert.ResourcePart.CompleteDefinition); + }); }); describe('UltraWarm instances', () => { @@ -214,7 +211,7 @@ describe('UltraWarm instances', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { ElasticsearchClusterConfig: { DedicatedMasterEnabled: true, WarmEnabled: true, @@ -234,7 +231,7 @@ describe('UltraWarm instances', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { ElasticsearchClusterConfig: { DedicatedMasterEnabled: true, WarmEnabled: true, @@ -259,7 +256,7 @@ test('can use tokens in capacity configuration', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { ElasticsearchClusterConfig: { InstanceCount: { Ref: 'dataNodes', @@ -295,7 +292,7 @@ describe('log groups', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { LogPublishingOptions: { SEARCH_SLOW_LOGS: { CloudWatchLogsLogGroupArn: { @@ -306,9 +303,9 @@ describe('log groups', () => { }, Enabled: true, }, - AUDIT_LOGS: assert.ABSENT, - ES_APPLICATION_LOGS: assert.ABSENT, - INDEX_SLOW_LOGS: assert.ABSENT, + AUDIT_LOGS: Match.absent(), + ES_APPLICATION_LOGS: Match.absent(), + INDEX_SLOW_LOGS: Match.absent(), }, }); }); @@ -321,7 +318,7 @@ describe('log groups', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { LogPublishingOptions: { INDEX_SLOW_LOGS: { CloudWatchLogsLogGroupArn: { @@ -332,9 +329,9 @@ describe('log groups', () => { }, Enabled: true, }, - AUDIT_LOGS: assert.ABSENT, - ES_APPLICATION_LOGS: assert.ABSENT, - SEARCH_SLOW_LOGS: assert.ABSENT, + AUDIT_LOGS: Match.absent(), + ES_APPLICATION_LOGS: Match.absent(), + SEARCH_SLOW_LOGS: Match.absent(), }, }); }); @@ -347,7 +344,7 @@ describe('log groups', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { LogPublishingOptions: { ES_APPLICATION_LOGS: { CloudWatchLogsLogGroupArn: { @@ -358,9 +355,9 @@ describe('log groups', () => { }, Enabled: true, }, - AUDIT_LOGS: assert.ABSENT, - SEARCH_SLOW_LOGS: assert.ABSENT, - INDEX_SLOW_LOGS: assert.ABSENT, + AUDIT_LOGS: Match.absent(), + SEARCH_SLOW_LOGS: Match.absent(), + INDEX_SLOW_LOGS: Match.absent(), }, }); }); @@ -381,7 +378,7 @@ describe('log groups', () => { enforceHttps: true, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { LogPublishingOptions: { AUDIT_LOGS: { CloudWatchLogsLogGroupArn: { @@ -392,9 +389,9 @@ describe('log groups', () => { }, Enabled: true, }, - ES_APPLICATION_LOGS: assert.ABSENT, - SEARCH_SLOW_LOGS: assert.ABSENT, - INDEX_SLOW_LOGS: assert.ABSENT, + ES_APPLICATION_LOGS: Match.absent(), + SEARCH_SLOW_LOGS: Match.absent(), + INDEX_SLOW_LOGS: Match.absent(), }, }); }); @@ -416,7 +413,7 @@ describe('log groups', () => { slowIndexLogEnabled: true, }, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { LogPublishingOptions: { ES_APPLICATION_LOGS: { CloudWatchLogsLogGroupArn: { @@ -445,10 +442,10 @@ describe('log groups', () => { }, Enabled: true, }, - AUDIT_LOGS: assert.ABSENT, + AUDIT_LOGS: Match.absent(), }, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { LogPublishingOptions: { ES_APPLICATION_LOGS: { CloudWatchLogsLogGroupArn: { @@ -477,7 +474,7 @@ describe('log groups', () => { }, Enabled: true, }, - AUDIT_LOGS: assert.ABSENT, + AUDIT_LOGS: Match.absent(), }, }); }); @@ -497,7 +494,7 @@ describe('log groups', () => { }); // Domain1 - expect(stack).toHaveResourceLike('Custom::CloudwatchLogResourcePolicy', { + Template.fromStack(stack).hasResourceProperties('Custom::CloudwatchLogResourcePolicy', { Create: { 'Fn::Join': [ '', @@ -515,7 +512,7 @@ describe('log groups', () => { }, }); // Domain2 - expect(stack).toHaveResourceLike('Custom::CloudwatchLogResourcePolicy', { + Template.fromStack(stack).hasResourceProperties('Custom::CloudwatchLogResourcePolicy', { Create: { 'Fn::Join': [ '', @@ -554,7 +551,7 @@ describe('log groups', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { LogPublishingOptions: { SEARCH_SLOW_LOGS: { CloudWatchLogsLogGroupArn: { @@ -565,9 +562,9 @@ describe('log groups', () => { }, Enabled: true, }, - AUDIT_LOGS: assert.ABSENT, - ES_APPLICATION_LOGS: assert.ABSENT, - INDEX_SLOW_LOGS: assert.ABSENT, + AUDIT_LOGS: Match.absent(), + ES_APPLICATION_LOGS: Match.absent(), + INDEX_SLOW_LOGS: Match.absent(), }, }); }); @@ -583,7 +580,7 @@ describe('log groups', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { LogPublishingOptions: { INDEX_SLOW_LOGS: { CloudWatchLogsLogGroupArn: { @@ -594,9 +591,9 @@ describe('log groups', () => { }, Enabled: true, }, - AUDIT_LOGS: assert.ABSENT, - ES_APPLICATION_LOGS: assert.ABSENT, - SEARCH_SLOW_LOGS: assert.ABSENT, + AUDIT_LOGS: Match.absent(), + ES_APPLICATION_LOGS: Match.absent(), + SEARCH_SLOW_LOGS: Match.absent(), }, }); }); @@ -612,7 +609,7 @@ describe('log groups', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { LogPublishingOptions: { ES_APPLICATION_LOGS: { CloudWatchLogsLogGroupArn: { @@ -623,9 +620,9 @@ describe('log groups', () => { }, Enabled: true, }, - AUDIT_LOGS: assert.ABSENT, - SEARCH_SLOW_LOGS: assert.ABSENT, - INDEX_SLOW_LOGS: assert.ABSENT, + AUDIT_LOGS: Match.absent(), + SEARCH_SLOW_LOGS: Match.absent(), + INDEX_SLOW_LOGS: Match.absent(), }, }); }); @@ -649,7 +646,7 @@ describe('log groups', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { LogPublishingOptions: { AUDIT_LOGS: { CloudWatchLogsLogGroupArn: { @@ -660,9 +657,9 @@ describe('log groups', () => { }, Enabled: true, }, - ES_APPLICATION_LOGS: assert.ABSENT, - SEARCH_SLOW_LOGS: assert.ABSENT, - INDEX_SLOW_LOGS: assert.ABSENT, + ES_APPLICATION_LOGS: Match.absent(), + SEARCH_SLOW_LOGS: Match.absent(), + INDEX_SLOW_LOGS: Match.absent(), }, }); }); @@ -744,7 +741,7 @@ describe('grants', () => { domain.grantReadWrite(user); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -936,7 +933,7 @@ describe('import', () => { expect(imported.domainArn).toMatch(RegExp(`es:testregion:1234:domain/${domainName}$`)); expect(imported.domainEndpoint).toEqual(domainEndpointWithoutHttps); - expect(stack).not.toHaveResource('AWS::Elasticsearch::Domain'); + Template.fromStack(stack).resourceCountIs('AWS::Elasticsearch::Domain', 0); }); test('static fromDomainAttributes(attributes) allows importing an external/existing domain', () => { @@ -953,7 +950,7 @@ describe('import', () => { expect(imported.domainArn).toEqual(domainArn); expect(imported.domainEndpoint).toEqual(domainEndpointWithoutHttps); - expect(stack).not.toHaveResource('AWS::Elasticsearch::Domain'); + Template.fromStack(stack).resourceCountIs('AWS::Elasticsearch::Domain', 0); }); test('static fromDomainAttributes(attributes) allows importing with token arn and endpoint', () => { @@ -991,7 +988,7 @@ describe('import', () => { expect(imported.domainArn).toEqual(domainArn); expect(imported.domainEndpoint).toEqual(domainEndpoint); - expect(stack).not.toHaveResource('AWS::Elasticsearch::Domain'); + Template.fromStack(stack).resourceCountIs('AWS::Elasticsearch::Domain', 0); }); }); @@ -1014,7 +1011,7 @@ describe('advanced security options', () => { enforceHttps: true, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { AdvancedSecurityOptions: { Enabled: true, InternalUserDatabaseEnabled: false, @@ -1048,7 +1045,7 @@ describe('advanced security options', () => { enforceHttps: true, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { AdvancedSecurityOptions: { Enabled: true, InternalUserDatabaseEnabled: true, @@ -1082,7 +1079,7 @@ describe('advanced security options', () => { enforceHttps: true, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { AdvancedSecurityOptions: { Enabled: true, InternalUserDatabaseEnabled: true, @@ -1113,7 +1110,7 @@ describe('advanced security options', () => { }, }); - expect(stack).toHaveResourceLike('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { GenerateSecretString: { GenerateStringKey: 'password', }, @@ -1190,7 +1187,7 @@ describe('custom endpoints', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { DomainEndpointOptions: { EnforceHTTPS: true, CustomEndpointEnabled: true, @@ -1200,7 +1197,7 @@ describe('custom endpoints', () => { }, }, }); - expect(stack).toHaveResourceLike('AWS::CertificateManager::Certificate', { + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { DomainName: customDomainName, ValidationMethod: 'EMAIL', }); @@ -1218,7 +1215,7 @@ describe('custom endpoints', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { DomainEndpointOptions: { EnforceHTTPS: true, CustomEndpointEnabled: true, @@ -1228,7 +1225,7 @@ describe('custom endpoints', () => { }, }, }); - expect(stack).toHaveResourceLike('AWS::CertificateManager::Certificate', { + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { DomainName: customDomainName, DomainValidationOptions: [ { @@ -1240,7 +1237,7 @@ describe('custom endpoints', () => { ], ValidationMethod: 'DNS', }); - expect(stack).toHaveResourceLike('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'search.example.com.', Type: 'CNAME', HostedZoneId: { @@ -1276,7 +1273,7 @@ describe('custom endpoints', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { DomainEndpointOptions: { EnforceHTTPS: true, CustomEndpointEnabled: true, @@ -1286,7 +1283,7 @@ describe('custom endpoints', () => { }, }, }); - expect(stack).toHaveResourceLike('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'search.example.com.', Type: 'CNAME', HostedZoneId: { @@ -1548,7 +1545,7 @@ describe('custom error responses', () => { }, }); // both configurations pass synth-time validation - expect(stack).toCountResources('AWS::Elasticsearch::Domain', 2); + Template.fromStack(stack).resourceCountIs('AWS::Elasticsearch::Domain', 2); }); test('error when availabilityZoneCount is not 2 or 3', () => { @@ -1606,7 +1603,7 @@ describe('custom error responses', () => { test('can specify future version', () => { new Domain(stack, 'Domain', { version: ElasticsearchVersion.of('8.2') }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { ElasticsearchVersion: '8.2', }); }); @@ -1618,7 +1615,7 @@ describe('unsigned basic auth', () => { useUnsignedBasicAuth: true, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { AdvancedSecurityOptions: { Enabled: true, InternalUserDatabaseEnabled: true, @@ -1649,7 +1646,7 @@ describe('unsigned basic auth', () => { useUnsignedBasicAuth: true, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { AdvancedSecurityOptions: { Enabled: true, InternalUserDatabaseEnabled: false, @@ -1683,7 +1680,7 @@ describe('unsigned basic auth', () => { useUnsignedBasicAuth: true, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { AdvancedSecurityOptions: { Enabled: true, InternalUserDatabaseEnabled: true, @@ -1746,7 +1743,7 @@ describe('advanced options', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { AdvancedOptions: { 'rest.action.multi.allow_explicit_index': 'true', 'indices.fielddata.cache.size': '50', @@ -1759,8 +1756,8 @@ describe('advanced options', () => { version: ElasticsearchVersion.V7_1, }); - expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { - AdvancedOptions: assert.ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::Elasticsearch::Domain', { + AdvancedOptions: Match.absent(), }); }); }); @@ -1800,7 +1797,7 @@ function testGrant( ? resolvedPaths : resolvedPaths[0]; - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { diff --git a/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts index 53d6afe3b2cb0..38ffc94c0c05b 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import { App, Stack } from '@aws-cdk/core'; import { ElasticsearchAccessPolicy } from '../lib/elasticsearch-access-policy'; @@ -26,7 +26,7 @@ test('minimal example renders correctly', () => { })], }); - expect(stack).toHaveResource('Custom::ElasticsearchAccessPolicy', { + Template.fromStack(stack).hasResourceProperties('Custom::ElasticsearchAccessPolicy', { ServiceToken: { 'Fn::GetAtt': [ 'AWS679f53fac002430cb0da5b7982bd22872D164C4C', diff --git a/packages/@aws-cdk/aws-elasticsearch/test/log-group-resource-policy.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/log-group-resource-policy.test.ts index 68518297588c9..815c0086d4a99 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/log-group-resource-policy.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/log-group-resource-policy.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import { App, Stack } from '@aws-cdk/core'; import { LogGroupResourcePolicy } from '../lib/log-group-resource-policy'; @@ -24,7 +24,7 @@ test('minimal example renders correctly', () => { })], }); - expect(stack).toHaveResource('Custom::CloudwatchLogResourcePolicy', { + Template.fromStack(stack).hasResourceProperties('Custom::CloudwatchLogResourcePolicy', { ServiceToken: { 'Fn::GetAtt': [ 'AWS679f53fac002430cb0da5b7982bd22872D164C4C', diff --git a/packages/@aws-cdk/aws-emr/package.json b/packages/@aws-cdk/aws-emr/package.json index eed831575d4d4..d8638f3174f7a 100644 --- a/packages/@aws-cdk/aws-emr/package.json +++ b/packages/@aws-cdk/aws-emr/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -76,7 +83,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-emrcontainers/package.json b/packages/@aws-cdk/aws-emrcontainers/package.json index acb848b8537f1..72b6f669f603a 100644 --- a/packages/@aws-cdk/aws-emrcontainers/package.json +++ b/packages/@aws-cdk/aws-emrcontainers/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-emrcontainers", "module": "aws_cdk.aws_emrcontainers" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-events-targets/README.md b/packages/@aws-cdk/aws-events-targets/README.md index 1282cee012c5e..d98b08d652086 100644 --- a/packages/@aws-cdk/aws-events-targets/README.md +++ b/packages/@aws-cdk/aws-events-targets/README.md @@ -19,7 +19,7 @@ Currently supported are: * [Start a CodePipeline pipeline](#start-a-codepipeline-pipeline) * Run an ECS task * [Invoke a Lambda function](#invoke-a-lambda-function) -* [Invoke a API Gateway REST API](#invoke-a-api-gateway-rest-api) +* [Invoke a API Gateway REST API](#invoke-an-api-gateway-rest-api) * Publish a message to an SNS topic * Send a message to an SQS queue * [Start a StepFunctions state machine](#start-a-stepfunctions-state-machine) @@ -29,6 +29,7 @@ Currently supported are: * [Log an event into a LogGroup](#log-an-event-into-a-loggroup) * Put a record to a Kinesis Data Firehose stream * [Put an event on an EventBridge bus](#put-an-event-on-an-eventbridge-bus) +* [Send an event to EventBridge API Destination](#invoke-an-api-destination) See the README of the `@aws-cdk/aws-events` library for more information on EventBridge. @@ -226,7 +227,7 @@ rule.addTarget(new targets.BatchJob( )); ``` -## Invoke a API Gateway REST API +## Invoke an API Gateway REST API Use the `ApiGateway` target to trigger a REST API. @@ -267,6 +268,31 @@ rule.addTarget( ) ``` +## Invoke an API Destination + +Use the `targets.ApiDestination` target to trigger an external API. You need to +create an `events.Connection` and `events.ApiDestination` as well. + +The code snippet below creates an external destination that is invoked every hour. + +```ts +const connection = new events.Connection(this, 'Connection', { + authorization: events.Authorization.apiKey('x-api-key', SecretValue.secretsManager('ApiSecretName')), + description: 'Connection with API Key x-api-key', +}); + +const destination = new events.ApiDestination(this, 'Destination', { + connection, + endpoint: 'https://example.com', + description: 'Calling example.com with API key x-api-key', +}); + +const rule = new events.Rule(this, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + targets: [new targets.ApiDestination(destination)], +}); +``` + ## Put an event on an EventBridge bus Use the `EventBus` target to route event to a different EventBus. diff --git a/packages/@aws-cdk/aws-events-targets/lib/api-destination.ts b/packages/@aws-cdk/aws-events-targets/lib/api-destination.ts new file mode 100644 index 0000000000000..8f2bc936a6d28 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/lib/api-destination.ts @@ -0,0 +1,98 @@ +import * as events from '@aws-cdk/aws-events'; +import * as iam from '@aws-cdk/aws-iam'; +import { addToDeadLetterQueueResourcePolicy, bindBaseTargetConfig, singletonEventRole, TargetBaseProps } from './util'; + +/** + * Customize the EventBridge Api Destinations Target + */ +export interface ApiDestinationProps extends TargetBaseProps { + /** + * The event to send + * + * @default - the entire EventBridge event + */ + readonly event?: events.RuleTargetInput; + + /** + * The role to assume before invoking the target + * + * @default - a new role will be created + */ + readonly eventRole?: iam.IRole; + + /** + * Additional headers sent to the API Destination + * + * These are merged with headers specified on the Connection, with + * the headers on the Connection taking precedence. + * + * You can only specify secret values on the Connection. + * + * @default - none + */ + readonly headerParameters?: Record; + + /** + * Path parameters to insert in place of path wildcards (`*`). + * + * If the API destination has a wilcard in the path, these path parts + * will be inserted in that place. + * + * @default - none + */ + readonly pathParameterValues?: string[] + + /** + * Additional query string parameters sent to the API Destination + * + * These are merged with headers specified on the Connection, with + * the headers on the Connection taking precedence. + * + * You can only specify secret values on the Connection. + * + * @default - none + */ + readonly queryStringParameters?: Record; +} + +/** + * Use an API Destination rule target. + */ +export class ApiDestination implements events.IRuleTarget { + constructor( + private readonly apiDestination: events.IApiDestination, + private readonly props: ApiDestinationProps = {}, + ) { } + + /** + * Returns a RuleTarget that can be used to trigger API destinations + * from an EventBridge event. + */ + public bind(_rule: events.IRule, _id?: string): events.RuleTargetConfig { + const httpParameters: events.CfnRule.HttpParametersProperty | undefined = + !!this.props.headerParameters ?? + !!this.props.pathParameterValues ?? + !!this.props.queryStringParameters + ? { + headerParameters: this.props.headerParameters, + pathParameterValues: this.props.pathParameterValues, + queryStringParameters: this.props.queryStringParameters, + } : undefined; + + if (this.props?.deadLetterQueue) { + addToDeadLetterQueueResourcePolicy(_rule, this.props.deadLetterQueue); + } + + return { + ...(this.props ? bindBaseTargetConfig(this.props) : {}), + arn: this.apiDestination.apiDestinationArn, + role: this.props?.eventRole ?? singletonEventRole(this.apiDestination, [new iam.PolicyStatement({ + resources: [this.apiDestination.apiDestinationArn], + actions: ['events:InvokeApiDestination'], + })]), + input: this.props.event, + targetResource: this.apiDestination, + httpParameters, + }; + } +} diff --git a/packages/@aws-cdk/aws-events-targets/lib/index.ts b/packages/@aws-cdk/aws-events-targets/lib/index.ts index bafb83751f468..6c91810ebca33 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/index.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/index.ts @@ -13,4 +13,5 @@ export * from './kinesis-stream'; export * from './log-group'; export * from './kinesis-firehose-stream'; export * from './api-gateway'; +export * from './api-destination'; export * from './util'; diff --git a/packages/@aws-cdk/aws-events-targets/package.json b/packages/@aws-cdk/aws-events-targets/package.json index b4e89c4043c34..650961f9757ef 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -79,17 +79,17 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "aws-sdk": "^2.848.0", - "aws-sdk-mock": "^5.5.0", - "jest": "^27.4.5" + "aws-sdk-mock": "5.6.0", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-apigateway": "0.0.0", diff --git a/packages/@aws-cdk/aws-events-targets/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-events-targets/rosetta/default.ts-fixture index f6bf67d19e31a..0de777dbbabf0 100644 --- a/packages/@aws-cdk/aws-events-targets/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/aws-events-targets/rosetta/default.ts-fixture @@ -1,5 +1,5 @@ // Fixture with packages imported, but nothing else -import { Duration, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { Duration, RemovalPolicy, SecretValue, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import * as targets from '@aws-cdk/aws-events-targets'; diff --git a/packages/@aws-cdk/aws-events-targets/test/api-destination/api-destination.test.ts b/packages/@aws-cdk/aws-events-targets/test/api-destination/api-destination.test.ts new file mode 100644 index 0000000000000..f0f29a335fbae --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/api-destination/api-destination.test.ts @@ -0,0 +1,69 @@ +import { Template } from '@aws-cdk/assertions'; +import * as events from '@aws-cdk/aws-events'; +import * as iam from '@aws-cdk/aws-iam'; +import { Duration, SecretValue, Stack } from '@aws-cdk/core'; +import * as targets from '../../lib'; + + +describe('with basic auth connection', () => { + let stack: Stack; + let connection: events.Connection; + let destination: events.ApiDestination; + let rule: events.Rule; + + beforeEach(() => { + stack = new Stack(); + connection = new events.Connection(stack, 'Connection', { + authorization: events.Authorization.basic('username', SecretValue.plainText('password')), + description: 'ConnectionDescription', + connectionName: 'testConnection', + }); + + destination = new events.ApiDestination(stack, 'Destination', { + connection, + endpoint: 'https://endpoint.com', + }); + + rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(Duration.minutes(1)), + }); + }); + + test('use api destination as an eventrule target', () => { + // WHEN + rule.addTarget(new targets.ApiDestination(destination)); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::Events::Rule', { + ScheduleExpression: 'rate(1 minute)', + State: 'ENABLED', + Targets: [ + { + Arn: { 'Fn::GetAtt': ['DestinationApiDestinationA879FAE5', 'Arn'] }, + Id: 'Target0', + RoleArn: { 'Fn::GetAtt': ['DestinationEventsRole7DA63556', 'Arn'] }, + }, + ], + }); + }); + + test('with an explicit event role', () => { + // WHEN + const eventRole = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('events.amazonaws.com'), + }); + rule.addTarget(new targets.ApiDestination(destination, { eventRole })); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::Events::Rule', { + Targets: [ + { + RoleArn: { 'Fn::GetAtt': ['Role1ABCC5F0', 'Arn'] }, + Id: 'Target0', + }, + ], + }); + }); +}); diff --git a/packages/@aws-cdk/aws-events-targets/test/api-gateway/api-gateway.test.ts b/packages/@aws-cdk/aws-events-targets/test/api-gateway/api-gateway.test.ts index 7c1033537dd41..06f4a84e3ce12 100644 --- a/packages/@aws-cdk/aws-events-targets/test/api-gateway/api-gateway.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/api-gateway/api-gateway.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as api from '@aws-cdk/aws-apigateway'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; @@ -20,7 +20,7 @@ test('use api gateway rest api as an event rule target', () => { rule.addTarget(new targets.ApiGateway(restApi)); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { @@ -81,7 +81,7 @@ test('with stage, path, method setting', () => { })); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { @@ -147,7 +147,7 @@ test('with http parameters', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { HttpParameters: { @@ -204,7 +204,7 @@ test('with an explicit event role', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { RoleArn: { @@ -234,7 +234,7 @@ test('use a Dead Letter Queue', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { DeadLetterConfig: { diff --git a/packages/@aws-cdk/aws-events-targets/test/aws-api/aws-api-handler.test.ts b/packages/@aws-cdk/aws-events-targets/test/aws-api/aws-api-handler.test.ts index e1ace923b9f61..2f3f0b269e6ff 100644 --- a/packages/@aws-cdk/aws-events-targets/test/aws-api/aws-api-handler.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/aws-api/aws-api-handler.test.ts @@ -31,10 +31,7 @@ test('calls the SDK with the right parameters', async () => { expect(updateServiceMock).toHaveBeenCalledWith({ service: 'cool-service', forceNewDeployment: true, - // NOTE - The below (representing a callback) should be included in the output. - // However, this was broken by aws-sdk-mock@5.5.0. - // See https://github.com/dwyl/aws-sdk-mock/issues/256 for more details. - }/*, expect.any(Function)*/); + }, expect.any(Function)); expect(console.log).toHaveBeenLastCalledWith('Response: %j', { success: true, diff --git a/packages/@aws-cdk/aws-events-targets/test/aws-api/aws-api.test.ts b/packages/@aws-cdk/aws-events-targets/test/aws-api/aws-api.test.ts index c605dfa64e992..cc6c58c51a43d 100644 --- a/packages/@aws-cdk/aws-events-targets/test/aws-api/aws-api.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/aws-api/aws-api.test.ts @@ -1,7 +1,7 @@ -import { countResources, expect as cdkExpect, haveResource, SynthUtils } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; -import { Stack } from '@aws-cdk/core'; +import { App, Stack } from '@aws-cdk/core'; import * as targets from '../../lib'; test('use AwsApi as an event rule target', () => { @@ -32,7 +32,7 @@ test('use AwsApi as an event rule target', () => { })); // THEN - cdkExpect(stack).to(haveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { @@ -70,12 +70,12 @@ test('use AwsApi as an event rule target', () => { }), }, ], - })); + }); // Uses a singleton function - cdkExpect(stack).to(countResources('AWS::Lambda::Function', 1)); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); - cdkExpect(stack).to(haveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -91,7 +91,7 @@ test('use AwsApi as an event rule target', () => { ], Version: '2012-10-17', }, - })); + }); }); test('with policy statement', () => { @@ -112,7 +112,7 @@ test('with policy statement', () => { })); // THEN - cdkExpect(stack).to(haveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { @@ -128,9 +128,9 @@ test('with policy statement', () => { }), }, ], - })); + }); - cdkExpect(stack).to(haveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -141,7 +141,7 @@ test('with policy statement', () => { ], Version: '2012-10-17', }, - })); + }); }); test('with service not in AWS SDK', () => { @@ -163,7 +163,7 @@ test('with service not in AWS SDK', () => { rule.addTarget(awsApi); // THEN - const assembly = SynthUtils.synthesize(stack); + const assembly = App.of(stack)!.synth().getStackArtifact(stack.stackName); expect(assembly.messages.length).toBe(1); const message = assembly.messages[0]; expect(message.entry.type).toBe('aws:cdk:warning'); diff --git a/packages/@aws-cdk/aws-events-targets/test/batch/batch.test.ts b/packages/@aws-cdk/aws-events-targets/test/batch/batch.test.ts index c379b72a523c6..66d583db4263d 100644 --- a/packages/@aws-cdk/aws-events-targets/test/batch/batch.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/batch/batch.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as batch from '@aws-cdk/aws-batch'; import { ContainerImage } from '@aws-cdk/aws-ecs'; import * as events from '@aws-cdk/aws-events'; @@ -41,7 +41,7 @@ describe('Batch job event target', () => { rule.addTarget(new targets.BatchJob(jobQueue.jobQueueArn, jobQueue, jobDefinition.jobDefinitionArn, jobDefinition)); // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { ScheduleExpression: 'rate(1 min)', State: 'ENABLED', Targets: [ @@ -62,9 +62,9 @@ describe('Batch job event target', () => { }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -81,7 +81,7 @@ describe('Batch job event target', () => { Roles: [ { Ref: 'MyJobEventsRoleCF43C336' }, ], - })); + }); }); test('use a Dead Letter Queue for the rule target', () => { @@ -108,7 +108,7 @@ describe('Batch job event target', () => { )); // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { @@ -133,9 +133,9 @@ describe('Batch job event target', () => { }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::SQS::QueuePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::QueuePolicy', { PolicyDocument: { Statement: [ { @@ -170,7 +170,7 @@ describe('Batch job event target', () => { Ref: 'Queue4A7E3555', }, ], - })); + }); }); test('specifying retry policy', () => { @@ -199,7 +199,7 @@ describe('Batch job event target', () => { )); // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { ScheduleExpression: 'rate(1 hour)', State: 'ENABLED', Targets: [ @@ -235,6 +235,6 @@ describe('Batch job event target', () => { }, }, ], - })); + }); }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts b/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts index 9ad848433a981..c0b7db6d75026 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; @@ -27,7 +27,7 @@ describe('CodeBuild event target', () => { rule.addTarget(new targets.CodeBuildProject(project)); // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: projectArn, @@ -35,9 +35,9 @@ describe('CodeBuild event target', () => { RoleArn: { 'Fn::GetAtt': ['MyProjectEventsRole5B7D93F5', 'Arn'] }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -48,9 +48,9 @@ describe('CodeBuild event target', () => { ], Version: '2012-10-17', }, - })); + }); - expect(stack).to(haveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -61,7 +61,7 @@ describe('CodeBuild event target', () => { ], Version: '2012-10-17', }, - })); + }); }); test('specifying event for codebuild project target', () => { @@ -82,7 +82,7 @@ describe('CodeBuild event target', () => { ); // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: projectArn, @@ -93,7 +93,7 @@ describe('CodeBuild event target', () => { }, }, ], - })); + }); }); test('specifying custom role for codebuild project target', () => { @@ -111,7 +111,7 @@ describe('CodeBuild event target', () => { rule.addTarget(new targets.CodeBuildProject(project, { eventRole: role })); // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: projectArn, @@ -119,7 +119,7 @@ describe('CodeBuild event target', () => { RoleArn: { 'Fn::GetAtt': ['MyRole', 'Arn'] }, }, ], - })); + }); }); test('specifying retry policy', () => { @@ -142,7 +142,7 @@ describe('CodeBuild event target', () => { ); // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { ScheduleExpression: 'rate(1 hour)', State: 'ENABLED', Targets: [ @@ -167,7 +167,7 @@ describe('CodeBuild event target', () => { }, }, ], - })); + }); }); test('use a Dead Letter Queue for the rule target', () => { @@ -191,7 +191,7 @@ describe('CodeBuild event target', () => { ); // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: projectArn, @@ -210,9 +210,9 @@ describe('CodeBuild event target', () => { }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::SQS::QueuePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::QueuePolicy', { PolicyDocument: { Statement: [ { @@ -247,6 +247,6 @@ describe('CodeBuild event target', () => { Ref: 'Queue4A7E3555', }, ], - })); + }); }); }); diff --git a/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.expected.json b/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.expected.json index 7f2c9d48da34b..912eb616ad2c8 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.expected.json @@ -83,6 +83,53 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, + "pipelinePipeline22F2A91DArtifactsBucketPolicy269103C2": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "pipelinePipeline22F2A91DArtifactsBucketC1799DCD" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "pipelinePipeline22F2A91DArtifactsBucketC1799DCD", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "pipelinePipeline22F2A91DArtifactsBucketC1799DCD", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, "pipelinePipeline22F2A91DRole58B7B05E": { "Type": "AWS::IAM::Role", "Properties": { @@ -112,6 +159,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts b/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts index 79dc7408cd4f7..aeccbee05fd7c 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; @@ -67,7 +67,7 @@ describe('CodePipeline event target', () => { }); test("adds the pipeline's ARN and role to the targets of the rule", () => { - expect(stack).to(haveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: pipelineArn, @@ -75,11 +75,11 @@ describe('CodePipeline event target', () => { RoleArn: { 'Fn::GetAtt': ['PipelineEventsRole46BEEA7C', 'Arn'] }, }, ], - })); + }); }); test("creates a policy that has StartPipeline permissions on the pipeline's ARN", () => { - expect(stack).to(haveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -90,7 +90,7 @@ describe('CodePipeline event target', () => { ], Version: '2012-10-17', }, - })); + }); }); }); @@ -106,7 +106,7 @@ describe('CodePipeline event target', () => { })); // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { ScheduleExpression: 'rate(1 minute)', State: 'ENABLED', Targets: [ @@ -155,7 +155,7 @@ describe('CodePipeline event target', () => { }, }, ], - })); + }); }); }); @@ -173,14 +173,14 @@ describe('CodePipeline event target', () => { }); test("points at the given event role in the rule's targets", () => { - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: pipelineArn, RoleArn: { 'Fn::GetAtt': ['MyRole', 'Arn'] }, }, ], - })); + }); }); }); }); diff --git a/packages/@aws-cdk/aws-events-targets/test/ecs/event-rule-target.test.ts b/packages/@aws-cdk/aws-events-targets/test/ecs/event-rule-target.test.ts index eb17b98622369..86045087ab756 100644 --- a/packages/@aws-cdk/aws-events-targets/test/ecs/event-rule-target.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/event-rule-target.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; @@ -48,7 +48,7 @@ test('Can use EC2 taskdef as EventRule target', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, @@ -119,7 +119,7 @@ test('Can import an EC2 task definition from task definition attributes as Event })); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, @@ -190,7 +190,7 @@ test('Can import a Fargate task definition from task definition attributes as Ev })); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, @@ -261,7 +261,7 @@ test('Can import a Task definition from task definition attributes as EventRule })); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, @@ -307,7 +307,7 @@ test('Can use Fargate taskdef as EventRule target', () => { // THEN expect(target.securityGroups?.length).toBeGreaterThan(0); // Generated security groups should be accessible. - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, @@ -438,7 +438,7 @@ test('Isolated subnet does not have AssignPublicIp=true', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { 'Fn::GetAtt': ['EcsCluster2F191ADEC', 'Arn'] }, @@ -529,7 +529,7 @@ test('uses multiple security groups', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, @@ -586,7 +586,7 @@ test('uses existing IAM role', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, @@ -630,7 +630,7 @@ test('uses the specific fargate platform version', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, diff --git a/packages/@aws-cdk/aws-events-targets/test/event-bus/event-rule-target.test.ts b/packages/@aws-cdk/aws-events-targets/test/event-bus/event-rule-target.test.ts index a6552a10d7a57..cb2940fdb0cd7 100644 --- a/packages/@aws-cdk/aws-events-targets/test/event-bus/event-rule-target.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/event-bus/event-rule-target.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import * as sqs from '@aws-cdk/aws-sqs'; @@ -18,7 +18,7 @@ test('Use EventBus as an event rule target', () => { ), )); - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: 'arn:aws:events:us-east-1:111111111111:default', @@ -32,7 +32,7 @@ test('Use EventBus as an event rule target', () => { }, ], }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [{ Effect: 'Allow', @@ -66,7 +66,7 @@ test('with supplied role', () => { { role }, )); - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [{ Arn: 'arn:aws:events:us-east-1:123456789012:default', Id: 'Target0', @@ -78,7 +78,7 @@ test('with supplied role', () => { }, }], }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [{ Effect: 'Allow', @@ -109,7 +109,7 @@ test('with a Dead Letter Queue specified', () => { { deadLetterQueue: queue }, )); - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [{ Arn: 'arn:aws:events:us-east-1:123456789012:default', Id: 'Target0', @@ -130,7 +130,7 @@ test('with a Dead Letter Queue specified', () => { }], }); - expect(stack).toHaveResource('AWS::SQS::QueuePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::QueuePolicy', { PolicyDocument: { Statement: [ { diff --git a/packages/@aws-cdk/aws-events-targets/test/kinesis-firehose/integ.kinesis-firehose-stream.expected.json b/packages/@aws-cdk/aws-events-targets/test/kinesis-firehose/integ.kinesis-firehose-stream.expected.json index 6b76a3d96a32e..0c7a34c7f4bd5 100644 --- a/packages/@aws-cdk/aws-events-targets/test/kinesis-firehose/integ.kinesis-firehose-stream.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/kinesis-firehose/integ.kinesis-firehose-stream.expected.json @@ -34,6 +34,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-events-targets/test/kinesis-firehose/kinesis-firehose-stream.test.ts b/packages/@aws-cdk/aws-events-targets/test/kinesis-firehose/kinesis-firehose-stream.test.ts index 6896f2c9bf934..1c0fb9ea9d88f 100644 --- a/packages/@aws-cdk/aws-events-targets/test/kinesis-firehose/kinesis-firehose-stream.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/kinesis-firehose/kinesis-firehose-stream.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as events from '@aws-cdk/aws-events'; import * as firehose from '@aws-cdk/aws-kinesisfirehose'; import { Stack } from '@aws-cdk/core'; @@ -30,7 +30,7 @@ describe('KinesisFirehoseStream event target', () => { }); test("adds the stream's ARN and role to the targets of the rule", () => { - expect(stack).to(haveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: streamArn, @@ -38,11 +38,11 @@ describe('KinesisFirehoseStream event target', () => { RoleArn: { 'Fn::GetAtt': ['MyStreamEventsRole5B6CC6AF', 'Arn'] }, }, ], - })); + }); }); test("creates a policy that has PutRecord and PutRecords permissions on the stream's ARN", () => { - expect(stack).to(haveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -53,7 +53,7 @@ describe('KinesisFirehoseStream event target', () => { ], Version: '2012-10-17', }, - })); + }); }); }); @@ -65,7 +65,7 @@ describe('KinesisFirehoseStream event target', () => { }); test('sets the input', () => { - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: streamArn, @@ -73,7 +73,7 @@ describe('KinesisFirehoseStream event target', () => { Input: '"fooBar"', }, ], - })); + }); }); }); }); diff --git a/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.expected.json b/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.expected.json index 4151890c35ef3..babfefc9b5972 100644 --- a/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.expected.json @@ -4,6 +4,9 @@ "Type": "AWS::Kinesis::Stream", "Properties": { "ShardCount": 1, + "StreamModeDetails": { + "StreamMode": "PROVISIONED" + }, "RetentionPeriodHours": 24, "StreamEncryption": { "Fn::If": [ @@ -115,4 +118,4 @@ ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-events-targets/test/kinesis/kinesis-stream.test.ts b/packages/@aws-cdk/aws-events-targets/test/kinesis/kinesis-stream.test.ts index a36e57dfa4dc2..1fb4a4785340d 100644 --- a/packages/@aws-cdk/aws-events-targets/test/kinesis/kinesis-stream.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/kinesis/kinesis-stream.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as events from '@aws-cdk/aws-events'; import * as kinesis from '@aws-cdk/aws-kinesis'; import { Stack } from '@aws-cdk/core'; @@ -30,7 +30,7 @@ describe('KinesisStream event target', () => { }); test("adds the stream's ARN and role to the targets of the rule", () => { - expect(stack).to(haveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: streamArn, @@ -38,11 +38,11 @@ describe('KinesisStream event target', () => { RoleArn: { 'Fn::GetAtt': ['MyStreamEventsRole5B6CC6AF', 'Arn'] }, }, ], - })); + }); }); test("creates a policy that has PutRecord and PutRecords permissions on the stream's ARN", () => { - expect(stack).to(haveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -53,7 +53,7 @@ describe('KinesisStream event target', () => { ], Version: '2012-10-17', }, - })); + }); }); }); @@ -65,7 +65,7 @@ describe('KinesisStream event target', () => { }); test('sets the partition key path', () => { - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: streamArn, @@ -76,7 +76,7 @@ describe('KinesisStream event target', () => { }, }, ], - })); + }); }); }); @@ -88,7 +88,7 @@ describe('KinesisStream event target', () => { }); test('sets the input', () => { - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: streamArn, @@ -96,7 +96,7 @@ describe('KinesisStream event target', () => { Input: '"fooBar"', }, ], - })); + }); }); }); }); diff --git a/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts b/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts index 5aa19af311b35..567f17e0a1fc9 100644 --- a/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as events from '@aws-cdk/aws-events'; import * as lambda from '@aws-cdk/aws-lambda'; import * as sqs from '@aws-cdk/aws-sqs'; @@ -24,7 +24,7 @@ test('use lambda as an event rule target', () => { // THEN const lambdaId = 'MyLambdaCCE802FB'; - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', FunctionName: { 'Fn::GetAtt': [ @@ -36,7 +36,7 @@ test('use lambda as an event rule target', () => { SourceArn: { 'Fn::GetAtt': ['Rule4C995B7F', 'Arn'] }, }); - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', FunctionName: { 'Fn::GetAtt': [ @@ -48,8 +48,8 @@ test('use lambda as an event rule target', () => { SourceArn: { 'Fn::GetAtt': ['Rule270732244', 'Arn'] }, }); - expect(stack).toCountResources('AWS::Events::Rule', 2); - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).resourceCountIs('AWS::Events::Rule', 2); + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { 'Fn::GetAtt': [lambdaId, 'Arn'] }, @@ -76,7 +76,7 @@ test('adding same lambda function as target mutiple times creates permission onl })); // THEN - expect(stack).toCountResources('AWS::Lambda::Permission', 1); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 1); }); test('adding different lambda functions as target mutiple times creates multiple permissions', () => { @@ -97,7 +97,7 @@ test('adding different lambda functions as target mutiple times creates multiple })); // THEN - expect(stack).toCountResources('AWS::Lambda::Permission', 2); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 2); }); test('adding same singleton lambda function as target mutiple times creates permission only once', () => { @@ -122,7 +122,7 @@ test('adding same singleton lambda function as target mutiple times creates perm })); // THEN - expect(stack).toCountResources('AWS::Lambda::Permission', 1); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 1); }); test('lambda handler and cloudwatch event across stacks', () => { @@ -145,7 +145,7 @@ test('lambda handler and cloudwatch event across stacks', () => { expect(() => app.synth()).not.toThrow(); // the Permission resource should be in the event stack - expect(eventStack).toCountResources('AWS::Lambda::Permission', 1); + Template.fromStack(eventStack).resourceCountIs('AWS::Lambda::Permission', 1); }); test('use a Dead Letter Queue for the rule target', () => { @@ -171,7 +171,7 @@ test('use a Dead Letter Queue for the rule target', () => { expect(() => app.synth()).not.toThrow(); // the Permission resource should be in the event stack - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { @@ -193,7 +193,7 @@ test('use a Dead Letter Queue for the rule target', () => { ], }); - expect(stack).toHaveResource('AWS::SQS::QueuePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::QueuePolicy', { PolicyDocument: { Statement: [ { @@ -300,7 +300,7 @@ test('must display a warning when using a Dead Letter Queue from another account expect(() => app.synth()).not.toThrow(); // the Permission resource should be in the event stack - expect(stack1).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack1).hasResourceProperties('AWS::Events::Rule', { ScheduleExpression: 'rate(1 minute)', State: 'ENABLED', Targets: [ @@ -319,7 +319,7 @@ test('must display a warning when using a Dead Letter Queue from another account ], }); - expect(stack1).not.toHaveResource('AWS::SQS::QueuePolicy'); + Template.fromStack(stack1).resourceCountIs('AWS::SQS::QueuePolicy', 0); let rule = stack1.node.children.find(child => child instanceof events.Rule); expect(rule?.node.metadataEntry[0].data).toMatch(/Cannot add a resource policy to your dead letter queue associated with rule .* because the queue is in a different account\. You must add the resource policy manually to the dead letter queue in account 222222222222\./); @@ -350,7 +350,7 @@ test('specifying retry policy', () => { expect(() => app.synth()).not.toThrow(); // the Permission resource should be in the event stack - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { ScheduleExpression: 'rate(1 minute)', State: 'ENABLED', Targets: [ diff --git a/packages/@aws-cdk/aws-events-targets/test/logs/log-group-resource-policy.test.ts b/packages/@aws-cdk/aws-events-targets/test/logs/log-group-resource-policy.test.ts index 4193c9899e3cb..bcf3309c030a1 100644 --- a/packages/@aws-cdk/aws-events-targets/test/logs/log-group-resource-policy.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/logs/log-group-resource-policy.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import { App, Stack } from '@aws-cdk/core'; import { LogGroupResourcePolicy } from '../../lib/log-group-resource-policy'; @@ -24,7 +24,7 @@ test('minimal example renders correctly', () => { })], }); - expect(stack).toHaveResource('Custom::CloudwatchLogResourcePolicy', { + Template.fromStack(stack).hasResourceProperties('Custom::CloudwatchLogResourcePolicy', { ServiceToken: { 'Fn::GetAtt': [ 'AWS679f53fac002430cb0da5b7982bd22872D164C4C', diff --git a/packages/@aws-cdk/aws-events-targets/test/logs/log-group.test.ts b/packages/@aws-cdk/aws-events-targets/test/logs/log-group.test.ts index e9c1170fe8bb0..509f09263f4fe 100644 --- a/packages/@aws-cdk/aws-events-targets/test/logs/log-group.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/logs/log-group.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as events from '@aws-cdk/aws-events'; import * as logs from '@aws-cdk/aws-logs'; import * as sqs from '@aws-cdk/aws-sqs'; @@ -20,7 +20,7 @@ test('use log group as an event rule target', () => { rule1.addTarget(new targets.CloudWatchLogGroup(logGroup)); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { ScheduleExpression: 'rate(1 minute)', State: 'ENABLED', Targets: [ @@ -72,7 +72,7 @@ test('use log group as an event rule target with rule target input', () => { })); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { ScheduleExpression: 'rate(1 minute)', State: 'ENABLED', Targets: [ @@ -135,7 +135,7 @@ test('specifying retry policy and dead letter queue', () => { })); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { ScheduleExpression: 'rate(1 minute)', State: 'ENABLED', Targets: [ diff --git a/packages/@aws-cdk/aws-events-targets/test/sns/sns.test.ts b/packages/@aws-cdk/aws-events-targets/test/sns/sns.test.ts index 11d6a2c6806bd..ac1d0dc5e740b 100644 --- a/packages/@aws-cdk/aws-events-targets/test/sns/sns.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/sns/sns.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as events from '@aws-cdk/aws-events'; import * as sns from '@aws-cdk/aws-sns'; import { Duration, Stack } from '@aws-cdk/core'; @@ -16,7 +16,7 @@ test('sns topic as an event rule target', () => { rule.addTarget(new targets.SnsTopic(topic)); // THEN - expect(stack).to(haveResource('AWS::SNS::TopicPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::SNS::TopicPolicy', { PolicyDocument: { Statement: [ { @@ -30,9 +30,9 @@ test('sns topic as an event rule target', () => { Version: '2012-10-17', }, Topics: [{ Ref: 'MyTopic86869434' }], - })); + }); - expect(stack).to(haveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { ScheduleExpression: 'rate(1 hour)', State: 'ENABLED', Targets: [ @@ -41,7 +41,7 @@ test('sns topic as an event rule target', () => { Id: 'Target0', }, ], - })); + }); }); test('multiple uses of a topic as a target results in a single policy statement', () => { @@ -58,7 +58,7 @@ test('multiple uses of a topic as a target results in a single policy statement' } // THEN - expect(stack).to(haveResource('AWS::SNS::TopicPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::SNS::TopicPolicy', { PolicyDocument: { Statement: [ { @@ -72,5 +72,5 @@ test('multiple uses of a topic as a target results in a single policy statement' Version: '2012-10-17', }, Topics: [{ Ref: 'MyTopic86869434' }], - })); + }); }); diff --git a/packages/@aws-cdk/aws-events-targets/test/sqs/sqs.test.ts b/packages/@aws-cdk/aws-events-targets/test/sqs/sqs.test.ts index ad2a5296714e6..f224a91171fcf 100644 --- a/packages/@aws-cdk/aws-events-targets/test/sqs/sqs.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/sqs/sqs.test.ts @@ -1,4 +1,4 @@ -import { expect as cdkExpect, haveResource } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as events from '@aws-cdk/aws-events'; import * as sqs from '@aws-cdk/aws-sqs'; import { Duration, Stack } from '@aws-cdk/core'; @@ -16,7 +16,7 @@ test('sqs queue as an event rule target', () => { rule.addTarget(new targets.SqsQueue(queue)); // THEN - cdkExpect(stack).to(haveResource('AWS::SQS::QueuePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::QueuePolicy', { PolicyDocument: { Statement: [ { @@ -48,9 +48,9 @@ test('sqs queue as an event rule target', () => { Version: '2012-10-17', }, Queues: [{ Ref: 'MyQueueE6CA6235' }], - })); + }); - cdkExpect(stack).to(haveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { ScheduleExpression: 'rate(1 hour)', State: 'ENABLED', Targets: [ @@ -64,7 +64,7 @@ test('sqs queue as an event rule target', () => { Id: 'Target0', }, ], - })); + }); }); test('multiple uses of a queue as a target results in multi policy statement because of condition', () => { @@ -81,7 +81,7 @@ test('multiple uses of a queue as a target results in multi policy statement bec } // THEN - cdkExpect(stack).to(haveResource('AWS::SQS::QueuePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::QueuePolicy', { PolicyDocument: { Statement: [ { @@ -138,7 +138,7 @@ test('multiple uses of a queue as a target results in multi policy statement bec Version: '2012-10-17', }, Queues: [{ Ref: 'MyQueueE6CA6235' }], - })); + }); }); test('fail if messageGroupId is specified on non-fifo queues', () => { @@ -161,7 +161,7 @@ test('fifo queues are synthesized correctly', () => { messageGroupId: 'MyMessageGroupId', })); - cdkExpect(stack).to(haveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { ScheduleExpression: 'rate(1 hour)', State: 'ENABLED', Targets: [ @@ -178,7 +178,7 @@ test('fifo queues are synthesized correctly', () => { }, }, ], - })); + }); }); test('dead letter queue is configured correctly', () => { @@ -194,7 +194,7 @@ test('dead letter queue is configured correctly', () => { deadLetterQueue, })); - cdkExpect(stack).to(haveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { ScheduleExpression: 'rate(1 hour)', State: 'ENABLED', Targets: [ @@ -216,7 +216,7 @@ test('dead letter queue is configured correctly', () => { }, }, ], - })); + }); }); test('specifying retry policy', () => { @@ -232,7 +232,7 @@ test('specifying retry policy', () => { maxEventAge: Duration.hours(2), })); - cdkExpect(stack).to(haveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { ScheduleExpression: 'rate(1 hour)', State: 'ENABLED', Targets: [ @@ -250,5 +250,5 @@ test('specifying retry policy', () => { }, }, ], - })); + }); }); diff --git a/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts b/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts index d50e41c1debce..158b19f34fa0a 100644 --- a/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import * as sqs from '@aws-cdk/aws-sqs'; @@ -22,14 +22,14 @@ test('State machine can be used as Event Rule target', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Input: '{"SomeParam":"SomeValue"}', }, ], }); - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -42,7 +42,7 @@ test('State machine can be used as Event Rule target', () => { ], }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -77,14 +77,14 @@ test('Existing role can be used for State machine Rule target', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Input: '{"SomeParam":"SomeValue"}', }, ], }); - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -97,7 +97,7 @@ test('Existing role can be used for State machine Rule target', () => { ], }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -135,7 +135,7 @@ test('specifying retry policy', () => { })); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { ScheduleExpression: 'rate(1 hour)', State: 'ENABLED', Targets: [ @@ -186,7 +186,7 @@ test('use a Dead Letter Queue for the rule target', () => { })); // the Permission resource should be in the event stack - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { ScheduleExpression: 'rate(1 minute)', State: 'ENABLED', Targets: [ @@ -214,7 +214,7 @@ test('use a Dead Letter Queue for the rule target', () => { ], }); - expect(stack).toHaveResource('AWS::SQS::QueuePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::QueuePolicy', { PolicyDocument: { Statement: [ { diff --git a/packages/@aws-cdk/aws-events/README.md b/packages/@aws-cdk/aws-events/README.md index 13bd483ca54cb..edaf54e77d244 100644 --- a/packages/@aws-cdk/aws-events/README.md +++ b/packages/@aws-cdk/aws-events/README.md @@ -153,6 +153,8 @@ The following targets are supported: * `targets.SfnStateMachine`: Trigger an AWS Step Functions state machine * `targets.BatchJob`: Queue an AWS Batch Job * `targets.AwsApi`: Make an AWS API call +* `targets.ApiGateway`: Invoke an AWS API Gateway +* `targets.ApiDestination`: Make an call to an external destination ### Cross-account and cross-region targets diff --git a/packages/@aws-cdk/aws-events/lib/api-destination.ts b/packages/@aws-cdk/aws-events/lib/api-destination.ts new file mode 100644 index 0000000000000..b3def6e064287 --- /dev/null +++ b/packages/@aws-cdk/aws-events/lib/api-destination.ts @@ -0,0 +1,107 @@ +import { IResource, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { HttpMethod, IConnection } from './connection'; +import { CfnApiDestination } from './events.generated'; + +/** + * The event API Destination properties + */ +export interface ApiDestinationProps { + /** + * The name for the API destination. + * @default - A unique name will be generated + */ + readonly apiDestinationName?: string; + + /** + * A description for the API destination. + * + * @default - none + */ + readonly description?: string; + + /** + * The ARN of the connection to use for the API destination + */ + readonly connection: IConnection; + + /** + * The URL to the HTTP invocation endpoint for the API destination.. + */ + readonly endpoint: string; + + /** + * The method to use for the request to the HTTP invocation endpoint. + * + * @default HttpMethod.POST + */ + readonly httpMethod?: HttpMethod; + + /** + * The maximum number of requests per second to send to the HTTP invocation endpoint. + * + * @default - Not rate limited + */ + readonly rateLimitPerSecond?: number; +} + +/** + * Interface for API Destinations + */ +export interface IApiDestination extends IResource { + /** + * The Name of the Api Destination created. + * @attribute + */ + readonly apiDestinationName: string; + + /** + * The ARN of the Api Destination created. + * @attribute + */ + readonly apiDestinationArn: string; +} + +/** + * Define an EventBridge Api Destination + * + * @resource AWS::Events::ApiDestination + */ +export class ApiDestination extends Resource implements IApiDestination { + /** + * The Connection to associate with Api Destination + */ + public readonly connection: IConnection; + + /** + * The Name of the Api Destination created. + * @attribute + */ + public readonly apiDestinationName: string; + + /** + * The ARN of the Api Destination created. + * @attribute + */ + public readonly apiDestinationArn: string; + + constructor(scope: Construct, id: string, props: ApiDestinationProps) { + super(scope, id, { + physicalName: props.apiDestinationName, + }); + + this.connection = props.connection; + + let apiDestination = new CfnApiDestination(this, 'ApiDestination', { + connectionArn: this.connection.connectionArn, + description: props.description, + httpMethod: props.httpMethod ?? HttpMethod.POST, + invocationEndpoint: props.endpoint, + invocationRateLimitPerSecond: props.rateLimitPerSecond, + name: this.physicalName, + }); + + this.apiDestinationName = this.getResourceNameAttribute(apiDestination.ref); + this.apiDestinationArn = apiDestination.attrArn; + } +} diff --git a/packages/@aws-cdk/aws-events/lib/connection.ts b/packages/@aws-cdk/aws-events/lib/connection.ts new file mode 100644 index 0000000000000..7eafe3b1196a7 --- /dev/null +++ b/packages/@aws-cdk/aws-events/lib/connection.ts @@ -0,0 +1,422 @@ +import { IResource, Resource, Stack, SecretValue } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnConnection } from './events.generated'; + +/** + * An API Destination Connection + * + * A connection defines the authorization type and credentials to use for authorization with an API destination HTTP endpoint. + */ +export interface ConnectionProps { + /** + * The name of the connection. + * + * @default - A name is automatically generated + */ + readonly connectionName?: string; + + /** + * The name of the connection. + * + * @default - none + */ + readonly description?: string; + + /** + * The authorization type for the connection. + */ + readonly authorization: Authorization; + + /** + * Additional string parameters to add to the invocation bodies + * + * @default - No additional parameters + */ + readonly bodyParameters?: Record; + + /** + * Additional string parameters to add to the invocation headers + * + * @default - No additional parameters + */ + readonly headerParameters?: Record; + + /** + * Additional string parameters to add to the invocation query strings + * + * @default - No additional parameters + */ + readonly queryStringParameters?: Record; +} + +/** + * Authorization type for an API Destination Connection + */ +export abstract class Authorization { + /** + * Use API key authorization + * + * API key authorization has two components: an API key name and an API key value. + * What these are depends on the target of your connection. + */ + public static apiKey(apiKeyName: string, apiKeyValue: SecretValue): Authorization { + return new class extends Authorization { + public _bind() { + return { + authorizationType: AuthorizationType.API_KEY, + authParameters: { + ApiKeyAuthParameters: { + ApiKeyName: apiKeyName, + ApiKeyValue: apiKeyValue, + }, + }, + }; + } + }(); + } + + /** + * Use username and password authorization + */ + public static basic(username: string, password: SecretValue): Authorization { + return new class extends Authorization { + public _bind() { + return { + authorizationType: AuthorizationType.BASIC, + authParameters: { + BasicAuthParameters: { + Username: username, + Password: password, + }, + }, + }; + } + }(); + } + + /** + * Use OAuth authorization + */ + public static oauth(props: OAuthAuthorizationProps): Authorization { + if (![HttpMethod.POST, HttpMethod.GET, HttpMethod.PUT].includes(props.httpMethod)) { + throw new Error('httpMethod must be one of GET, POST, PUT'); + } + + return new class extends Authorization { + public _bind() { + return { + authorizationType: AuthorizationType.OAUTH_CLIENT_CREDENTIALS, + authParameters: { + OAuthParameters: { + AuthorizationEndpoint: props.authorizationEndpoint, + ClientParameters: { + ClientID: props.clientId, + ClientSecret: props.clientSecret, + }, + HttpMethod: props.httpMethod, + OAuthHttpParameters: { + BodyParameters: renderHttpParameters(props.bodyParameters), + HeaderParameters: renderHttpParameters(props.headerParameters), + QueryStringParameters: renderHttpParameters(props.queryStringParameters), + }, + }, + }, + }; + } + }(); + + } + + /** + * Bind the authorization to the construct and return the authorization properties + * + * @internal + */ + public abstract _bind(): AuthorizationBindResult; +} + +/** + * Properties for `Authorization.oauth()` + */ +export interface OAuthAuthorizationProps { + + /** + * The URL to the authorization endpoint + */ + readonly authorizationEndpoint: string; + + /** + * The method to use for the authorization request. + * + * (Can only choose POST, GET or PUT). + */ + readonly httpMethod: HttpMethod; + + /** + * The client ID to use for OAuth authorization for the connection. + */ + readonly clientId: string; + + /** + * The client secret associated with the client ID to use for OAuth authorization for the connection. + */ + readonly clientSecret: SecretValue; + + /** + * Additional string parameters to add to the OAuth request body + * + * @default - No additional parameters + */ + readonly bodyParameters?: Record; + + /** + * Additional string parameters to add to the OAuth request header + * + * @default - No additional parameters + */ + readonly headerParameters?: Record; + + /** + * Additional string parameters to add to the OAuth request query string + * + * @default - No additional parameters + */ + readonly queryStringParameters?: Record; +} + +/** + * An additional HTTP parameter to send along with the OAuth request + */ +export abstract class HttpParameter { + /** + * Make an OAuthParameter from a string value + * + * The value is not treated as a secret. + */ + public static fromString(value: string): HttpParameter { + return new class extends HttpParameter { + public _render(name: string) { + return { + Key: name, + Value: value, + }; + } + }(); + } + + /** + * Make an OAuthParameter from a secret + */ + public static fromSecret(value: SecretValue): HttpParameter { + return new class extends HttpParameter { + public _render(name: string) { + return { + Key: name, + Value: value, + IsSecretValue: true, + }; + } + }(); + } + + /** + * Render the paramter value + * + * @internal + */ + public abstract _render(name: string): any; +} + +/** + * Result of the 'bind' operation of the 'Authorization' class + * + * @internal + */ +export interface AuthorizationBindResult { + /** + * The authorization type + */ + readonly authorizationType: AuthorizationType; + + /** + * The authorization parameters (depends on the type) + */ + readonly authParameters: any; +} + +/** + * Interface for EventBus Connections + */ +export interface IConnection extends IResource { + /** + * The Name for the connection. + * @attribute + */ + readonly connectionName: string; + + /** + * The ARN of the connection created. + * @attribute + */ + readonly connectionArn: string; + + /** + * The ARN for the secret created for the connection. + * @attribute + */ + readonly connectionSecretArn: string; +} + +/** + * Interface with properties necessary to import a reusable Connection + */ +export interface ConnectionAttributes { + /** + * The Name for the connection. + */ + readonly connectionName: string; + + /** + * The ARN of the connection created. + */ + readonly connectionArn: string; + + /** + * The ARN for the secret created for the connection. + */ + readonly connectionSecretArn: string; +} + +/** + * Define an EventBridge Connection + * + * @resource AWS::Events::Connection + */ +export class Connection extends Resource implements IConnection { + /** + * Import an existing connection resource + * @param scope Parent construct + * @param id Construct ID + * @param connectionArn ARN of imported connection + */ + public static fromEventBusArn(scope: Construct, id: string, connectionArn: string, connectionSecretArn: string): IConnection { + const parts = Stack.of(scope).parseArn(connectionArn); + + return new ImportedConnection(scope, id, { + connectionArn: connectionArn, + connectionName: parts.resourceName || '', + connectionSecretArn: connectionSecretArn, + }); + } + + /** + * Import an existing connection resource + * @param scope Parent construct + * @param id Construct ID + * @param attrs Imported connection properties + */ + public static fromConnectionAttributes(scope: Construct, id: string, attrs: ConnectionAttributes): IConnection { + return new ImportedConnection(scope, id, attrs); + } + + /** + * The Name for the connection. + * @attribute + */ + public readonly connectionName: string; + + /** + * The ARN of the connection created. + * @attribute + */ + public readonly connectionArn: string; + + /** + * The ARN for the secret created for the connection. + * @attribute + */ + public readonly connectionSecretArn: string; + + constructor(scope: Construct, id: string, props: ConnectionProps) { + super(scope, id, { + physicalName: props.connectionName, + }); + + const authBind = props.authorization._bind(); + + const invocationHttpParameters = !!props.headerParameters || !!props.queryStringParameters || !!props.bodyParameters ? { + BodyParameters: renderHttpParameters(props.bodyParameters), + HeaderParameters: renderHttpParameters(props.headerParameters), + QueryStringParameters: renderHttpParameters(props.queryStringParameters), + } : undefined; + + let connection = new CfnConnection(this, 'Connection', { + authorizationType: authBind.authorizationType, + authParameters: { + ...authBind.authParameters, + InvocationHttpParameters: invocationHttpParameters, + }, + description: props.description, + name: this.physicalName, + }); + + this.connectionName = this.getResourceNameAttribute(connection.ref); + this.connectionArn = connection.attrArn; + this.connectionSecretArn = connection.attrSecretArn; + } +} + +class ImportedConnection extends Resource { + public readonly connectionArn: string; + public readonly connectionName: string; + public readonly connectionSecretArn: string; + constructor(scope: Construct, id: string, attrs: ConnectionAttributes) { + const arnParts = Stack.of(scope).parseArn(attrs.connectionArn); + super(scope, id, { + account: arnParts.account, + region: arnParts.region, + }); + + this.connectionArn = attrs.connectionArn; + this.connectionName = attrs.connectionName; + this.connectionSecretArn = attrs.connectionSecretArn; + } +} + +/** + * Supported HTTP operations. + */ +export enum HttpMethod { + /** POST */ + POST = 'POST', + /** GET */ + GET = 'GET', + /** HEAD */ + HEAD = 'HEAD', + /** OPTIONS */ + OPTIONS = 'OPTIONS', + /** PUT */ + PUT = 'PUT', + /** PATCH */ + PATCH = 'PATCH', + /** DELETE */ + DELETE = 'DELETE', +} + +/** + * Supported Authorization Types. + */ +enum AuthorizationType { + /** API_KEY */ + API_KEY = 'API_KEY', + /** BASIC */ + BASIC = 'BASIC', + /** OAUTH_CLIENT_CREDENTIALS */ + OAUTH_CLIENT_CREDENTIALS = 'OAUTH_CLIENT_CREDENTIALS', +} + +function renderHttpParameters(ps?: Record) { + if (!ps || Object.keys(ps).length === 0) { return undefined; } + + return Object.entries(ps).map(([name, p]) => p._render(name)); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events/lib/index.ts b/packages/@aws-cdk/aws-events/lib/index.ts index 718b236bf6e91..34dcfcf792b9d 100644 --- a/packages/@aws-cdk/aws-events/lib/index.ts +++ b/packages/@aws-cdk/aws-events/lib/index.ts @@ -7,6 +7,8 @@ export * from './event-pattern'; export * from './schedule'; export * from './on-event-options'; export * from './archive'; +export * from './connection'; +export * from './api-destination'; // AWS::Events CloudFormation Resources: export * from './events.generated'; diff --git a/packages/@aws-cdk/aws-events/lib/input.ts b/packages/@aws-cdk/aws-events/lib/input.ts index 77798ceebd3a1..826aedaf4230a 100644 --- a/packages/@aws-cdk/aws-events/lib/input.ts +++ b/packages/@aws-cdk/aws-events/lib/input.ts @@ -11,8 +11,12 @@ export abstract class RuleTargetInput { /** * Pass text to the event target * - * May contain strings returned by EventField.from() to substitute in parts of the + * May contain strings returned by `EventField.from()` to substitute in parts of the * matched event. + * + * The Rule Target input value will be a single string: the string you pass + * here. Do not use this method to pass a complex value like a JSON object to + * a Rule Target. Use `RuleTargetInput.fromObject()` instead. */ public static fromText(text: string): RuleTargetInput { return new FieldAwareEventInput(text, InputType.Text); @@ -24,7 +28,7 @@ export abstract class RuleTargetInput { * This is only useful when passing to a target that does not * take a single argument. * - * May contain strings returned by EventField.from() to substitute in parts + * May contain strings returned by `EventField.from()` to substitute in parts * of the matched event. */ public static fromMultilineText(text: string): RuleTargetInput { @@ -34,7 +38,7 @@ export abstract class RuleTargetInput { /** * Pass a JSON object to the event target * - * May contain strings returned by EventField.from() to substitute in parts of the + * May contain strings returned by `EventField.from()` to substitute in parts of the * matched event. */ public static fromObject(obj: any): RuleTargetInput { diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index 19f84e8cc479c..232fd5582f9f7 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -253,13 +253,13 @@ export class Rule extends Resource implements IRule { arn: targetProps.arn, roleArn, ecsParameters: targetProps.ecsParameters, + httpParameters: targetProps.httpParameters, kinesisParameters: targetProps.kinesisParameters, runCommandParameters: targetProps.runCommandParameters, batchParameters: targetProps.batchParameters, deadLetterConfig: targetProps.deadLetterConfig, retryPolicy: targetProps.retryPolicy, sqsParameters: targetProps.sqsParameters, - httpParameters: targetProps.httpParameters, input: inputProps && inputProps.input, inputPath: inputProps && inputProps.inputPath, inputTransformer: inputProps?.inputTemplate !== undefined ? { diff --git a/packages/@aws-cdk/aws-events/lib/target.ts b/packages/@aws-cdk/aws-events/lib/target.ts index ba164f041c2a2..d90927bdaf0b0 100644 --- a/packages/@aws-cdk/aws-events/lib/target.ts +++ b/packages/@aws-cdk/aws-events/lib/target.ts @@ -66,6 +66,13 @@ export interface RuleTargetConfig { */ readonly ecsParameters?: CfnRule.EcsParametersProperty; + /** + * Contains the HTTP parameters to use when the target is a API Gateway REST endpoint + * or EventBridge API destination. + * @default - None + */ + readonly httpParameters?: CfnRule.HttpParametersProperty; + /** * Settings that control shard assignment, when the target is a Kinesis * stream. If you don't include this parameter, eventId is used as the @@ -85,11 +92,6 @@ export interface RuleTargetConfig { */ readonly sqsParameters?: CfnRule.SqsParametersProperty; - /** - * Parameters used when the rule invoke api gateway. - */ - readonly httpParameters?: CfnRule.HttpParametersProperty; - /** * What input to send to the event target * diff --git a/packages/@aws-cdk/aws-events/package.json b/packages/@aws-cdk/aws-events/package.json index 405359458cc0b..377a1c8ac9df9 100644 --- a/packages/@aws-cdk/aws-events/package.json +++ b/packages/@aws-cdk/aws-events/package.json @@ -81,12 +81,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-iam": "0.0.0", @@ -111,7 +111,8 @@ "props-default-doc:@aws-cdk/aws-events.RuleTargetConfig.role", "props-default-doc:@aws-cdk/aws-events.RuleTargetConfig.runCommandParameters", "props-default-doc:@aws-cdk/aws-events.RuleTargetConfig.sqsParameters", - "props-default-doc:@aws-cdk/aws-events.RuleTargetConfig.httpParameters" + "props-default-doc:@aws-cdk/aws-events.RuleTargetConfig.httpParameters", + "from-method:@aws-cdk/aws-events.ApiDestination" ] }, "stability": "stable", diff --git a/packages/@aws-cdk/aws-events/test/api-destination.test.ts b/packages/@aws-cdk/aws-events/test/api-destination.test.ts new file mode 100644 index 0000000000000..de0578aacf108 --- /dev/null +++ b/packages/@aws-cdk/aws-events/test/api-destination.test.ts @@ -0,0 +1,35 @@ +import { Template } from '@aws-cdk/assertions'; +import { Stack, SecretValue } from '@aws-cdk/core'; +import * as events from '../lib'; + + +test('creates an api destination for an EventBus', () => { + // GIVEN + const stack = new Stack(); + const connection = new events.Connection(stack, 'Connection', { + authorization: events.Authorization.basic('username', SecretValue.plainText('password')), + connectionName: 'testConnection', + description: 'ConnectionDescription', + }); + + // WHEN + new events.ApiDestination(stack, 'ApiDestination', { + apiDestinationName: 'ApiDestination', + connection, + description: 'ApiDestination', + httpMethod: events.HttpMethod.GET, + endpoint: 'someendpoint', + rateLimitPerSecond: 60, + }); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::Events::ApiDestination', { + ConnectionArn: { 'Fn::GetAtt': ['Connection07624BCD', 'Arn'] }, + Description: 'ApiDestination', + HttpMethod: 'GET', + InvocationEndpoint: 'someendpoint', + InvocationRateLimitPerSecond: 60, + Name: 'ApiDestination', + }); +}); diff --git a/packages/@aws-cdk/aws-events/test/archive.test.ts b/packages/@aws-cdk/aws-events/test/archive.test.ts index 0c37a0ec4ae39..8119961738cf1 100644 --- a/packages/@aws-cdk/aws-events/test/archive.test.ts +++ b/packages/@aws-cdk/aws-events/test/archive.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Duration, Stack } from '@aws-cdk/core'; import { EventBus } from '../lib'; import { Archive } from '../lib/archive'; @@ -9,7 +9,7 @@ describe('archive', () => { const stack = new Stack(); // WHEN - let eventBus = new EventBus(stack, 'Bus'); + const eventBus = new EventBus(stack, 'Bus'); new Archive(stack, 'Archive', { sourceEventBus: eventBus, @@ -20,11 +20,11 @@ describe('archive', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::EventBus', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::EventBus', { Name: 'Bus', }); - expect(stack).toHaveResource('AWS::Events::Archive', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Archive', { EventPattern: { account: [{ Ref: 'AWS::AccountId', @@ -38,15 +38,14 @@ describe('archive', () => { ], }, }); - - }); + test('creates an archive for an EventBus with a pattern including a detailType property', () => { // GIVEN const stack = new Stack(); // WHEN - let eventBus = new EventBus(stack, 'Bus'); + const eventBus = new EventBus(stack, 'Bus'); new Archive(stack, 'Archive', { sourceEventBus: eventBus, @@ -58,11 +57,11 @@ describe('archive', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::EventBus', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::EventBus', { Name: 'Bus', }); - expect(stack).toHaveResource('AWS::Events::Archive', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Archive', { EventPattern: { 'account': [{ Ref: 'AWS::AccountId', @@ -77,7 +76,5 @@ describe('archive', () => { ], }, }); - - }); }); diff --git a/packages/@aws-cdk/aws-events/test/connection.test.ts b/packages/@aws-cdk/aws-events/test/connection.test.ts new file mode 100644 index 0000000000000..bb8e3073acaf9 --- /dev/null +++ b/packages/@aws-cdk/aws-events/test/connection.test.ts @@ -0,0 +1,104 @@ +import { Template } from '@aws-cdk/assertions'; +import { SecretValue, Stack } from '@aws-cdk/core'; +import * as events from '../lib'; + +test('basic connection', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new events.Connection(stack, 'Connection', { + authorization: events.Authorization.basic('username', SecretValue.plainText('password')), + connectionName: 'testConnection', + description: 'ConnectionDescription', + }); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::Events::Connection', { + AuthorizationType: 'BASIC', + AuthParameters: { + BasicAuthParameters: { + Password: 'password', + Username: 'username', + }, + }, + Name: 'testConnection', + Description: 'ConnectionDescription', + }); +}); + +test('API key connection', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new events.Connection(stack, 'Connection', { + authorization: events.Authorization.apiKey('keyname', SecretValue.plainText('keyvalue')), + }); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::Events::Connection', { + AuthorizationType: 'API_KEY', + AuthParameters: { + ApiKeyAuthParameters: { + ApiKeyName: 'keyname', + ApiKeyValue: 'keyvalue', + }, + }, + }); +}); + +test('oauth connection', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new events.Connection(stack, 'Connection', { + authorization: events.Authorization.oauth({ + authorizationEndpoint: 'authorizationEndpoint', + clientId: 'clientID', + clientSecret: SecretValue.plainText('clientSecret'), + httpMethod: events.HttpMethod.GET, + headerParameters: { + oAuthHeaderKey: events.HttpParameter.fromString('oAuthHeaderValue'), + }, + }), + headerParameters: { + invocationHeaderKey: events.HttpParameter.fromString('invocationHeaderValue'), + }, + connectionName: 'testConnection', + description: 'ConnectionDescription', + }); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::Events::Connection', { + AuthorizationType: 'OAUTH_CLIENT_CREDENTIALS', + AuthParameters: { + OAuthParameters: { + AuthorizationEndpoint: 'authorizationEndpoint', + ClientParameters: { + ClientID: 'clientID', + ClientSecret: 'clientSecret', + }, + HttpMethod: 'GET', + OAuthHttpParameters: { + HeaderParameters: [{ + Key: 'oAuthHeaderKey', + Value: 'oAuthHeaderValue', + }], + }, + }, + InvocationHttpParameters: { + HeaderParameters: [{ + Key: 'invocationHeaderKey', + Value: 'invocationHeaderValue', + }], + }, + }, + Name: 'testConnection', + Description: 'ConnectionDescription', + }); +}); diff --git a/packages/@aws-cdk/aws-events/test/event-bus.test.ts b/packages/@aws-cdk/aws-events/test/event-bus.test.ts index 71f089a58b23c..34406986c608e 100644 --- a/packages/@aws-cdk/aws-events/test/event-bus.test.ts +++ b/packages/@aws-cdk/aws-events/test/event-bus.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import { Aws, CfnResource, Stack, Arn, App, PhysicalName, CfnOutput } from '@aws-cdk/core'; @@ -13,7 +13,7 @@ describe('event bus', () => { new EventBus(stack, 'Bus'); // THEN - expect(stack).toHaveResource('AWS::Events::EventBus', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::EventBus', { Name: 'Bus', }); }); @@ -26,7 +26,7 @@ describe('event bus', () => { new EventBus(stack, 'Bus', {}); // THEN - expect(stack).toHaveResource('AWS::Events::EventBus', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::EventBus', { Name: 'Bus', }); }); @@ -41,11 +41,9 @@ describe('event bus', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::EventBus', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::EventBus', { Name: 'myEventBus', }); - - }); test('partner event bus', () => { @@ -58,12 +56,10 @@ describe('event bus', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::EventBus', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::EventBus', { Name: 'aws.partner/PartnerName/acct1/repo1', EventSourceName: 'aws.partner/PartnerName/acct1/repo1', }); - - }); test('imported event bus', () => { @@ -82,12 +78,10 @@ describe('event bus', () => { }, }); - expect(stack).toHaveResource('Test::Resource', { + Template.fromStack(stack).hasResourceProperties('Test::Resource', { EventBusArn1: { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }, EventBusArn2: { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }, }); - - }); test('imported event bus from name', () => { @@ -99,8 +93,6 @@ describe('event bus', () => { // WHEN expect(stack.resolve(eventBus.eventBusName)).toEqual(stack.resolve(importEB.eventBusName)); - - }); test('same account imported event bus has right resource env', () => { @@ -113,8 +105,6 @@ describe('event bus', () => { // WHEN 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('cross account imported event bus has right resource env', () => { @@ -134,8 +124,6 @@ describe('event bus', () => { // WHEN expect(importEB.env.account).toEqual(arnParts.account); expect(importEB.env.region).toEqual(arnParts.region); - - }); test('can get bus name', () => { @@ -154,11 +142,9 @@ describe('event bus', () => { }); // THEN - expect(stack).toHaveResource('Test::Resource', { + Template.fromStack(stack).hasResourceProperties('Test::Resource', { EventBusName: { Ref: 'BusEA82B648' }, }); - - }); test('can get bus arn', () => { @@ -177,11 +163,9 @@ describe('event bus', () => { }); // THEN - expect(stack).toHaveResource('Test::Resource', { + Template.fromStack(stack).hasResourceProperties('Test::Resource', { EventBusArn: { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }, }); - - }); test('event bus name cannot be default', () => { @@ -197,8 +181,6 @@ describe('event bus', () => { expect(() => { createInvalidBus(); }).toThrow(/'eventBusName' must not be 'default'/); - - }); test('event bus name cannot contain slash', () => { @@ -214,8 +196,6 @@ describe('event bus', () => { expect(() => { createInvalidBus(); }).toThrow(/'eventBusName' must not contain '\/'/); - - }); test('event bus cannot have name and source name', () => { @@ -232,8 +212,6 @@ describe('event bus', () => { expect(() => { createInvalidBus(); }).toThrow(/'eventBusName' and 'eventSourceName' cannot both be provided/); - - }); test('event bus name cannot be empty string', () => { @@ -249,8 +227,6 @@ describe('event bus', () => { expect(() => { createInvalidBus(); }).toThrow(/'eventBusName' must satisfy: /); - - }); test('does not throw if eventBusName is a token', () => { @@ -261,8 +237,6 @@ describe('event bus', () => { expect(() => new EventBus(stack, 'EventBus', { eventBusName: Aws.STACK_NAME, })).not.toThrow(); - - }); test('event bus source name must follow pattern', () => { @@ -278,8 +252,6 @@ describe('event bus', () => { expect(() => { createInvalidBus(); }).toThrow(/'eventSourceName' must satisfy: \/\^aws/); - - }); test('event bus source name cannot be empty string', () => { @@ -295,8 +267,6 @@ describe('event bus', () => { expect(() => { createInvalidBus(); }).toThrow(/'eventSourceName' must satisfy: /); - - }); testDeprecated('can grant PutEvents', () => { @@ -310,7 +280,7 @@ describe('event bus', () => { EventBus.grantPutEvents(role); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -327,8 +297,6 @@ describe('event bus', () => { }, ], }); - - }); test('can grant PutEvents using grantAllPutEvents', () => { @@ -342,7 +310,7 @@ describe('event bus', () => { EventBus.grantAllPutEvents(role); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -359,9 +327,8 @@ describe('event bus', () => { }, ], }); - - }); + test('can grant PutEvents to a specific event bus', () => { // GIVEN const stack = new Stack(); @@ -375,7 +342,7 @@ describe('event bus', () => { eventBus.grantPutEventsTo(role); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -397,9 +364,8 @@ describe('event bus', () => { }, ], }); - - }); + test('can archive events', () => { // GIVEN const stack = new Stack(); @@ -415,11 +381,11 @@ describe('event bus', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::EventBus', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::EventBus', { Name: 'Bus', }); - expect(stack).toHaveResource('AWS::Events::Archive', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Archive', { SourceArn: { 'Fn::GetAtt': [ 'BusEA82B648', @@ -448,9 +414,8 @@ describe('event bus', () => { RetentionDays: 0, ArchiveName: 'MyArchive', }); - - }); + test('can archive events from an imported EventBus', () => { // GIVEN const stack = new Stack(); @@ -468,11 +433,11 @@ describe('event bus', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::EventBus', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::EventBus', { Name: 'Bus', }); - expect(stack).toHaveResource('AWS::Events::Archive', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Archive', { SourceArn: { 'Fn::GetAtt': [ 'BusEA82B648', @@ -524,9 +489,8 @@ describe('event bus', () => { RetentionDays: 0, ArchiveName: 'MyArchive', }); - - }); + test('cross account event bus uses generated physical name', () => { // GIVEN const app = new App(); @@ -551,7 +515,7 @@ describe('event bus', () => { new CfnOutput(stack2, 'BusName', { value: bus1.eventBusName }); // THEN - expect(stack1).toHaveResource('AWS::Events::EventBus', { + Template.fromStack(stack1).hasResourceProperties('AWS::Events::EventBus', { Name: 'stack1stack1busca19bdf8ab2e51b62a5a', }); }); diff --git a/packages/@aws-cdk/aws-events/test/input.test.ts b/packages/@aws-cdk/aws-events/test/input.test.ts index 76ec2cd2d2bd9..2c5536620a9fb 100644 --- a/packages/@aws-cdk/aws-events/test/input.test.ts +++ b/packages/@aws-cdk/aws-events/test/input.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { User } from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import { EventField, IRuleTarget, RuleTargetInput, Schedule } from '../lib'; @@ -17,14 +17,13 @@ describe('input', () => { rule.addTarget(new SomeTarget(RuleTargetInput.fromObject({ SomeObject: 'withAValue' }))); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Input: '{"SomeObject":"withAValue"}', }, ], }); - }); test('can use joined JSON containing refs in JSON object', () => { @@ -41,7 +40,7 @@ describe('input', () => { }))); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { InputTransformer: { @@ -62,8 +61,6 @@ describe('input', () => { }, ], }); - - }); test('can use joined JSON containing refs in JSON object with tricky inputs', () => { @@ -80,7 +77,7 @@ describe('input', () => { }))); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { InputTransformer: { @@ -101,8 +98,6 @@ describe('input', () => { }, ], }); - - }); test('can use joined JSON containing refs in JSON object and concat', () => { @@ -119,7 +114,7 @@ describe('input', () => { }))); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { InputTransformer: { @@ -140,8 +135,6 @@ describe('input', () => { }, ], }); - - }); test('can use joined JSON containing refs in JSON object and quotes', () => { @@ -158,7 +151,7 @@ describe('input', () => { }))); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { InputTransformer: { @@ -179,8 +172,6 @@ describe('input', () => { }, ], }); - - }); test('can use joined JSON containing refs in JSON object and multiple keys', () => { @@ -197,7 +188,7 @@ describe('input', () => { }))); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { InputTransformer: { @@ -218,8 +209,6 @@ describe('input', () => { }, ], }); - - }); test('can use token', () => { @@ -234,7 +223,7 @@ describe('input', () => { rule.addTarget(new SomeTarget(RuleTargetInput.fromObject({ userArn: user.userArn }))); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Input: { @@ -255,7 +244,6 @@ describe('input', () => { }, ], }); - }); }); @@ -271,15 +259,13 @@ describe('input', () => { rule.addTarget(new SomeTarget(RuleTargetInput.fromMultilineText('I have\nmultiple lines'))); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Input: '"I have"\n"multiple lines"', }, ], }); - - }); test('escaped newlines are not interpreted as newlines', () => { @@ -290,18 +276,16 @@ describe('input', () => { }); // WHEN - rule.addTarget(new SomeTarget(RuleTargetInput.fromMultilineText('this is not\\na real newline'))), + rule.addTarget(new SomeTarget(RuleTargetInput.fromMultilineText('this is not\\na real newline'))); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Input: '"this is not\\\\na real newline"', }, ], }); - - }); test('can use Tokens in text templates', () => { @@ -317,15 +301,13 @@ describe('input', () => { rule.addTarget(new SomeTarget(RuleTargetInput.fromText(`hello ${world}`))); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Input: '"hello world"', }, ], }); - - }); }); }); diff --git a/packages/@aws-cdk/aws-events/test/rule.test.ts b/packages/@aws-cdk/aws-events/test/rule.test.ts index 21782c089b40c..1cbd776441fce 100644 --- a/packages/@aws-cdk/aws-events/test/rule.test.ts +++ b/packages/@aws-cdk/aws-events/test/rule.test.ts @@ -1,5 +1,5 @@ /* eslint-disable object-curly-newline */ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import { EventBus, EventField, IRule, IRuleTarget, RuleTargetConfig, RuleTargetInput, Schedule } from '../lib'; @@ -15,7 +15,7 @@ describe('rule', () => { schedule: Schedule.rate(cdk.Duration.minutes(10)), }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyRuleA44AB831': { 'Type': 'AWS::Events::Rule', @@ -26,7 +26,6 @@ describe('rule', () => { }, }, }); - }); test('can get rule name', () => { @@ -42,11 +41,9 @@ describe('rule', () => { }, }); - expect(stack).toHaveResource('Test::Resource', { + Template.fromStack(stack).hasResourceProperties('Test::Resource', { RuleName: { Ref: 'MyRuleA44AB831' }, }); - - }); test('get rate as token', () => { @@ -60,18 +57,15 @@ describe('rule', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'Name': 'rateInMinutes', 'ScheduleExpression': 'rate(5 minutes)', }); - - }); test('Seconds is not an allowed value for Schedule rate', () => { const lazyDuration = cdk.Duration.seconds(cdk.Lazy.number({ produce: () => 5 })); expect(() => Schedule.rate(lazyDuration)).toThrow(/Allowed units for scheduling/i); - }); test('Millis is not an allowed value for Schedule rate', () => { @@ -79,7 +73,6 @@ describe('rule', () => { // THEN expect(() => Schedule.rate(lazyDuration)).toThrow(/Allowed units for scheduling/i); - }); test('rule with physical name', () => { @@ -93,11 +86,9 @@ describe('rule', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Name: 'PhysicalName', }); - - }); test('eventPattern is rendered properly', () => { @@ -119,7 +110,7 @@ describe('rule', () => { }, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyRuleA44AB831': { 'Type': 'AWS::Events::Rule', @@ -140,8 +131,6 @@ describe('rule', () => { }, }, }); - - }); test('fails synthesis if neither eventPattern nor scheudleExpression are specified', () => { @@ -149,7 +138,6 @@ describe('rule', () => { const stack = new cdk.Stack(app, 'MyStack'); new Rule(stack, 'Rule'); expect(() => app.synth()).toThrow(/Either 'eventPattern' or 'schedule' must be defined/); - }); test('addEventPattern can be used to add filters', () => { @@ -173,7 +161,7 @@ describe('rule', () => { }, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyRuleA44AB831': { 'Type': 'AWS::Events::Rule', @@ -202,7 +190,6 @@ describe('rule', () => { }, }, }); - }); test('addEventPattern can de-duplicate filters and keep the order', () => { @@ -217,7 +204,7 @@ describe('rule', () => { detailType: ['EC2 Instance State-change Notification', 'AWS API Call via CloudTrail'], }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyRuleA44AB831': { 'Type': 'AWS::Events::Rule', @@ -233,7 +220,6 @@ describe('rule', () => { }, }, }); - }); test('targets can be added via props or addTarget with input transformer', () => { @@ -261,7 +247,7 @@ describe('rule', () => { rule.addTarget(t2); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'EventRule5A491D2C': { 'Type': 'AWS::Events::Rule', @@ -291,7 +277,6 @@ describe('rule', () => { }, }, }); - }); test('input template can contain tokens', () => { @@ -337,7 +322,7 @@ describe('rule', () => { }), }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'EventRule5A491D2C': { 'Type': 'AWS::Events::Rule', @@ -378,8 +363,6 @@ describe('rule', () => { }, }, }); - - }); test('target can declare role which will be used', () => { @@ -404,7 +387,7 @@ describe('rule', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'Targets': [ { 'Arn': 'ARN2', @@ -413,8 +396,6 @@ describe('rule', () => { }, ], }); - - }); test('in cross-account scenario, target role is only used in target account', () => { @@ -442,7 +423,7 @@ describe('rule', () => { }); // THEN - expect(ruleStack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(ruleStack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { 'Fn::Join': ['', [ @@ -453,7 +434,7 @@ describe('rule', () => { }, ], }); - expect(targetStack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(targetStack).hasResourceProperties('AWS::Events::Rule', { 'Targets': [ { 'Arn': 'ARN2', @@ -462,8 +443,6 @@ describe('rule', () => { }, ], }); - - }); test('asEventRuleTarget can use the ruleArn and a uniqueId of the rule', () => { @@ -490,7 +469,6 @@ describe('rule', () => { expect(stack.resolve(receivedRuleArn)).toEqual(stack.resolve(rule.ruleArn)); expect(receivedRuleId).toEqual(cdk.Names.uniqueId(rule)); - }); test('fromEventRuleArn', () => { @@ -503,7 +481,6 @@ describe('rule', () => { // THEN expect(importedRule.ruleArn).toEqual('arn:aws:events:us-east-2:123456789012:rule/example'); expect(importedRule.ruleName).toEqual('example'); - }); test('rule can be disabled', () => { @@ -517,11 +494,9 @@ describe('rule', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'State': 'DISABLED', }); - - }); test('can add multiple targets with the same id', () => { @@ -535,7 +510,7 @@ describe('rule', () => { rule.addTarget(new SomeTarget()); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { 'Arn': 'ARN1', @@ -553,8 +528,6 @@ describe('rule', () => { }, ], }); - - }); test('sqsParameters are generated when they are specified in target props', () => { @@ -572,7 +545,7 @@ describe('rule', () => { targets: [t1], }); - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { 'Arn': 'ARN1', @@ -583,7 +556,6 @@ describe('rule', () => { }, ], }); - }); test('associate rule with event bus', () => { @@ -600,13 +572,11 @@ describe('rule', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { EventBusName: { Ref: 'EventBus7B8748AA', }, }); - - }); test('throws with eventBus and schedule', () => { @@ -620,7 +590,6 @@ describe('rule', () => { schedule: Schedule.rate(cdk.Duration.minutes(10)), eventBus, })).toThrow(/Cannot associate rule with 'eventBus' when using 'schedule'/); - }); test('allow an imported target if is in the same account and region', () => { @@ -639,7 +608,7 @@ describe('rule', () => { rule.addTarget(new SomeTarget('T', resource)); - expect(sourceStack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(sourceStack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { 'Arn': 'ARN1', @@ -650,8 +619,6 @@ describe('rule', () => { }, ], }); - - }); describe('for cross-account and/or cross-region targets', () => { @@ -668,8 +635,6 @@ describe('rule', () => { expect(() => { rule.addTarget(new SomeTarget('T', resource)); }).toThrow(/You need to provide a concrete region/); - - }); test('requires that the target stack specify a concrete account', () => { @@ -685,8 +650,6 @@ describe('rule', () => { expect(() => { rule.addTarget(new SomeTarget('T', resource)); }).toThrow(/You need to provide a concrete account for the target stack when using cross-account or cross-region events/); - - }); test('requires that the target stack specify a concrete region', () => { @@ -703,8 +666,6 @@ describe('rule', () => { expect(() => { rule.addTarget(new SomeTarget('T', resource)); }).toThrow(/You need to provide a concrete region for the target stack when using cross-account or cross-region events/); - - }); test('creates cross-account targets if in the same region', () => { @@ -726,7 +687,7 @@ describe('rule', () => { rule.addTarget(new SomeTarget('T', resource)); - expect(sourceStack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(sourceStack).hasResourceProperties('AWS::Events::Rule', { 'State': 'ENABLED', 'Targets': [ { @@ -745,7 +706,7 @@ describe('rule', () => { ], }); - expect(targetStack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(targetStack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { 'Arn': 'ARN1', @@ -756,8 +717,6 @@ describe('rule', () => { }, ], }); - - }); test('creates cross-region targets', () => { @@ -779,7 +738,7 @@ describe('rule', () => { rule.addTarget(new SomeTarget('T', resource)); - expect(sourceStack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(sourceStack).hasResourceProperties('AWS::Events::Rule', { 'State': 'ENABLED', 'Targets': [ { @@ -798,7 +757,7 @@ describe('rule', () => { ], }); - expect(targetStack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(targetStack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { 'Arn': 'ARN1', @@ -809,8 +768,6 @@ describe('rule', () => { }, ], }); - - }); test('do not create duplicated targets', () => { @@ -834,7 +791,7 @@ describe('rule', () => { // same target should be skipped rule.addTarget(new SomeTarget('T1', resource)); - expect(sourceStack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(sourceStack).hasResourceProperties('AWS::Events::Rule', { 'State': 'ENABLED', 'Targets': [ { @@ -853,7 +810,7 @@ describe('rule', () => { ], }); - expect(sourceStack).not.toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(sourceStack).hasResourceProperties('AWS::Events::Rule', Match.not({ 'State': 'ENABLED', 'Targets': [ { @@ -870,9 +827,7 @@ describe('rule', () => { }, }, ], - }); - - + })); }); test('requires that the target is not imported', () => { @@ -893,8 +848,6 @@ describe('rule', () => { expect(() => { rule.addTarget(new SomeTarget('T', resource)); }).toThrow(/Cannot create a cross-account or cross-region rule for an imported resource/); - - }); test('requires that the source and target stacks be part of the same App', () => { @@ -911,8 +864,6 @@ describe('rule', () => { expect(() => { rule.addTarget(new SomeTarget('T', resource)); }).toThrow(/Event stack and target stack must belong to the same CDK app/); - - }); test('generates the correct rules in the source and target stacks when eventPattern is passed in the constructor', () => { @@ -944,7 +895,7 @@ describe('rule', () => { rule.addTarget(new SomeTarget('T1', resource1)); rule.addTarget(new SomeTarget('T2', resource2)); - expect(sourceStack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(sourceStack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'some-event', @@ -968,7 +919,7 @@ describe('rule', () => { ], }); - expect(targetStack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(targetStack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'some-event', @@ -982,7 +933,7 @@ describe('rule', () => { }, ], }); - expect(targetStack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(targetStack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'some-event', @@ -998,13 +949,11 @@ describe('rule', () => { }); const eventBusPolicyStack = app.node.findChild(`EventBusPolicy-${sourceAccount}-us-west-2-${targetAccount}`) as cdk.Stack; - expect(eventBusPolicyStack).toHaveResourceLike('AWS::Events::EventBusPolicy', { + Template.fromStack(eventBusPolicyStack).hasResourceProperties('AWS::Events::EventBusPolicy', { 'Action': 'events:PutEvents', 'StatementId': `Allow-account-${sourceAccount}`, 'Principal': sourceAccount, }); - - }); test('generates the correct rule in the target stack when addEventPattern in the source rule is used', () => { @@ -1034,7 +983,7 @@ describe('rule', () => { source: ['some-event'], }); - expect(targetStack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(targetStack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'some-event', @@ -1048,8 +997,6 @@ describe('rule', () => { }, ], }); - - }); }); }); diff --git a/packages/@aws-cdk/aws-events/test/schedule.test.ts b/packages/@aws-cdk/aws-events/test/schedule.test.ts index d853da9ba6c30..9dea322c62d0a 100644 --- a/packages/@aws-cdk/aws-events/test/schedule.test.ts +++ b/packages/@aws-cdk/aws-events/test/schedule.test.ts @@ -8,7 +8,6 @@ describe('schedule', () => { minute: '0/10', weekDay: 'MON-FRI', }).expressionString); - }); test('cron expressions day and dow are mutex: given month day', () => { @@ -18,7 +17,6 @@ describe('schedule', () => { hour: '8', day: '1', }).expressionString); - }); test('cron expressions day and dow are mutex: given neither', () => { @@ -27,28 +25,24 @@ describe('schedule', () => { 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', () => { @@ -56,41 +50,35 @@ describe('schedule', () => { 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/util.test.ts b/packages/@aws-cdk/aws-events/test/util.test.ts index eb354ca9c48a0..b885041163316 100644 --- a/packages/@aws-cdk/aws-events/test/util.test.ts +++ b/packages/@aws-cdk/aws-events/test/util.test.ts @@ -23,21 +23,18 @@ describe('util', () => { case: [1], }, }); - }); 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'] } }); - }); 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', () => { @@ -52,7 +49,6 @@ describe('util', () => { }, }, })).toThrow(/Invalid event pattern field array. Type mismatch between existing pattern \[1\] and added pattern \{"value":\["hello"\]\}/); - }); test('deduplicate match values in pattern array', () => { @@ -90,7 +86,6 @@ describe('util', () => { 'detail-type': ['AWS API Call via CloudTrail'], 'time': [{ prefix: '2017-10-02' }, { prefix: '2017-10-03' }], }); - }); }); }); diff --git a/packages/@aws-cdk/aws-eventschemas/package.json b/packages/@aws-cdk/aws-eventschemas/package.json index 2519229df0210..42b0c59bdf000 100644 --- a/packages/@aws-cdk/aws-eventschemas/package.json +++ b/packages/@aws-cdk/aws-eventschemas/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-finspace/package.json b/packages/@aws-cdk/aws-finspace/package.json index e0ceee21687c8..cdbf1478941c4 100644 --- a/packages/@aws-cdk/aws-finspace/package.json +++ b/packages/@aws-cdk/aws-finspace/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-finspace", "module": "aws_cdk.aws_finspace" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-fis/package.json b/packages/@aws-cdk/aws-fis/package.json index 6f1c8403c703b..ca2716b5c2b54 100644 --- a/packages/@aws-cdk/aws-fis/package.json +++ b/packages/@aws-cdk/aws-fis/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-fis", "module": "aws_cdk.aws_fis" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-fms/package.json b/packages/@aws-cdk/aws-fms/package.json index 4059f9d6fd54f..5ab1866fdca94 100644 --- a/packages/@aws-cdk/aws-fms/package.json +++ b/packages/@aws-cdk/aws-fms/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-forecast/.eslintrc.js b/packages/@aws-cdk/aws-forecast/.eslintrc.js new file mode 100644 index 0000000000000..2658ee8727166 --- /dev/null +++ b/packages/@aws-cdk/aws-forecast/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-forecast/.gitignore b/packages/@aws-cdk/aws-forecast/.gitignore new file mode 100644 index 0000000000000..62ebc95d75ce6 --- /dev/null +++ b/packages/@aws-cdk/aws-forecast/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk +nyc.config.js +!.eslintrc.js +!jest.config.js +junit.xml diff --git a/packages/@aws-cdk/aws-forecast/.npmignore b/packages/@aws-cdk/aws-forecast/.npmignore new file mode 100644 index 0000000000000..f931fede67c44 --- /dev/null +++ b/packages/@aws-cdk/aws-forecast/.npmignore @@ -0,0 +1,29 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json + +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml +test/ +!*.lit.ts diff --git a/packages/@aws-cdk/aws-forecast/LICENSE b/packages/@aws-cdk/aws-forecast/LICENSE new file mode 100644 index 0000000000000..82ad00bb02d0b --- /dev/null +++ b/packages/@aws-cdk/aws-forecast/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-forecast/NOTICE b/packages/@aws-cdk/aws-forecast/NOTICE new file mode 100644 index 0000000000000..1b7adbb891265 --- /dev/null +++ b/packages/@aws-cdk/aws-forecast/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-forecast/README.md b/packages/@aws-cdk/aws-forecast/README.md new file mode 100644 index 0000000000000..5d81bb62ad1ec --- /dev/null +++ b/packages/@aws-cdk/aws-forecast/README.md @@ -0,0 +1,31 @@ +# AWS::Forecast Construct Library + + +--- + +![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) + +> All classes with the `Cfn` prefix in this module ([CFN Resources]) are always stable and safe to use. +> +> [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib + +--- + + + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +```ts nofixture +import * as forecast from '@aws-cdk/aws-forecast'; +``` + + + +There are no hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet. +However, you can still use the automatically generated [L1](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_l1_using) constructs, and use this service exactly as you would using CloudFormation directly. + +For more information on the resources and properties available for this service, see the [CloudFormation documentation for AWS::Forecast](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_Forecast.html). + +(Read the [CDK Contributing Guide](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md) if you are interested in contributing to this construct library.) + + diff --git a/packages/@aws-cdk/aws-forecast/jest.config.js b/packages/@aws-cdk/aws-forecast/jest.config.js new file mode 100644 index 0000000000000..3a2fd93a1228a --- /dev/null +++ b/packages/@aws-cdk/aws-forecast/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-forecast/lib/index.ts b/packages/@aws-cdk/aws-forecast/lib/index.ts new file mode 100644 index 0000000000000..9a67512fd1db3 --- /dev/null +++ b/packages/@aws-cdk/aws-forecast/lib/index.ts @@ -0,0 +1,2 @@ +// AWS::Forecast CloudFormation Resources: +export * from './forecast.generated'; diff --git a/packages/@aws-cdk/aws-forecast/package.json b/packages/@aws-cdk/aws-forecast/package.json new file mode 100644 index 0000000000000..f73f6e685b3f3 --- /dev/null +++ b/packages/@aws-cdk/aws-forecast/package.json @@ -0,0 +1,110 @@ +{ + "name": "@aws-cdk/aws-forecast", + "version": "0.0.0", + "description": "AWS::Forecast Construct Library", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "projectReferences": true, + "targets": { + "dotnet": { + "namespace": "Amazon.CDK.AWS.Forecast", + "packageId": "Amazon.CDK.AWS.Forecast", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "java": { + "package": "software.amazon.awscdk.services.forecast", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "forecast" + } + }, + "python": { + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 1" + ], + "distName": "aws-cdk.aws-forecast", + "module": "aws_cdk.aws_forecast" + } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-forecast" + }, + "homepage": "https://github.com/aws/aws-cdk", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "cfn2ts": "cfn2ts", + "build+test": "yarn build && yarn test", + "build+test+package": "yarn build+test && yarn package", + "compat": "cdk-compat", + "gen": "cfn2ts", + "rosetta:extract": "yarn --silent jsii-rosetta extract", + "build+extract": "yarn build && yarn rosetta:extract", + "build+test+extract": "yarn build+test && yarn rosetta:extract" + }, + "cdk-build": { + "cloudformation": "AWS::Forecast", + "jest": true, + "env": { + "AWSLINT_BASE_CONSTRUCT": "true" + } + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "AWS::Forecast", + "aws-forecast" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assertions": "0.0.0", + "@aws-cdk/cdk-build-tools": "0.0.0", + "@aws-cdk/cfn2ts": "0.0.0", + "@aws-cdk/pkglint": "0.0.0", + "@types/jest": "^27.4.1" + }, + "dependencies": { + "@aws-cdk/core": "0.0.0" + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "cfn-only", + "awscdkio": { + "announce": false + }, + "publishConfig": { + "tag": "latest" + } +} diff --git a/packages/@aws-cdk/aws-forecast/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-forecast/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..e208762bca03c --- /dev/null +++ b/packages/@aws-cdk/aws-forecast/rosetta/default.ts-fixture @@ -0,0 +1,8 @@ +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; + +class MyStack extends Stack { + constructor(scope: Construct, id: string) { + /// here + } +} diff --git a/packages/@aws-cdk/aws-codeguruprofiler/test/codeguruprofiler.test.ts b/packages/@aws-cdk/aws-forecast/test/forecast.test.ts similarity index 73% rename from packages/@aws-cdk/aws-codeguruprofiler/test/codeguruprofiler.test.ts rename to packages/@aws-cdk/aws-forecast/test/forecast.test.ts index c4505ad966984..465c7bdea0693 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/test/codeguruprofiler.test.ts +++ b/packages/@aws-cdk/aws-forecast/test/forecast.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import '@aws-cdk/assertions'; import {} from '../lib'; test('No tests are specified for this package', () => { diff --git a/packages/@aws-cdk/aws-frauddetector/package.json b/packages/@aws-cdk/aws-frauddetector/package.json index 2aa6f52f38db5..de77f2a097f9f 100644 --- a/packages/@aws-cdk/aws-frauddetector/package.json +++ b/packages/@aws-cdk/aws-frauddetector/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-frauddetector", "module": "aws_cdk.aws_frauddetector" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-fsx/README.md b/packages/@aws-cdk/aws-fsx/README.md index ebcd311266036..472d367835313 100644 --- a/packages/@aws-cdk/aws-fsx/README.md +++ b/packages/@aws-cdk/aws-fsx/README.md @@ -43,7 +43,7 @@ Linux operating system. It also provides read-after-write consistency and suppor Import to your project: -```ts +```ts nofixture import * as fsx from '@aws-cdk/aws-fsx'; ``` @@ -52,14 +52,14 @@ import * as fsx from '@aws-cdk/aws-fsx'; Setup required properties and create: ```ts -const stack = new Stack(app, 'Stack'); -const vpc = new Vpc(stack, 'VPC'); +declare const vpc: ec2.Vpc; -const fileSystem = new LustreFileSystem(stack, 'FsxLustreFileSystem', { - lustreConfiguration: { deploymentType: LustreDeploymentType.SCRATCH_2 }, +const fileSystem = new fsx.LustreFileSystem(this, 'FsxLustreFileSystem', { + lustreConfiguration: { deploymentType: fsx.LustreDeploymentType.SCRATCH_2 }, storageCapacityGiB: 1200, vpc, - vpcSubnet: vpc.privateSubnets[0]}); + vpcSubnet: vpc.privateSubnets[0], +}); ``` ### Connecting @@ -68,6 +68,9 @@ To control who can access the file system, use the `.connections` attribute. FSx need to specify the port. This example allows an EC2 instance to connect to a file system: ```ts +declare const fileSystem: fsx.LustreFileSystem; +declare const instance: ec2.Instance; + fileSystem.connections.allowDefaultPortFrom(instance); ``` @@ -78,33 +81,34 @@ used to mount the file system on an EC2 instance. The following example shows ho instance, and then use User Data to mount the file system on the instance at start-up: ```ts -const app = new App(); -const stack = new Stack(app, 'AwsCdkFsxLustre'); -const vpc = new Vpc(stack, 'VPC'); +import * as iam from '@aws-cdk/aws-iam'; +declare const vpc: ec2.Vpc; const lustreConfiguration = { - deploymentType: LustreDeploymentType.SCRATCH_2, + deploymentType: fsx.LustreDeploymentType.SCRATCH_2, }; -const fs = new LustreFileSystem(stack, 'FsxLustreFileSystem', { + +const fs = new fsx.LustreFileSystem(this, 'FsxLustreFileSystem', { lustreConfiguration, storageCapacityGiB: 1200, vpc, - vpcSubnet: vpc.privateSubnets[0]}); + vpcSubnet: vpc.privateSubnets[0], +}); -const inst = new Instance(stack, 'inst', { - instanceType: InstanceType.of(InstanceClass.T2, InstanceSize.LARGE), - machineImage: new AmazonLinuxImage({ - generation: AmazonLinuxGeneration.AMAZON_LINUX_2, +const inst = new ec2.Instance(this, 'inst', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.LARGE), + machineImage: new ec2.AmazonLinuxImage({ + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, }), vpc, vpcSubnets: { - subnetType: SubnetType.PUBLIC, + subnetType: ec2.SubnetType.PUBLIC, }, }); fs.connections.allowDefaultPortFrom(inst); // Need to give the instance access to read information about FSx to determine the file system's mount name. -inst.role.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AmazonFSxReadOnlyAccess')); +inst.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonFSxReadOnlyAccess')); const mountPath = '/mnt/fsx'; const dnsName = fs.dnsName; @@ -120,7 +124,8 @@ inst.userData.addCommands( `chown ec2-user:ec2-user ${mountPath}`, // Set the file system up to mount automatically on start up and mount it. `echo "${dnsName}@tcp:/${mountName} ${mountPath} lustre defaults,noatime,flock,_netdev 0 0" >> /etc/fstab`, - 'mount -a'); + 'mount -a', +); ``` ### Importing @@ -131,31 +136,30 @@ system, and then also import the VPC the file system is in and add an EC2 instan system. ```ts -const app = new App(); -const stack = new Stack(app, 'AwsCdkFsxLustreImport'); - -const sg = SecurityGroup.fromSecurityGroupId(stack, 'FsxSecurityGroup', '{SECURITY-GROUP-ID}'); -const fs = LustreFileSystem.fromLustreFileSystemAttributes(stack, 'FsxLustreFileSystem', { - dnsName: '{FILE-SYSTEM-DNS-NAME}' - fileSystemId: '{FILE-SYSTEM-ID}', - securityGroup: sg +const sg = ec2.SecurityGroup.fromSecurityGroupId(this, 'FsxSecurityGroup', '{SECURITY-GROUP-ID}'); +const fs = fsx.LustreFileSystem.fromLustreFileSystemAttributes(this, 'FsxLustreFileSystem', { + dnsName: '{FILE-SYSTEM-DNS-NAME}', + fileSystemId: '{FILE-SYSTEM-ID}', + securityGroup: sg, }); -const vpc = Vpc.fromVpcAttributes(stack, 'Vpc', { - availabilityZones: ['us-west-2a', 'us-west-2b'], - publicSubnetIds: ['{US-WEST-2A-SUBNET-ID}', '{US-WEST-2B-SUBNET-ID}'], - vpcId: '{VPC-ID}' +const vpc = ec2.Vpc.fromVpcAttributes(this, 'Vpc', { + availabilityZones: ['us-west-2a', 'us-west-2b'], + publicSubnetIds: ['{US-WEST-2A-SUBNET-ID}', '{US-WEST-2B-SUBNET-ID}'], + vpcId: '{VPC-ID}', }); -const inst = new Instance(stack, 'inst', { - instanceType: InstanceType.of(InstanceClass.T2, InstanceSize.LARGE), - machineImage: new AmazonLinuxImage({ - generation: AmazonLinuxGeneration.AMAZON_LINUX_2 + +const inst = new ec2.Instance(this, 'inst', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.LARGE), + machineImage: new ec2.AmazonLinuxImage({ + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, }), vpc, vpcSubnets: { - subnetType: SubnetType.PUBLIC, - } + subnetType: ec2.SubnetType.PUBLIC, + }, }); + fs.connections.allowDefaultPortFrom(inst); ``` diff --git a/packages/@aws-cdk/aws-fsx/lib/lustre-file-system.ts b/packages/@aws-cdk/aws-fsx/lib/lustre-file-system.ts index b88b3e99ac081..7b145252941cb 100644 --- a/packages/@aws-cdk/aws-fsx/lib/lustre-file-system.ts +++ b/packages/@aws-cdk/aws-fsx/lib/lustre-file-system.ts @@ -21,7 +21,12 @@ export enum LustreDeploymentType { /** * Long term storage. Data is replicated and file servers are replaced if they fail. */ - PERSISTENT_1 = 'PERSISTENT_1' + PERSISTENT_1 = 'PERSISTENT_1', + /** + * Newer type of long term storage with higher throughput tiers. + * Data is replicated and file servers are replaced if they fail. + */ + PERSISTENT_2 = 'PERSISTENT_2', } /** @@ -276,12 +281,20 @@ export class LustreFileSystem extends FileSystemBase { private validatePerUnitStorageThroughput(deploymentType: LustreDeploymentType, perUnitStorageThroughput?: number) { if (perUnitStorageThroughput === undefined) { return; } - if (deploymentType !== LustreDeploymentType.PERSISTENT_1) { - throw new Error('perUnitStorageThroughput can only be set for the PERSISTENT_1 deployment type'); + if (deploymentType !== LustreDeploymentType.PERSISTENT_1 && deploymentType !== LustreDeploymentType.PERSISTENT_2) { + throw new Error('perUnitStorageThroughput can only be set for the PERSISTENT_1/PERSISTENT_2 deployment types, received: ' + deploymentType); + } + + if (deploymentType === LustreDeploymentType.PERSISTENT_1) { + if (![50, 100, 200].includes(perUnitStorageThroughput)) { + throw new Error('perUnitStorageThroughput must be 50, 100, or 200 MB/s/TiB for PERSISTENT_1 deployment type, got: ' + perUnitStorageThroughput); + } } - if (![50, 100, 200].includes(perUnitStorageThroughput)) { - throw new Error('perUnitStorageThroughput must be 50, 100, or 200 MB/s/TiB'); + if (deploymentType === LustreDeploymentType.PERSISTENT_2) { + if (![125, 250, 500, 1000].includes(perUnitStorageThroughput)) { + throw new Error('perUnitStorageThroughput must be 125, 250, 500 or 1000 MB/s/TiB for PERSISTENT_2 deployment type, got: ' + perUnitStorageThroughput); + } } } diff --git a/packages/@aws-cdk/aws-fsx/package.json b/packages/@aws-cdk/aws-fsx/package.json index c1d2f36963dbf..0d1a669039c85 100644 --- a/packages/@aws-cdk/aws-fsx/package.json +++ b/packages/@aws-cdk/aws-fsx/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -74,12 +81,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-ec2": "0.0.0", diff --git a/packages/@aws-cdk/aws-fsx/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-fsx/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..6035663692944 --- /dev/null +++ b/packages/@aws-cdk/aws-fsx/rosetta/default.ts-fixture @@ -0,0 +1,12 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as fsx from '@aws-cdk/aws-fsx'; +import * as ec2 from '@aws-cdk/aws-ec2'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-fsx/test/lustre-file-system.test.ts b/packages/@aws-cdk/aws-fsx/test/lustre-file-system.test.ts index ad4c4462d8c58..cff4f97f1149f 100644 --- a/packages/@aws-cdk/aws-fsx/test/lustre-file-system.test.ts +++ b/packages/@aws-cdk/aws-fsx/test/lustre-file-system.test.ts @@ -1,5 +1,5 @@ import { strictEqual } from 'assert'; -import { expect as expectCDK, haveResource, ResourcePart } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import { ISubnet, Port, SecurityGroup, Subnet, Vpc } from '@aws-cdk/aws-ec2'; import { Key } from '@aws-cdk/aws-kms'; import { Aws, Stack, Token } from '@aws-cdk/core'; @@ -35,15 +35,15 @@ describe('FSx for Lustre File System', () => { vpcSubnet, }); - expectCDK(stack).to(haveResource('AWS::FSx::FileSystem')); - expectCDK(stack).to(haveResource('AWS::EC2::SecurityGroup')); + Template.fromStack(stack).hasResource('AWS::FSx::FileSystem', {}); + Template.fromStack(stack).hasResource('AWS::EC2::SecurityGroup', {}); strictEqual( fileSystem.dnsName, `${fileSystem.fileSystemId}.fsx.${stack.region}.${Aws.URL_SUFFIX}`); - expectCDK(stack).to(haveResource('AWS::FSx::FileSystem', { + Template.fromStack(stack).hasResource('AWS::FSx::FileSystem', { DeletionPolicy: 'Retain', - }, ResourcePart.CompleteDefinition)); + }); }); test('file system is created correctly when security group is provided', () => { @@ -63,8 +63,8 @@ describe('FSx for Lustre File System', () => { vpcSubnet, }); - expectCDK(stack).to(haveResource('AWS::FSx::FileSystem')); - expectCDK(stack).to(haveResource('AWS::EC2::SecurityGroup')); + Template.fromStack(stack).hasResource('AWS::FSx::FileSystem', {}); + Template.fromStack(stack).hasResource('AWS::EC2::SecurityGroup', {}); }); test('encrypted file system is created correctly with custom KMS', () => { @@ -88,11 +88,11 @@ describe('FSx for Lustre File System', () => { * in generated CDK, hence hardcoding the MD5 hash here for assertion. Assumption is that the path of the Key wont * change in this UT. Checked the unique id by generating the cloud formation stack. */ - expectCDK(stack).to(haveResource('AWS::FSx::FileSystem', { + Template.fromStack(stack).hasResourceProperties('AWS::FSx::FileSystem', { KmsKeyId: { Ref: 'customKeyFSDDB87C6D', }, - })); + }); }); test('file system is created correctly when weekly maintenance time is provided', () => { @@ -118,13 +118,13 @@ describe('FSx for Lustre File System', () => { vpcSubnet, }); - expectCDK(stack).to(haveResource('AWS::FSx::FileSystem', { + Template.fromStack(stack).hasResourceProperties('AWS::FSx::FileSystem', { LustreConfiguration: { DeploymentType: 'SCRATCH_2', WeeklyMaintenanceStartTime: '7:12:34', }, - })); - expectCDK(stack).to(haveResource('AWS::EC2::SecurityGroup')); + }); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', {}); }); describe('when validating props', () => { @@ -145,13 +145,13 @@ describe('FSx for Lustre File System', () => { vpcSubnet, }); - expectCDK(stack).to(haveResource('AWS::FSx::FileSystem', { + Template.fromStack(stack).hasResourceProperties('AWS::FSx::FileSystem', { LustreConfiguration: { DeploymentType: LustreDeploymentType.SCRATCH_2, ExportPath: exportPath, ImportPath: importPath, }, - })); + }); }); test('export and import paths are Tokens', () => { @@ -172,13 +172,13 @@ describe('FSx for Lustre File System', () => { vpcSubnet, }); - expectCDK(stack).to(haveResource('AWS::FSx::FileSystem', { + Template.fromStack(stack).hasResourceProperties('AWS::FSx::FileSystem', { LustreConfiguration: { DeploymentType: LustreDeploymentType.SCRATCH_2, ExportPath: exportPathResolved, ImportPath: importPathResolved, }, - })); + }); }); test('only export path is Token', () => { @@ -299,12 +299,12 @@ describe('FSx for Lustre File System', () => { vpcSubnet, }); - expectCDK(stack).to(haveResource('AWS::FSx::FileSystem', { + Template.fromStack(stack).hasResourceProperties('AWS::FSx::FileSystem', { LustreConfiguration: { DeploymentType: LustreDeploymentType.SCRATCH_2, ImportedFileChunkSize: size, }, - })); + }); }); test.each([ @@ -342,12 +342,12 @@ describe('FSx for Lustre File System', () => { vpcSubnet, }); - expectCDK(stack).to(haveResource('AWS::FSx::FileSystem', { + Template.fromStack(stack).hasResourceProperties('AWS::FSx::FileSystem', { LustreConfiguration: { DeploymentType: LustreDeploymentType.SCRATCH_2, ImportPath: importPath, }, - })); + }); }); test('import path is Token', () => { @@ -364,12 +364,12 @@ describe('FSx for Lustre File System', () => { vpcSubnet, }); - expectCDK(stack).to(haveResource('AWS::FSx::FileSystem', { + Template.fromStack(stack).hasResourceProperties('AWS::FSx::FileSystem', { LustreConfiguration: { DeploymentType: LustreDeploymentType.SCRATCH_2, ImportPath: importPathResolved, }, - })); + }); }); test('invalid import path format', () => { @@ -428,18 +428,24 @@ describe('FSx for Lustre File System', () => { vpcSubnet, }); - expectCDK(stack).to(haveResource('AWS::FSx::FileSystem', { + Template.fromStack(stack).hasResourceProperties('AWS::FSx::FileSystem', { LustreConfiguration: { DeploymentType: LustreDeploymentType.PERSISTENT_1, PerUnitStorageThroughput: throughput, }, - })); + }); }); - test('invalid perUnitStorageThroughput', () => { + test.each([ + 1, + 125, + 250, + 500, + 1000, + ])('invalid perUnitStorageThroughput', (invalidValue: number) => { lustreConfiguration = { deploymentType: LustreDeploymentType.PERSISTENT_1, - perUnitStorageThroughput: 1, + perUnitStorageThroughput: invalidValue, }; expect(() => { @@ -449,7 +455,7 @@ describe('FSx for Lustre File System', () => { vpc, vpcSubnet, }); - }).toThrowError('perUnitStorageThroughput must be 50, 100, or 200 MB/s/TiB'); + }).toThrowError('perUnitStorageThroughput must be 50, 100, or 200 MB/s/TiB for PERSISTENT_1 deployment type, got: ' + invalidValue); }); test('setting perUnitStorageThroughput on wrong deploymentType', () => { @@ -465,7 +471,57 @@ describe('FSx for Lustre File System', () => { vpc, vpcSubnet, }); - }).toThrowError('perUnitStorageThroughput can only be set for the PERSISTENT_1 deployment type'); + }).toThrowError('perUnitStorageThroughput can only be set for the PERSISTENT_1/PERSISTENT_2 deployment types'); + }); + }); + + describe('perUnitStorageThroughput_Persistent_2', () => { + test.each([ + 125, + 250, + 500, + 1000, + ])('valid perUnitStorageThroughput of %d', (throughput: number) => { + lustreConfiguration = { + deploymentType: LustreDeploymentType.PERSISTENT_2, + perUnitStorageThroughput: throughput, + }; + + new LustreFileSystem(stack, 'FsxFileSystem', { + lustreConfiguration, + storageCapacityGiB: storageCapacity, + vpc, + vpcSubnet, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::FSx::FileSystem', { + LustreConfiguration: { + DeploymentType: LustreDeploymentType.PERSISTENT_2, + PerUnitStorageThroughput: throughput, + }, + }); + }); + + test.each([ + 1, + 50, + 100, + 200, + 550, + ])('invalid perUnitStorageThroughput', (invalidValue: number) => { + lustreConfiguration = { + deploymentType: LustreDeploymentType.PERSISTENT_2, + perUnitStorageThroughput: invalidValue, + }; + + expect(() => { + new LustreFileSystem(stack, 'FsxFileSystem', { + lustreConfiguration, + storageCapacityGiB: storageCapacity, + vpc, + vpcSubnet, + }); + }).toThrowError('perUnitStorageThroughput must be 125, 250, 500 or 1000 MB/s/TiB for PERSISTENT_2 deployment type, got: ' + invalidValue); }); }); @@ -477,6 +533,9 @@ describe('FSx for Lustre File System', () => { [1200, LustreDeploymentType.PERSISTENT_1], [2400, LustreDeploymentType.PERSISTENT_1], [4800, LustreDeploymentType.PERSISTENT_1], + [1200, LustreDeploymentType.PERSISTENT_2], + [2400, LustreDeploymentType.PERSISTENT_2], + [4800, LustreDeploymentType.PERSISTENT_2], ])('proper multiple for storage capacity of %d on %s', (value: number, deploymentType: LustreDeploymentType) => { lustreConfiguration = { deploymentType, @@ -489,12 +548,12 @@ describe('FSx for Lustre File System', () => { vpcSubnet, }); - expectCDK(stack).to(haveResource('AWS::FSx::FileSystem', { + Template.fromStack(stack).hasResourceProperties('AWS::FSx::FileSystem', { LustreConfiguration: { DeploymentType: deploymentType, }, StorageCapacity: value, - })); + }); }); test.each([ @@ -502,6 +561,8 @@ describe('FSx for Lustre File System', () => { [2401, LustreDeploymentType.SCRATCH_2], [1, LustreDeploymentType.PERSISTENT_1], [2401, LustreDeploymentType.PERSISTENT_1], + [1, LustreDeploymentType.PERSISTENT_2], + [2401, LustreDeploymentType.PERSISTENT_2], ])('invalid value of %d for storage capacity on %s', (invalidValue: number, deploymentType: LustreDeploymentType) => { lustreConfiguration = { deploymentType, @@ -529,12 +590,12 @@ describe('FSx for Lustre File System', () => { vpcSubnet, }); - expectCDK(stack).to(haveResource('AWS::FSx::FileSystem', { + Template.fromStack(stack).hasResourceProperties('AWS::FSx::FileSystem', { LustreConfiguration: { DeploymentType: LustreDeploymentType.SCRATCH_1, }, StorageCapacity: validValue, - })); + }); }); test.each([1, 3601])('invalid value of %d for storage capacity with SCRATCH_1', (invalidValue: number) => { @@ -566,8 +627,8 @@ describe('FSx for Lustre File System', () => { fs.connections.allowToAnyIpv4(Port.tcp(443)); - expectCDK(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: 'sg-123456789', - })); + }); }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/package.json b/packages/@aws-cdk/aws-gamelift/package.json index 183269e840777..401b1c4375cd7 100644 --- a/packages/@aws-cdk/aws-gamelift/package.json +++ b/packages/@aws-cdk/aws-gamelift/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -76,7 +83,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-globalaccelerator-endpoints/package.json b/packages/@aws-cdk/aws-globalaccelerator-endpoints/package.json index 244b676b1fd7a..ff31bbcb08cee 100644 --- a/packages/@aws-cdk/aws-globalaccelerator-endpoints/package.json +++ b/packages/@aws-cdk/aws-globalaccelerator-endpoints/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -69,14 +76,14 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "aws-sdk": "^2.848.0", - "aws-sdk-mock": "^5.5.0", - "jest": "^27.4.5" + "aws-sdk-mock": "5.6.0", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-ec2": "0.0.0", diff --git a/packages/@aws-cdk/aws-globalaccelerator-endpoints/test/endpoints.test.ts b/packages/@aws-cdk/aws-globalaccelerator-endpoints/test/endpoints.test.ts index 01b55508f11a0..c241c8ccd6646 100644 --- a/packages/@aws-cdk/aws-globalaccelerator-endpoints/test/endpoints.test.ts +++ b/packages/@aws-cdk/aws-globalaccelerator-endpoints/test/endpoints.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as ga from '@aws-cdk/aws-globalaccelerator'; @@ -32,7 +32,7 @@ test('Application Load Balancer with all properties', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::GlobalAccelerator::EndpointGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::GlobalAccelerator::EndpointGroup', { EndpointConfigurations: [ { EndpointId: { Ref: 'ALBAEE750D2' }, @@ -57,7 +57,7 @@ test('Get region from imported ALB', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::GlobalAccelerator::EndpointGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::GlobalAccelerator::EndpointGroup', { EndpointGroupRegion: 'us-west-2', EndpointConfigurations: [ { @@ -79,7 +79,7 @@ test('Network Load Balancer with all properties', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::GlobalAccelerator::EndpointGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::GlobalAccelerator::EndpointGroup', { EndpointConfigurations: [ { EndpointId: { Ref: 'NLB55158F82' }, @@ -102,7 +102,7 @@ test('Get region from imported NLB', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::GlobalAccelerator::EndpointGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::GlobalAccelerator::EndpointGroup', { EndpointGroupRegion: 'us-west-2', EndpointConfigurations: [ { @@ -124,7 +124,7 @@ test('CFN EIP with all properties', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::GlobalAccelerator::EndpointGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::GlobalAccelerator::EndpointGroup', { EndpointConfigurations: [ { EndpointId: { 'Fn::GetAtt': ['ElasticIpAddress', 'AllocationId'] }, @@ -151,7 +151,7 @@ test('EC2 Instance with all properties', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::GlobalAccelerator::EndpointGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::GlobalAccelerator::EndpointGroup', { EndpointConfigurations: [ { EndpointId: { Ref: 'InstanceC1063A87' }, diff --git a/packages/@aws-cdk/aws-globalaccelerator/README.md b/packages/@aws-cdk/aws-globalaccelerator/README.md index bdbb6780fadd2..d0805df646d65 100644 --- a/packages/@aws-cdk/aws-globalaccelerator/README.md +++ b/packages/@aws-cdk/aws-globalaccelerator/README.md @@ -32,12 +32,8 @@ Here's an example that sets up a Global Accelerator for two Application Load Balancers in two different AWS Regions: ```ts -import globalaccelerator = require('@aws-cdk/aws-globalaccelerator'); -import ga_endpoints = require('@aws-cdk/aws-globalaccelerator-endpoints'); -import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); - // Create an Accelerator -const accelerator = new globalaccelerator.Accelerator(stack, 'Accelerator'); +const accelerator = new globalaccelerator.Accelerator(this, 'Accelerator'); // Create a Listener const listener = accelerator.addListener('Listener', { @@ -48,10 +44,10 @@ const listener = accelerator.addListener('Listener', { }); // Import the Load Balancers -const nlb1 = elbv2.NetworkLoadBalancer.fromNetworkLoadBalancerAttributes(stack, 'NLB1', { +const nlb1 = elbv2.NetworkLoadBalancer.fromNetworkLoadBalancerAttributes(this, 'NLB1', { loadBalancerArn: 'arn:aws:elasticloadbalancing:us-west-2:111111111111:loadbalancer/app/my-load-balancer1/e16bef66805b', }); -const nlb2 = elbv2.NetworkLoadBalancer.fromNetworkLoadBalancerAttributes(stack, 'NLB2', { +const nlb2 = elbv2.NetworkLoadBalancer.fromNetworkLoadBalancerAttributes(this, 'NLB2', { loadBalancerArn: 'arn:aws:elasticloadbalancing:ap-south-1:111111111111:loadbalancer/app/my-load-balancer2/5513dc2ea8a1', }); @@ -95,7 +91,8 @@ There are 4 types of Endpoints, and they can be found in the ### Application Load Balancers ```ts -const alb = new elbv2.ApplicationLoadBalancer(...); +declare const alb: elbv2.ApplicationLoadBalancer; +declare const listener: globalaccelerator.Listener; listener.addEndpointGroup('Group', { endpoints: [ @@ -110,7 +107,8 @@ listener.addEndpointGroup('Group', { ### Network Load Balancers ```ts -const nlb = new elbv2.NetworkLoadBalancer(...); +declare const nlb: elbv2.NetworkLoadBalancer; +declare const listener: globalaccelerator.Listener; listener.addEndpointGroup('Group', { endpoints: [ @@ -124,7 +122,8 @@ listener.addEndpointGroup('Group', { ### EC2 Instances ```ts -const instance = new ec2.instance(...); +declare const listener: globalaccelerator.Listener; +declare const instance: ec2.Instance; listener.addEndpointGroup('Group', { endpoints: [ @@ -139,7 +138,8 @@ listener.addEndpointGroup('Group', { ### Elastic IP Addresses ```ts -const eip = new ec2.CfnEIP(...); +declare const listener: globalaccelerator.Listener; +declare const eip: ec2.CfnEIP; listener.addEndpointGroup('Group', { endpoints: [ @@ -170,22 +170,23 @@ which you can use in connection rules. You must pass a reference to the VPC in w context the security group will be looked up. Example: ```ts -// ... +declare const listener: globalaccelerator.Listener; // Non-open ALB -const alb = new elbv2.ApplicationLoadBalancer(stack, 'ALB', { /* ... */ }); +declare const alb: elbv2.ApplicationLoadBalancer; const endpointGroup = listener.addEndpointGroup('Group', { endpoints: [ new ga_endpoints.ApplicationLoadBalancerEndpoint(alb, { - preserveClientIps: true, - })], + preserveClientIp: true, + }), ], }); // Remember that there is only one AGA security group per VPC. +declare const vpc: ec2.Vpc; const agaSg = endpointGroup.connectionsPeer('GlobalAcceleratorSG', vpc); // Allow connections from the AGA to the ALB -alb.connections.allowFrom(agaSg, Port.tcp(443)); +alb.connections.allowFrom(agaSg, ec2.Port.tcp(443)); ``` diff --git a/packages/@aws-cdk/aws-globalaccelerator/package.json b/packages/@aws-cdk/aws-globalaccelerator/package.json index 60cf16e7f10bc..10223d0d40b55 100644 --- a/packages/@aws-cdk/aws-globalaccelerator/package.json +++ b/packages/@aws-cdk/aws-globalaccelerator/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -74,13 +81,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-ec2": "0.0.0", diff --git a/packages/@aws-cdk/aws-globalaccelerator/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-globalaccelerator/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..fab068d6fb540 --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/rosetta/default.ts-fixture @@ -0,0 +1,14 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as globalaccelerator from '@aws-cdk/aws-globalaccelerator'; +import * as ga_endpoints from '@aws-cdk/aws-globalaccelerator-endpoints'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; +import * as ec2 from '@aws-cdk/aws-ec2'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator-security-group.test.ts b/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator-security-group.test.ts index 7191988ed2ed6..dfa4ea9a918bc 100644 --- a/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator-security-group.test.ts +++ b/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator-security-group.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, ResourcePart } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ga from '../lib'; import { testFixture } from './util'; @@ -22,7 +22,7 @@ test('custom resource exists', () => { endpointGroup.connectionsPeer('GlobalAcceleratorSG', vpc); // THEN - expect(stack).to(haveResource('Custom::AWS', { + Template.fromStack(stack).hasResource('Custom::AWS', { Properties: { ServiceToken: { 'Fn::GetAtt': [ @@ -48,7 +48,7 @@ test('custom resource exists', () => { 'GroupGlobalAcceleratorSGCustomResourceCustomResourcePolicy9C957AD2', 'GroupC77FDACD', ], - }, ResourcePart.CompleteDefinition)); + }); }); test('can create security group rule', () => { @@ -73,7 +73,7 @@ test('can create security group rule', () => { instanceConnections.allowFrom(gaSg, ec2.Port.tcp(443)); // THEN - expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { IpProtocol: 'tcp', FromPort: 443, GroupId: { @@ -89,5 +89,5 @@ test('can create security group rule', () => { ], }, ToPort: 443, - })); + }); }); diff --git a/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator.test.ts b/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator.test.ts index bd46c697ac1c3..94296978b50b6 100644 --- a/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator.test.ts +++ b/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import { Duration } from '@aws-cdk/core'; import * as ga from '../lib'; import { testFixture } from './util'; @@ -11,9 +11,9 @@ test('create accelerator', () => { new ga.Accelerator(stack, 'Accelerator'); // THEN - expect(stack).to(haveResourceLike('AWS::GlobalAccelerator::Accelerator', { + Template.fromStack(stack).hasResourceProperties('AWS::GlobalAccelerator::Accelerator', { Enabled: true, - })); + }); }); test('create listener', () => { @@ -33,7 +33,7 @@ test('create listener', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::GlobalAccelerator::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::GlobalAccelerator::Listener', { AcceleratorArn: { 'Fn::GetAtt': [ 'Accelerator8EB0B6B1', @@ -48,7 +48,7 @@ test('create listener', () => { ], Protocol: 'TCP', ClientAffinity: 'NONE', - })); + }); }); test('toPort defaults to fromPort if left out', () => { @@ -64,14 +64,14 @@ test('toPort defaults to fromPort if left out', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::GlobalAccelerator::Listener', { + Template.fromStack(stack).hasResourceProperties('AWS::GlobalAccelerator::Listener', { PortRanges: [ { FromPort: 123, ToPort: 123, }, ], - })); + }); }); test('create endpointgroup', () => { @@ -92,7 +92,7 @@ test('create endpointgroup', () => { new ga.EndpointGroup(stack, 'Group', { listener }); // THEN - expect(stack).to(haveResourceLike('AWS::GlobalAccelerator::EndpointGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::GlobalAccelerator::EndpointGroup', { EndpointGroupRegion: { Ref: 'AWS::Region', }, @@ -102,7 +102,7 @@ test('create endpointgroup', () => { 'ListenerArn', ], }, - })); + }); }); test('endpointgroup region is the first endpoint\'s region', () => { @@ -125,9 +125,9 @@ test('endpointgroup region is the first endpoint\'s region', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::GlobalAccelerator::EndpointGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::GlobalAccelerator::EndpointGroup', { EndpointGroupRegion: 'us-bla-5', - })); + }); }); test('endpointgroup with all parameters', () => { @@ -156,7 +156,7 @@ test('endpointgroup with all parameters', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::GlobalAccelerator::EndpointGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::GlobalAccelerator::EndpointGroup', { EndpointGroupRegion: 'us-bla-5', HealthCheckIntervalSeconds: 10, HealthCheckPath: '/ping', @@ -170,7 +170,7 @@ test('endpointgroup with all parameters', () => { ], ThresholdCount: 23, TrafficDialPercentage: 86, - })); + }); }); test('addEndpoint', () => { @@ -201,7 +201,7 @@ test('addEndpoint', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::GlobalAccelerator::EndpointGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::GlobalAccelerator::EndpointGroup', { EndpointConfigurations: [ { EndpointId: 'i-123', @@ -209,5 +209,5 @@ test('addEndpoint', () => { Weight: 30, }, ], - })); + }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-glue/package.json b/packages/@aws-cdk/aws-glue/package.json index 99d837bdb1fb9..29c190ddeca63 100644 --- a/packages/@aws-cdk/aws-glue/package.json +++ b/packages/@aws-cdk/aws-glue/package.json @@ -85,8 +85,8 @@ "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/assets": "0.0.0", diff --git a/packages/@aws-cdk/aws-glue/test/integ.job.expected.json b/packages/@aws-cdk/aws-glue/test/integ.job.expected.json index 61f4f60434db1..d50a9de59a00e 100644 --- a/packages/@aws-cdk/aws-glue/test/integ.job.expected.json +++ b/packages/@aws-cdk/aws-glue/test/integ.job.expected.json @@ -43,6 +43,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-glue/test/integ.table.expected.json b/packages/@aws-cdk/aws-glue/test/integ.table.expected.json index 32fda38f59b0f..c76cbb5544660 100644 --- a/packages/@aws-cdk/aws-glue/test/integ.table.expected.json +++ b/packages/@aws-cdk/aws-glue/test/integ.table.expected.json @@ -482,6 +482,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -559,6 +563,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -671,6 +679,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-glue/test/table.test.ts b/packages/@aws-cdk/aws-glue/test/table.test.ts index a7aa71474724f..e3f5df9bb6a3f 100644 --- a/packages/@aws-cdk/aws-glue/test/table.test.ts +++ b/packages/@aws-cdk/aws-glue/test/table.test.ts @@ -1281,6 +1281,10 @@ describe('grants', () => { Action: [ 's3:DeleteObject*', 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', 's3:Abort*', ], Effect: 'Allow', @@ -1393,6 +1397,10 @@ describe('grants', () => { 's3:List*', 's3:DeleteObject*', 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', 's3:Abort*', ], Effect: 'Allow', diff --git a/packages/@aws-cdk/aws-greengrass/package.json b/packages/@aws-cdk/aws-greengrass/package.json index 039b0f7b121c0..8677a50cafc70 100644 --- a/packages/@aws-cdk/aws-greengrass/package.json +++ b/packages/@aws-cdk/aws-greengrass/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-greengrassv2/package.json b/packages/@aws-cdk/aws-greengrassv2/package.json index 84617dc1706c6..4456cac21c5ff 100644 --- a/packages/@aws-cdk/aws-greengrassv2/package.json +++ b/packages/@aws-cdk/aws-greengrassv2/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-groundstation/package.json b/packages/@aws-cdk/aws-groundstation/package.json index 579d1cbbdf63f..a6850e0bcd71a 100644 --- a/packages/@aws-cdk/aws-groundstation/package.json +++ b/packages/@aws-cdk/aws-groundstation/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-groundstation", "module": "aws_cdk.aws_groundstation" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-guardduty/package.json b/packages/@aws-cdk/aws-guardduty/package.json index ce3691441d42e..fd8c3f540cdd6 100644 --- a/packages/@aws-cdk/aws-guardduty/package.json +++ b/packages/@aws-cdk/aws-guardduty/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -76,7 +83,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-healthlake/package.json b/packages/@aws-cdk/aws-healthlake/package.json index b6afc75d8b64a..3c6e25ae6d523 100644 --- a/packages/@aws-cdk/aws-healthlake/package.json +++ b/packages/@aws-cdk/aws-healthlake/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-healthlake", "module": "aws_cdk.aws_healthlake" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-iam/README.md b/packages/@aws-cdk/aws-iam/README.md index 438b54693b5b5..73f55df549756 100644 --- a/packages/@aws-cdk/aws-iam/README.md +++ b/packages/@aws-cdk/aws-iam/README.md @@ -202,7 +202,7 @@ const principal = new iam.AccountPrincipal('123456789000') > NOTE: If you need to define an IAM condition that uses a token (such as a > deploy-time attribute of another resource) in a JSON map key, use `CfnJson` to -> render this condition. See [this test](./test/integ-condition-with-ref.ts) for +> render this condition. See [this test](./test/integ.condition-with-ref.ts) for > an example. The `WebIdentityPrincipal` class can be used as a principal for web identities like @@ -457,6 +457,27 @@ const user = iam.User.fromUserAttributes(this, 'MyImportedUserByAttributes', { }); ``` +### Access Keys + +The ability for a user to make API calls via the CLI or an SDK is enabled by the user having an +access key pair. To create an access key: + +```ts +const user = new iam.User(this, 'MyUser'); +const accessKey = new iam.AccessKey(this, 'MyAccessKey', { user: user }); +``` + +You can force CloudFormation to rotate the access key by providing a monotonically increasing `serial` +property. Simply provide a higher serial value than any number used previously: + +```ts +const user = new iam.User(this, 'MyUser'); +const accessKey = new iam.AccessKey(this, 'MyAccessKey', { user: user, serial: 1 }); +``` + +An access key may only be associated with a single user and cannot be "moved" between users. Changing +the user associated with an access key replaces the access key (and its ID and secret value). + ## Groups An IAM user group is a collection of IAM users. User groups let you specify permissions for multiple users. diff --git a/packages/@aws-cdk/aws-iam/lib/access-key.ts b/packages/@aws-cdk/aws-iam/lib/access-key.ts new file mode 100644 index 0000000000000..259e46c67a572 --- /dev/null +++ b/packages/@aws-cdk/aws-iam/lib/access-key.ts @@ -0,0 +1,93 @@ +import { IResource, Resource, SecretValue } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnAccessKey } from './iam.generated'; +import { IUser } from './user'; + +/** + * Valid statuses for an IAM Access Key. + */ +export enum AccessKeyStatus { + /** + * An active access key. An active key can be used to make API calls. + */ + ACTIVE = 'Active', + + /** + * An inactive access key. An inactive key cannot be used to make API calls. + */ + INACTIVE = 'Inactive' +} + +/** + * Represents an IAM Access Key. + * + * @see https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html + */ +export interface IAccessKey extends IResource { + /** + * The Access Key ID. + * + * @attribute + */ + readonly accessKeyId: string; + + /** + * The Secret Access Key. + * + * @attribute + */ + readonly secretAccessKey: SecretValue; +} + +/** + * Properties for defining an IAM access key. + */ +export interface AccessKeyProps { + /** + * A CloudFormation-specific value that signifies the access key should be + * replaced/rotated. This value can only be incremented. Incrementing this + * value will cause CloudFormation to replace the Access Key resource. + * + * @default - No serial value + */ + readonly serial?: number; + + /** + * The status of the access key. An Active access key is allowed to be used + * to make API calls; An Inactive key cannot. + * + * @default - The access key is active + */ + readonly status?: AccessKeyStatus; + + /** + * The IAM user this key will belong to. + * + * Changing this value will result in the access key being deleted and a new + * access key (with a different ID and secret value) being assigned to the new + * user. + */ + readonly user: IUser; +} + +/** + * Define a new IAM Access Key. + */ +export class AccessKey extends Resource implements IAccessKey { + public readonly accessKeyId: string; + public readonly secretAccessKey: SecretValue; + + constructor(scope: Construct, id: string, props: AccessKeyProps) { + super(scope, id); + const accessKey = new CfnAccessKey(this, 'Resource', { + userName: props.user.userName, + serial: props.serial, + status: props.status, + }); + + this.accessKeyId = accessKey.ref; + + // Not actually 'plainText', but until we have a more apt constructor + this.secretAccessKey = SecretValue.plainText(accessKey.attrSecretAccessKey); + } +} diff --git a/packages/@aws-cdk/aws-iam/lib/index.ts b/packages/@aws-cdk/aws-iam/lib/index.ts index 06c2a9bb6cdcd..7b13245b49f39 100644 --- a/packages/@aws-cdk/aws-iam/lib/index.ts +++ b/packages/@aws-cdk/aws-iam/lib/index.ts @@ -13,6 +13,7 @@ export * from './unknown-principal'; export * from './oidc-provider'; export * from './permissions-boundary'; export * from './saml-provider'; +export * from './access-key'; // AWS::IAM CloudFormation Resources: export * from './iam.generated'; diff --git a/packages/@aws-cdk/aws-iam/lib/role.ts b/packages/@aws-cdk/aws-iam/lib/role.ts index ac48fdd3d7710..73ba42c051800 100644 --- a/packages/@aws-cdk/aws-iam/lib/role.ts +++ b/packages/@aws-cdk/aws-iam/lib/role.ts @@ -274,6 +274,21 @@ export class Role extends Resource implements IRole { : new ImmutableRole(scope, `ImmutableRole${id}`, importedRole, options.addGrantsToResources ?? false); } + /** + * Import an external role by name. + * + * The imported role is assumed to exist in the same account as the account + * the scope's containing Stack is being deployed to. + */ + public static fromRoleName(scope: Construct, id: string, roleName: string) { + return Role.fromRoleArn(scope, id, Stack.of(scope).formatArn({ + region: '', + service: 'iam', + resource: 'role', + resourceName: roleName, + })); + } + public readonly grantPrincipal: IPrincipal = this; public readonly principalAccount: string | undefined = this.env.account; diff --git a/packages/@aws-cdk/aws-iam/package.json b/packages/@aws-cdk/aws-iam/package.json index bb376f95e3bab..5a405fbce243d 100644 --- a/packages/@aws-cdk/aws-iam/package.json +++ b/packages/@aws-cdk/aws-iam/package.json @@ -79,16 +79,15 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/aws-lambda": "^8.10.89", - "@types/jest": "^27.0.3", + "@types/aws-lambda": "^8.10.92", + "@types/jest": "^27.4.1", "@types/sinon": "^9.0.11", - "jest": "^27.4.5", + "jest": "^27.5.1", "sinon": "^9.2.4" }, "dependencies": { @@ -108,9 +107,11 @@ "awslint": { "exclude": [ "from-signature:@aws-cdk/aws-iam.Role.fromRoleArn", + "from-method:@aws-cdk/aws-iam.AccessKey", "construct-interface-extends-iconstruct:@aws-cdk/aws-iam.IManagedPolicy", "props-physical-name:@aws-cdk/aws-iam.OpenIdConnectProviderProps", "props-physical-name:@aws-cdk/aws-iam.SamlProviderProps", + "props-physical-name:@aws-cdk/aws-iam.AccessKeyProps", "resource-interface-extends-resource:@aws-cdk/aws-iam.IManagedPolicy", "docs-public-apis:@aws-cdk/aws-iam.IUser" ] diff --git a/packages/@aws-cdk/aws-iam/test/access-key.test.ts b/packages/@aws-cdk/aws-iam/test/access-key.test.ts new file mode 100644 index 0000000000000..c63bfa7d739c2 --- /dev/null +++ b/packages/@aws-cdk/aws-iam/test/access-key.test.ts @@ -0,0 +1,79 @@ +import { Template } from '@aws-cdk/assertions'; +import { App, Stack } from '@aws-cdk/core'; +import { AccessKey, AccessKeyStatus, User } from '../lib'; + +describe('IAM Access keys', () => { + test('user name is identifed via reference', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'MyStack'); + const user = new User(stack, 'MyUser'); + + // WHEN + new AccessKey(stack, 'MyAccessKey', { user }); + + // THEN + Template.fromStack(stack).templateMatches({ + Resources: { + MyUserDC45028B: { + Type: 'AWS::IAM::User', + }, + MyAccessKeyF0FFBE2E: { + Type: 'AWS::IAM::AccessKey', + Properties: { + UserName: { Ref: 'MyUserDC45028B' }, + }, + }, + }, + }); + }); + + test('active status is specified with correct capitalization', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'MyStack'); + const user = new User(stack, 'MyUser'); + + // WHEN + new AccessKey(stack, 'MyAccessKey', { user, status: AccessKeyStatus.ACTIVE }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::AccessKey', { Status: 'Active' }); + }); + + test('inactive status is specified with correct capitalization', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'MyStack'); + const user = new User(stack, 'MyUser'); + + // WHEN + new AccessKey(stack, 'MyAccessKey', { + user, + status: AccessKeyStatus.INACTIVE, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::AccessKey', { + Status: 'Inactive', + }); + }); + + test('access key secret ', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'MyStack'); + const user = new User(stack, 'MyUser'); + + // WHEN + const accessKey = new AccessKey(stack, 'MyAccessKey', { + user, + }); + + // THEN + expect(stack.resolve(accessKey.secretAccessKey)).toStrictEqual({ + 'Fn::GetAtt': ['MyAccessKeyF0FFBE2E', 'SecretAccessKey'], + }); + }); + +}); diff --git a/packages/@aws-cdk/aws-iam/test/auto-cross-stack-refs.test.ts b/packages/@aws-cdk/aws-iam/test/auto-cross-stack-refs.test.ts index 1f8384c600fb9..4ede7daf57355 100644 --- a/packages/@aws-cdk/aws-iam/test/auto-cross-stack-refs.test.ts +++ b/packages/@aws-cdk/aws-iam/test/auto-cross-stack-refs.test.ts @@ -1,5 +1,4 @@ -import { SynthUtils } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as iam from '../lib'; @@ -25,7 +24,7 @@ describe('automatic cross-stack references', () => { // // THEN - expect(stackWithUser).toMatchTemplate({ + Template.fromStack(stackWithUser).templateMatches({ Resources: { User00B015A1: { Type: 'AWS::IAM::User', @@ -35,7 +34,7 @@ describe('automatic cross-stack references', () => { }, }, }); - expect(stackWithGroup).toMatchTemplate({ + Template.fromStack(stackWithGroup).templateMatches({ Outputs: { ExportsOutputRefGroupC77FDACD8CF7DD5B: { Value: { Ref: 'GroupC77FDACD' }, @@ -59,6 +58,6 @@ describe('automatic cross-stack references', () => { group.addUser(user); // THEN - expect(() => SynthUtils.synthesize(stack1)).toThrow(/Cannot reference across apps/); + expect(() => cdk.App.of(stack1)!.synth()).toThrow(/Cannot reference across apps/); }); }); diff --git a/packages/@aws-cdk/aws-iam/test/cross-account.test.ts b/packages/@aws-cdk/aws-iam/test/cross-account.test.ts index 21ca3ce48c945..12eef2860ce1e 100644 --- a/packages/@aws-cdk/aws-iam/test/cross-account.test.ts +++ b/packages/@aws-cdk/aws-iam/test/cross-account.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as constructs from 'constructs'; import * as iam from '../lib'; @@ -191,7 +191,7 @@ function doGrant(resource: FakeResource, principal: iam.IPrincipal) { } function assertTrustCreated(stack: cdk.Stack, principal: any) { - expect(stack).toHaveResource('Test::Fake::Resource', { + Template.fromStack(stack).hasResourceProperties('Test::Fake::Resource', { ResourcePolicy: { Statement: [ { @@ -207,17 +207,17 @@ function assertTrustCreated(stack: cdk.Stack, principal: any) { } function noTrustCreated(stack: cdk.Stack) { - expect(stack).not.toHaveResourceLike('Test::Fake::Resource', { + expect(Template.fromStack(stack).findResources('Test::Fake::Resource', { ResourcePolicy: { Statement: [ {}, ], }, - }); + })).toEqual({}); } function assertPolicyCreated(stack: cdk.Stack) { - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -232,5 +232,5 @@ function assertPolicyCreated(stack: cdk.Stack) { } function noPolicyCreated(stack: cdk.Stack) { - expect(stack).not.toHaveResource('AWS::IAM::Policy'); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 0); } diff --git a/packages/@aws-cdk/aws-iam/test/escape-hatch.test.ts b/packages/@aws-cdk/aws-iam/test/escape-hatch.test.ts index b9467638c7307..f22639c83c447 100644 --- a/packages/@aws-cdk/aws-iam/test/escape-hatch.test.ts +++ b/packages/@aws-cdk/aws-iam/test/escape-hatch.test.ts @@ -1,7 +1,7 @@ // tests for the L1 escape hatches (overrides). those are in the IAM module // because we want to verify them end-to-end, as a complement to the unit // tests in the @aws-cdk/core module -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import * as iam from '../lib'; @@ -17,7 +17,7 @@ describe('IAM escape hatches', () => { const cfn = user.node.findChild('Resource') as iam.CfnUser; cfn.addPropertyOverride('UserName', 'OverriddenUserName'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'user2C2B57AE': { 'Type': 'AWS::IAM::User', @@ -39,7 +39,7 @@ describe('IAM escape hatches', () => { cfn.addPropertyOverride('Hello.World', 'Boom'); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'user2C2B57AE': { 'Type': 'AWS::IAM::User', @@ -69,7 +69,7 @@ describe('IAM escape hatches', () => { cfn.addOverride('UpdatePolicy.UseOnlineResharding.Type', 'None'); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'user2C2B57AE': { 'Type': 'AWS::IAM::User', diff --git a/packages/@aws-cdk/aws-iam/test/grant.test.ts b/packages/@aws-cdk/aws-iam/test/grant.test.ts index 04b2466ea21da..1a4ca30016d6c 100644 --- a/packages/@aws-cdk/aws-iam/test/grant.test.ts +++ b/packages/@aws-cdk/aws-iam/test/grant.test.ts @@ -1,5 +1,4 @@ -import { ResourcePart } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { CfnResource, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import * as iam from '../lib'; @@ -109,9 +108,9 @@ function applyGrantWithDependencyTo(principal: iam.IPrincipal) { } function expectDependencyOn(id: string) { - expect(stack).toHaveResource('CDK::Test::SomeResource', (props: any) => { + Template.fromStack(stack).hasResource('CDK::Test::SomeResource', (props: any) => { return (props?.DependsOn ?? []).includes(id); - }, ResourcePart.CompleteDefinition); + }); } class FakeResourceWithPolicy extends Resource implements iam.IResourceWithPolicy { diff --git a/packages/@aws-cdk/aws-iam/test/group.test.ts b/packages/@aws-cdk/aws-iam/test/group.test.ts index 6fc7129a3a1d9..781d2b2ece0b5 100644 --- a/packages/@aws-cdk/aws-iam/test/group.test.ts +++ b/packages/@aws-cdk/aws-iam/test/group.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { App, Stack } from '@aws-cdk/core'; import { Group, ManagedPolicy, User } from '../lib'; @@ -8,7 +8,7 @@ describe('IAM groups', () => { const stack = new Stack(app, 'MyStack'); new Group(stack, 'MyGroup'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyGroupCBA54B1B: { Type: 'AWS::IAM::Group' } }, }); }); @@ -22,7 +22,7 @@ describe('IAM groups', () => { user1.addToGroup(group); group.addUser(user2); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyGroupCBA54B1B: { Type: 'AWS::IAM::Group' }, @@ -50,7 +50,7 @@ describe('IAM groups', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Group', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Group', { ManagedPolicyArns: [ { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::aws:policy/asdf']] }, ], diff --git a/packages/@aws-cdk/aws-iam/test/immutable-role.test.ts b/packages/@aws-cdk/aws-iam/test/immutable-role.test.ts index 3ba5c870f8659..12cb38dc9542b 100644 --- a/packages/@aws-cdk/aws-iam/test/immutable-role.test.ts +++ b/packages/@aws-cdk/aws-iam/test/immutable-role.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import * as iam from '../lib'; @@ -33,7 +33,7 @@ describe('ImmutableRole', () => { immutableRole.attachInlinePolicy(policy); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -58,7 +58,7 @@ describe('ImmutableRole', () => { immutableRole.addManagedPolicy({ managedPolicyArn: 'Arn2' }); - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { 'ManagedPolicyArns': [ 'Arn1', ], @@ -76,7 +76,7 @@ describe('ImmutableRole', () => { actions: ['s3:*'], })); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Version': '2012-10-17', 'Statement': [ @@ -98,7 +98,7 @@ describe('ImmutableRole', () => { resourceArns: ['*'], }); - expect(stack).not.toHaveResourceLike('AWS::IAM::Policy', { + expect(Template.fromStack(stack).findResources('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -108,7 +108,7 @@ describe('ImmutableRole', () => { }, ], }, - }); + })).toEqual({}); }); // this pattern is used here: diff --git a/packages/@aws-cdk/aws-iam/test/integ.access-key.expected.json b/packages/@aws-cdk/aws-iam/test/integ.access-key.expected.json new file mode 100644 index 0000000000000..d0d33a2ebefb3 --- /dev/null +++ b/packages/@aws-cdk/aws-iam/test/integ.access-key.expected.json @@ -0,0 +1,22 @@ +{ + "Resources": { + "TestUser6A619381": { + "Type": "AWS::IAM::User" + }, + "TestAccessKey4BFC5CF5": { + "Type": "AWS::IAM::AccessKey", + "Properties": { + "UserName": { + "Ref": "TestUser6A619381" + } + } + } + }, + "Outputs": { + "AccessKeyId": { + "Value": { + "Ref": "TestAccessKey4BFC5CF5" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/integ.access-key.ts b/packages/@aws-cdk/aws-iam/test/integ.access-key.ts new file mode 100644 index 0000000000000..65d229ed2b500 --- /dev/null +++ b/packages/@aws-cdk/aws-iam/test/integ.access-key.ts @@ -0,0 +1,12 @@ +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { AccessKey, User } from '../lib'; + +const app = new App(); +const stack = new Stack(app, 'integ-iam-access-key-1'); + +const user = new User(stack, 'TestUser'); +const accessKey = new AccessKey(stack, 'TestAccessKey', { user }); + +new CfnOutput(stack, 'AccessKeyId', { value: accessKey.accessKeyId }); + +app.synth(); diff --git a/packages/@aws-cdk/aws-iam/test/lazy-role.test.ts b/packages/@aws-cdk/aws-iam/test/lazy-role.test.ts index 34d8919ccd14c..a21baf9fb5963 100644 --- a/packages/@aws-cdk/aws-iam/test/lazy-role.test.ts +++ b/packages/@aws-cdk/aws-iam/test/lazy-role.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as iam from '../lib'; @@ -13,7 +13,7 @@ describe('IAM lazy role', () => { }); // THEN - expect(stack).not.toHaveResource('AWS::IAM::Role'); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 0); }); test('creates the resource when a property is read', () => { @@ -27,7 +27,7 @@ describe('IAM lazy role', () => { // THEN expect(roleArn).not.toBeNull(); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Version: '2012-10-17', Statement: [{ diff --git a/packages/@aws-cdk/aws-iam/test/managed-policy.test.ts b/packages/@aws-cdk/aws-iam/test/managed-policy.test.ts index 9ed969b4a0afe..718915956d29b 100644 --- a/packages/@aws-cdk/aws-iam/test/managed-policy.test.ts +++ b/packages/@aws-cdk/aws-iam/test/managed-policy.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import { Group, ManagedPolicy, PolicyDocument, PolicyStatement, Role, ServicePrincipal, User } from '../lib'; @@ -49,7 +49,7 @@ describe('managed policy', () => { const group = new Group(stack, 'MyGroup'); group.addManagedPolicy(policy); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyManagedPolicy9F3720AE: { Type: 'AWS::IAM::ManagedPolicy', @@ -89,7 +89,7 @@ describe('managed policy', () => { }), }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyManagedPolicy9F3720AE: { Type: 'AWS::IAM::ManagedPolicy', @@ -120,7 +120,7 @@ describe('managed policy', () => { statements: [new PolicyStatement({ resources: ['arn'], actions: ['sns:Subscribe'] })], }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyManagedPolicy9F3720AE: { Type: 'AWS::IAM::ManagedPolicy', @@ -148,7 +148,7 @@ describe('managed policy', () => { const group = new Group(stack, 'MyGroup'); group.addManagedPolicy(policy); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyManagedPolicy9F3720AE: { Type: 'AWS::IAM::ManagedPolicy', @@ -192,7 +192,7 @@ describe('managed policy', () => { statements: [new PolicyStatement({ resources: ['*'], actions: ['dynamodb:PutItem'] })], }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { User1E278A736: { Type: 'AWS::IAM::User' }, Group1BEBD4686: { Type: 'AWS::IAM::Group' }, @@ -248,7 +248,7 @@ describe('managed policy', () => { p.attachToRole(role); p.attachToRole(role); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyManagedPolicy9F3720AE: { Type: 'AWS::IAM::ManagedPolicy', @@ -295,7 +295,7 @@ describe('managed policy', () => { p.attachToRole(new Role(stack, 'Role1', { assumedBy: new ServicePrincipal('test.service') })); p.addStatements(new PolicyStatement({ resources: ['*'], actions: ['dynamodb:GetItem'] })); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyManagedPolicy9F3720AE: { Type: 'AWS::IAM::ManagedPolicy', @@ -346,7 +346,7 @@ describe('managed policy', () => { policy.addStatements(new PolicyStatement({ resources: ['*'], actions: ['*'] })); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyManagedPolicy9F3720AE: { Type: 'AWS::IAM::ManagedPolicy', @@ -390,7 +390,7 @@ describe('managed policy', () => { group.addManagedPolicy(policy); role.addManagedPolicy(policy); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyUserDC45028B: { Type: 'AWS::IAM::User', @@ -466,7 +466,7 @@ describe('managed policy', () => { group.addManagedPolicy(policy); role.addManagedPolicy(policy); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyUserDC45028B: { Type: 'AWS::IAM::User', @@ -594,7 +594,7 @@ describe('managed policy', () => { value: mp.managedPolicyArn, }); - expect(stack2).toMatchTemplate({ + Template.fromStack(stack2).templateMatches({ Outputs: { Output: { Value: { diff --git a/packages/@aws-cdk/aws-iam/test/oidc-provider.test.ts b/packages/@aws-cdk/aws-iam/test/oidc-provider.test.ts index 805e7bbb66ac0..eda196a97aa5f 100644 --- a/packages/@aws-cdk/aws-iam/test/oidc-provider.test.ts +++ b/packages/@aws-cdk/aws-iam/test/oidc-provider.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { App, Stack, Token } from '@aws-cdk/core'; import * as sinon from 'sinon'; import * as iam from '../lib'; @@ -20,7 +20,7 @@ describe('OpenIdConnectProvider resource', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDKOpenIdConnectProvider', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDKOpenIdConnectProvider', { Url: 'https://openid-endpoint', }); }); @@ -61,7 +61,7 @@ describe('OpenIdConnectProvider resource', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDKOpenIdConnectProvider', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDKOpenIdConnectProvider', { Url: 'https://my-url', ClientIDList: ['client1', 'client2'], ThumbprintList: ['thumb1'], @@ -103,7 +103,7 @@ describe('custom resource provider infrastructure', () => { new iam.OpenIdConnectProvider(stack, 'Provider1', { url: 'provider1' }); // THEN - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { Policies: [ { PolicyName: 'Inline', diff --git a/packages/@aws-cdk/aws-iam/test/permissions-boundary.test.ts b/packages/@aws-cdk/aws-iam/test/permissions-boundary.test.ts index ab304f3481a15..b30137798074f 100644 --- a/packages/@aws-cdk/aws-iam/test/permissions-boundary.test.ts +++ b/packages/@aws-cdk/aws-iam/test/permissions-boundary.test.ts @@ -1,6 +1,5 @@ import * as path from 'path'; -import { ABSENT } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import { App, CfnResource, CustomResourceProvider, CustomResourceProviderRuntime, Stack } from '@aws-cdk/core'; import * as iam from '../lib'; @@ -21,7 +20,7 @@ test('apply imported boundary to a role', () => { iam.PermissionsBoundary.of(role).apply(iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess')); // THEN - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { PermissionsBoundary: { 'Fn::Join': ['', [ 'arn:', @@ -40,7 +39,7 @@ test('apply imported boundary to a user', () => { iam.PermissionsBoundary.of(user).apply(iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess')); // THEN - expect(stack).toHaveResource('AWS::IAM::User', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::User', { PermissionsBoundary: { 'Fn::Join': ['', [ 'arn:', @@ -68,7 +67,7 @@ test('apply newly created boundary to a role', () => { })); // THEN - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { PermissionsBoundary: { Ref: 'Policy23B91518' }, }); }); @@ -91,7 +90,7 @@ test('apply boundary to role created by a custom resource', () => { })); // THEN - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { PermissionsBoundary: { Ref: 'Policy23B91518' }, }); }); @@ -113,7 +112,7 @@ test('apply boundary to users created via CfnResource', () => { })); // THEN - expect(stack).toHaveResource('AWS::IAM::User', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::User', { PermissionsBoundary: { Ref: 'Policy23B91518' }, }); }); @@ -135,7 +134,7 @@ test('apply boundary to roles created via CfnResource', () => { })); // THEN - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { PermissionsBoundary: { Ref: 'Policy23B91518' }, }); }); @@ -149,8 +148,8 @@ test('unapply inherited boundary from a user: order 1', () => { iam.PermissionsBoundary.of(user).clear(); // THEN - expect(stack).toHaveResource('AWS::IAM::User', { - PermissionsBoundary: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::IAM::User', { + PermissionsBoundary: Match.absent(), }); }); @@ -163,7 +162,7 @@ test('unapply inherited boundary from a user: order 2', () => { iam.PermissionsBoundary.of(stack).apply(iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess')); // THEN - expect(stack).toHaveResource('AWS::IAM::User', { - PermissionsBoundary: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::IAM::User', { + PermissionsBoundary: Match.absent(), }); }); diff --git a/packages/@aws-cdk/aws-iam/test/policy-document.test.ts b/packages/@aws-cdk/aws-iam/test/policy-document.test.ts index 16a9904087647..ca759d438ab89 100644 --- a/packages/@aws-cdk/aws-iam/test/policy-document.test.ts +++ b/packages/@aws-cdk/aws-iam/test/policy-document.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import { Lazy, Stack, Token } from '@aws-cdk/core'; import { @@ -492,7 +492,7 @@ describe('IAM policy document', () => { ), }); - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -568,7 +568,7 @@ describe('IAM policy document', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { diff --git a/packages/@aws-cdk/aws-iam/test/policy-statement.test.ts b/packages/@aws-cdk/aws-iam/test/policy-statement.test.ts index 09b6e30b1e4c0..7498ee2814d80 100644 --- a/packages/@aws-cdk/aws-iam/test/policy-statement.test.ts +++ b/packages/@aws-cdk/aws-iam/test/policy-statement.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import { Stack } from '@aws-cdk/core'; import { AnyPrincipal, Group, PolicyDocument, PolicyStatement } from '../lib'; diff --git a/packages/@aws-cdk/aws-iam/test/policy.test.ts b/packages/@aws-cdk/aws-iam/test/policy.test.ts index cb6c2fc88cf52..ea40450756935 100644 --- a/packages/@aws-cdk/aws-iam/test/policy.test.ts +++ b/packages/@aws-cdk/aws-iam/test/policy.test.ts @@ -1,5 +1,4 @@ -import { ResourcePart } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { App, CfnResource, Stack } from '@aws-cdk/core'; import { AnyPrincipal, CfnPolicy, Group, Policy, PolicyDocument, PolicyStatement, Role, ServicePrincipal, User } from '../lib'; @@ -28,7 +27,7 @@ describe('IAM policy', () => { const group = new Group(stack, 'MyGroup'); group.attachInlinePolicy(policy); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyPolicy39D66CF6: @@ -69,7 +68,7 @@ describe('IAM policy', () => { const group = new Group(stack, 'MyGroup'); group.attachInlinePolicy(policy); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyPolicy39D66CF6: { Type: 'AWS::IAM::Policy', @@ -96,7 +95,7 @@ describe('IAM policy', () => { const user = new User(stack, 'MyUser'); user.attachInlinePolicy(policy); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyPolicy39D66CF6: @@ -135,7 +134,7 @@ describe('IAM policy', () => { statements: [new PolicyStatement({ resources: ['*'], actions: ['dynamodb:PutItem'] })], }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { User1E278A736: { Type: 'AWS::IAM::User' }, @@ -186,7 +185,7 @@ describe('IAM policy', () => { p.attachToUser(user); p.attachToUser(user); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyPolicy39D66CF6: @@ -219,7 +218,7 @@ describe('IAM policy', () => { p.attachToRole(new Role(stack, 'Role1', { assumedBy: new ServicePrincipal('test.service') })); p.addStatements(new PolicyStatement({ resources: ['*'], actions: ['dynamodb:GetItem'] })); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyTestPolicy316BDB50: @@ -275,7 +274,7 @@ describe('IAM policy', () => { policy.addStatements(new PolicyStatement({ resources: ['*'], actions: ['*'] })); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyPolicy39D66CF6: @@ -349,7 +348,7 @@ describe('IAM policy', () => { test("generated policy name is the same as the logical id if it's shorter than 128 characters", () => { createPolicyWithLogicalId(stack, 'Foo'); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyName': 'Foo', }); }); @@ -360,7 +359,7 @@ describe('IAM policy', () => { createPolicyWithLogicalId(stack, logicalIdOver128); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyName': logicalId128, }); @@ -385,7 +384,7 @@ describe('IAM policy', () => { res.node.addDependency(pol); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { Resource: { Type: 'Some::Resource', @@ -411,10 +410,10 @@ describe('IAM policy', () => { res.node.addDependency(pol); // THEN - expect(stack).toHaveResource('Some::Resource', { + Template.fromStack(stack).hasResource('Some::Resource', { Type: 'Some::Resource', DependsOn: ['Pol0FE9AD5D'], - }, ResourcePart.CompleteDefinition); + }); }); test('empty policy is OK if force=false', () => { diff --git a/packages/@aws-cdk/aws-iam/test/principals.test.ts b/packages/@aws-cdk/aws-iam/test/principals.test.ts index fdbeaa0dc87d4..7a07a50e80fb9 100644 --- a/packages/@aws-cdk/aws-iam/test/principals.test.ts +++ b/packages/@aws-cdk/aws-iam/test/principals.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import { Template } from '@aws-cdk/assertions'; import { App, CfnOutput, Stack } from '@aws-cdk/core'; import * as iam from '../lib'; @@ -21,7 +20,7 @@ test('use of cross-stack role reference does not lead to URLSuffix being exporte // THEN app.synth(); - expect(first).toMatchTemplate({ + Template.fromStack(first).templateMatches({ Resources: { Role1ABCC5F0: { Type: 'AWS::IAM::Role', @@ -145,7 +144,7 @@ test('SAML principal', () => { // THEN expect(stack.resolve(principal.federated)).toStrictEqual({ Ref: 'MyProvider730BA1C8' }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -214,7 +213,7 @@ test('PrincipalWithConditions.addCondition should work', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -277,7 +276,7 @@ test('Can enable session tags', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { diff --git a/packages/@aws-cdk/aws-iam/test/role.from-role-arn.test.ts b/packages/@aws-cdk/aws-iam/test/role.from-role-arn.test.ts index 68e36b388fa0f..05a0fdaac4d58 100644 --- a/packages/@aws-cdk/aws-iam/test/role.from-role-arn.test.ts +++ b/packages/@aws-cdk/aws-iam/test/role.from-role-arn.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { App, Aws, CfnElement, Lazy, Stack } from '@aws-cdk/core'; import { AnyPrincipal, ArnPrincipal, IRole, Policy, PolicyStatement, Role } from '../lib'; @@ -196,7 +196,7 @@ describe('IAM Role.fromRoleArn', () => { }); test("does NOT generate a default Policy resource pointing at the imported role's physical name", () => { - expect(roleStack).not.toHaveResourceLike('AWS::IAM::Policy'); + Template.fromStack(roleStack).resourceCountIs('AWS::IAM::Policy', 0); }); }); @@ -283,7 +283,7 @@ describe('IAM Role.fromRoleArn', () => { }); test("does NOT generate a default Policy resource pointing at the imported role's physical name", () => { - expect(roleStack).not.toHaveResourceLike('AWS::IAM::Policy'); + Template.fromStack(roleStack).resourceCountIs('AWS::IAM::Policy', 0); }); }); @@ -322,7 +322,7 @@ describe('IAM Role.fromRoleArn', () => { }); test("does NOT generate a default Policy resource pointing at the imported role's physical name", () => { - expect(roleStack).not.toHaveResourceLike('AWS::IAM::Policy'); + Template.fromStack(roleStack).resourceCountIs('AWS::IAM::Policy', 0); }); }); }); @@ -357,7 +357,7 @@ describe('IAM Role.fromRoleArn', () => { assumedBy: new ArnPrincipal(importedRole.roleName), }); - expect(roleStack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(roleStack).hasResourceProperties('AWS::IAM::Role', { 'AssumeRolePolicyDocument': { 'Statement': [ { @@ -515,7 +515,7 @@ describe('IAM Role.fromRoleArn', () => { roles: [importedRole], }); - expect(roleStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(roleStack).hasResourceProperties('AWS::IAM::Policy', { 'Roles': [ 'codebuild-role', ], @@ -535,7 +535,7 @@ describe('IAM Role.fromRoleArn', () => { roles: [importedRole], }); - expect(roleStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(roleStack).hasResourceProperties('AWS::IAM::Policy', { 'Roles': [ 'codebuild-role', ], @@ -560,6 +560,14 @@ describe('IAM Role.fromRoleArn', () => { }); }); +test('Role.fromRoleName', () => { + const app = new App(); + const stack = new Stack(app, 'Stack', { env: { region: 'asdf', account: '1234' } }); + const role = Role.fromRoleName(stack, 'MyRole', 'MyRole'); + + expect(stack.resolve(role.roleArn)).toEqual({ 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::1234:role/MyRole']] }); +}); + function somePolicyStatement() { return new PolicyStatement({ actions: ['s3:*'], @@ -611,5 +619,5 @@ function _assertStackContainsPolicyResource(stack: Stack, roleNames: any[], name expected.PolicyName = nameOfPolicy; } - expect(stack).toHaveResourceLike('AWS::IAM::Policy', expected); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', expected); } diff --git a/packages/@aws-cdk/aws-iam/test/role.test.ts b/packages/@aws-cdk/aws-iam/test/role.test.ts index ffa1ccd305c58..e0927e14b80ad 100644 --- a/packages/@aws-cdk/aws-iam/test/role.test.ts +++ b/packages/@aws-cdk/aws-iam/test/role.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import { Duration, Stack, App } from '@aws-cdk/core'; import { AnyPrincipal, ArnPrincipal, CompositePrincipal, FederatedPrincipal, ManagedPolicy, PolicyStatement, Role, ServicePrincipal, User, Policy, PolicyDocument } from '../lib'; @@ -11,7 +11,7 @@ describe('IAM role', () => { assumedBy: new ServicePrincipal('sns.amazonaws.com'), }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyRoleF48FFE04: @@ -45,7 +45,7 @@ describe('IAM role', () => { role.grantPassRole(user); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -70,7 +70,7 @@ describe('IAM role', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -98,7 +98,7 @@ describe('IAM role', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -126,7 +126,7 @@ describe('IAM role', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -147,13 +147,13 @@ describe('IAM role', () => { // by default we don't expect a role policy const before = new Stack(); new Role(before, 'MyRole', { assumedBy: new ServicePrincipal('sns.amazonaws.com') }); - expect(before).not.toHaveResource('AWS::IAM::Policy'); + Template.fromStack(before).resourceCountIs('AWS::IAM::Policy', 0); // add a policy to the role const after = new Stack(); const afterRole = new Role(after, 'MyRole', { assumedBy: new ServicePrincipal('sns.amazonaws.com') }); afterRole.addToPolicy(new PolicyStatement({ resources: ['myresource'], actions: ['service:myaction'] })); - expect(after).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(after).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -183,7 +183,7 @@ describe('IAM role', () => { }); role.addManagedPolicy({ managedPolicyArn: 'managed3' }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyRoleF48FFE04: @@ -218,7 +218,7 @@ describe('IAM role', () => { new Role(stack, 'MyRole', { assumedBy: cognitoPrincipal }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Version: '2012-10-17', Statement: [ @@ -240,7 +240,7 @@ describe('IAM role', () => { test('is not specified by default', () => { const stack = new Stack(); new Role(stack, 'MyRole', { assumedBy: new ServicePrincipal('sns.amazonaws.com') }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyRoleF48FFE04: { Type: 'AWS::IAM::Role', @@ -268,7 +268,7 @@ describe('IAM role', () => { new Role(stack, 'MyRole', { maxSessionDuration: Duration.seconds(3700), assumedBy: new ServicePrincipal('sns.amazonaws.com') }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { MaxSessionDuration: 3700, }); }); @@ -301,7 +301,7 @@ describe('IAM role', () => { ), }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -335,7 +335,7 @@ describe('IAM role', () => { permissionsBoundary, }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { PermissionsBoundary: { 'Fn::Join': [ '', @@ -363,7 +363,7 @@ describe('IAM role', () => { assumedBy: new AnyPrincipal(), }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -385,7 +385,7 @@ describe('IAM role', () => { description: 'This is a role description.', }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyRoleF48FFE04: @@ -418,7 +418,7 @@ describe('IAM role', () => { description: '', }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyRoleF48FFE04: @@ -555,7 +555,7 @@ test('managed policy ARNs are deduplicated', () => { }); role.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('SuperDeveloper')); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { ManagedPolicyArns: [ { 'Fn::Join': [ diff --git a/packages/@aws-cdk/aws-iam/test/saml-provider.test.ts b/packages/@aws-cdk/aws-iam/test/saml-provider.test.ts index 83ade3741fb3a..edcbde4685296 100644 --- a/packages/@aws-cdk/aws-iam/test/saml-provider.test.ts +++ b/packages/@aws-cdk/aws-iam/test/saml-provider.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import { SamlMetadataDocument, SamlProvider } from '../lib'; @@ -12,7 +12,7 @@ test('SAML provider', () => { metadataDocument: SamlMetadataDocument.fromXml('document'), }); - expect(stack).toHaveResource('AWS::IAM::SAMLProvider', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::SAMLProvider', { SamlMetadataDocument: 'document', }); }); @@ -23,7 +23,7 @@ test('SAML provider name', () => { name: 'provider-name', }); - expect(stack).toHaveResource('AWS::IAM::SAMLProvider', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::SAMLProvider', { SamlMetadataDocument: 'document', Name: 'provider-name', }); diff --git a/packages/@aws-cdk/aws-iam/test/user.test.ts b/packages/@aws-cdk/aws-iam/test/user.test.ts index d7435b08cbf24..83e610e43a51b 100644 --- a/packages/@aws-cdk/aws-iam/test/user.test.ts +++ b/packages/@aws-cdk/aws-iam/test/user.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { App, SecretValue, Stack, Token } from '@aws-cdk/core'; import { Group, ManagedPolicy, Policy, PolicyStatement, User } from '../lib'; @@ -7,7 +7,7 @@ describe('IAM user', () => { const app = new App(); const stack = new Stack(app, 'MyStack'); new User(stack, 'MyUser'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyUserDC45028B: { Type: 'AWS::IAM::User' } }, }); }); @@ -19,7 +19,7 @@ describe('IAM user', () => { password: SecretValue.plainText('1234'), }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyUserDC45028B: @@ -48,7 +48,7 @@ describe('IAM user', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::User', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::User', { ManagedPolicyArns: [ { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::aws:policy/asdf']] }, ], @@ -65,7 +65,7 @@ describe('IAM user', () => { permissionsBoundary, }); - expect(stack).toHaveResource('AWS::IAM::User', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::User', { PermissionsBoundary: { 'Fn::Join': [ '', @@ -212,7 +212,7 @@ describe('IAM user', () => { })); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { Users: ['john'], PolicyDocument: { Statement: [ @@ -243,7 +243,7 @@ describe('IAM user', () => { })); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { Users: ['john'], PolicyDocument: { Statement: [ @@ -270,7 +270,7 @@ describe('IAM user', () => { otherGroup.addUser(user); // THEN - expect(stack).toHaveResource('AWS::IAM::UserToGroupAddition', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::UserToGroupAddition', { GroupName: { Ref: 'GroupC77FDACD', }, @@ -279,7 +279,7 @@ describe('IAM user', () => { ], }); - expect(stack).toHaveResource('AWS::IAM::UserToGroupAddition', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::UserToGroupAddition', { GroupName: { Ref: 'OtherGroup85E5C653', }, diff --git a/packages/@aws-cdk/aws-imagebuilder/package.json b/packages/@aws-cdk/aws-imagebuilder/package.json index cb7bd4966ac33..6d7581afc8a3f 100644 --- a/packages/@aws-cdk/aws-imagebuilder/package.json +++ b/packages/@aws-cdk/aws-imagebuilder/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-inspector/package.json b/packages/@aws-cdk/aws-inspector/package.json index 433bac89fa1b6..446b49eec052b 100644 --- a/packages/@aws-cdk/aws-inspector/package.json +++ b/packages/@aws-cdk/aws-inspector/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -76,7 +83,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-inspectorv2/.eslintrc.js b/packages/@aws-cdk/aws-inspectorv2/.eslintrc.js new file mode 100644 index 0000000000000..2658ee8727166 --- /dev/null +++ b/packages/@aws-cdk/aws-inspectorv2/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-inspectorv2/.gitignore b/packages/@aws-cdk/aws-inspectorv2/.gitignore new file mode 100644 index 0000000000000..62ebc95d75ce6 --- /dev/null +++ b/packages/@aws-cdk/aws-inspectorv2/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk +nyc.config.js +!.eslintrc.js +!jest.config.js +junit.xml diff --git a/packages/@aws-cdk/aws-inspectorv2/.npmignore b/packages/@aws-cdk/aws-inspectorv2/.npmignore new file mode 100644 index 0000000000000..f931fede67c44 --- /dev/null +++ b/packages/@aws-cdk/aws-inspectorv2/.npmignore @@ -0,0 +1,29 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json + +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml +test/ +!*.lit.ts diff --git a/packages/@aws-cdk/aws-inspectorv2/LICENSE b/packages/@aws-cdk/aws-inspectorv2/LICENSE new file mode 100644 index 0000000000000..82ad00bb02d0b --- /dev/null +++ b/packages/@aws-cdk/aws-inspectorv2/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-inspectorv2/NOTICE b/packages/@aws-cdk/aws-inspectorv2/NOTICE new file mode 100644 index 0000000000000..1b7adbb891265 --- /dev/null +++ b/packages/@aws-cdk/aws-inspectorv2/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-inspectorv2/README.md b/packages/@aws-cdk/aws-inspectorv2/README.md new file mode 100644 index 0000000000000..5d16d199514e3 --- /dev/null +++ b/packages/@aws-cdk/aws-inspectorv2/README.md @@ -0,0 +1,31 @@ +# AWS::InspectorV2 Construct Library + + +--- + +![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) + +> All classes with the `Cfn` prefix in this module ([CFN Resources]) are always stable and safe to use. +> +> [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib + +--- + + + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +```ts nofixture +import * as inspector from '@aws-cdk/aws-inspectorv2'; +``` + + + +There are no hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet. +However, you can still use the automatically generated [L1](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_l1_using) constructs, and use this service exactly as you would using CloudFormation directly. + +For more information on the resources and properties available for this service, see the [CloudFormation documentation for AWS::InspectorV2](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_InspectorV2.html). + +(Read the [CDK Contributing Guide](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md) if you are interested in contributing to this construct library.) + + diff --git a/packages/@aws-cdk/aws-inspectorv2/jest.config.js b/packages/@aws-cdk/aws-inspectorv2/jest.config.js new file mode 100644 index 0000000000000..3a2fd93a1228a --- /dev/null +++ b/packages/@aws-cdk/aws-inspectorv2/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-inspectorv2/lib/index.ts b/packages/@aws-cdk/aws-inspectorv2/lib/index.ts new file mode 100644 index 0000000000000..d41ac2a9185de --- /dev/null +++ b/packages/@aws-cdk/aws-inspectorv2/lib/index.ts @@ -0,0 +1,2 @@ +// AWS::InspectorV2 CloudFormation Resources: +export * from './inspectorv2.generated'; diff --git a/packages/@aws-cdk/aws-inspectorv2/package.json b/packages/@aws-cdk/aws-inspectorv2/package.json new file mode 100644 index 0000000000000..6d3ec7b525446 --- /dev/null +++ b/packages/@aws-cdk/aws-inspectorv2/package.json @@ -0,0 +1,110 @@ +{ + "name": "@aws-cdk/aws-inspectorv2", + "version": "0.0.0", + "description": "AWS::InspectorV2 Construct Library", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "projectReferences": true, + "targets": { + "dotnet": { + "namespace": "Amazon.CDK.AWS.InspectorV2", + "packageId": "Amazon.CDK.AWS.InspectorV2", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "java": { + "package": "software.amazon.awscdk.services.inspectorv2", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "inspectorv2" + } + }, + "python": { + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 1" + ], + "distName": "aws-cdk.aws-inspectorv2", + "module": "aws_cdk.aws_inspectorv2" + } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-inspectorv2" + }, + "homepage": "https://github.com/aws/aws-cdk", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "cfn2ts": "cfn2ts", + "build+test": "yarn build && yarn test", + "build+test+package": "yarn build+test && yarn package", + "compat": "cdk-compat", + "gen": "cfn2ts", + "rosetta:extract": "yarn --silent jsii-rosetta extract", + "build+extract": "yarn build && yarn rosetta:extract", + "build+test+extract": "yarn build+test && yarn rosetta:extract" + }, + "cdk-build": { + "cloudformation": "AWS::InspectorV2", + "jest": true, + "env": { + "AWSLINT_BASE_CONSTRUCT": "true" + } + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "AWS::InspectorV2", + "aws-inspectorv2" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assertions": "0.0.0", + "@aws-cdk/cdk-build-tools": "0.0.0", + "@aws-cdk/cfn2ts": "0.0.0", + "@aws-cdk/pkglint": "0.0.0", + "@types/jest": "^27.4.1" + }, + "dependencies": { + "@aws-cdk/core": "0.0.0" + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "cfn-only", + "awscdkio": { + "announce": false + }, + "publishConfig": { + "tag": "latest" + } +} diff --git a/packages/@aws-cdk/aws-inspectorv2/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-inspectorv2/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..e208762bca03c --- /dev/null +++ b/packages/@aws-cdk/aws-inspectorv2/rosetta/default.ts-fixture @@ -0,0 +1,8 @@ +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; + +class MyStack extends Stack { + constructor(scope: Construct, id: string) { + /// here + } +} diff --git a/packages/@aws-cdk/aws-inspectorv2/test/inspectorv2.test.ts b/packages/@aws-cdk/aws-inspectorv2/test/inspectorv2.test.ts new file mode 100644 index 0000000000000..465c7bdea0693 --- /dev/null +++ b/packages/@aws-cdk/aws-inspectorv2/test/inspectorv2.test.ts @@ -0,0 +1,6 @@ +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-iot-actions/README.md b/packages/@aws-cdk/aws-iot-actions/README.md index ffb45017ed721..088fda5f3e5b8 100644 --- a/packages/@aws-cdk/aws-iot-actions/README.md +++ b/packages/@aws-cdk/aws-iot-actions/README.md @@ -21,13 +21,32 @@ supported AWS Services. Instances of these classes should be passed to Currently supported are: +- Republish a message to another MQTT topic - Invoke a Lambda function - Put objects to a S3 bucket - Put logs to CloudWatch Logs - Capture CloudWatch metrics - Change state for a CloudWatch alarm +- Put records to Kinesis Data stream - Put records to Kinesis Data Firehose stream - Send messages to SQS queues +- Publish messages on SNS topics + +## Republish a message to another MQTT topic + +The code snippet below creates an AWS IoT Rule that republish a message to +another MQTT topic when it is triggered. + +```ts +new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, timestamp() as timestamp, temperature FROM 'device/+/data'"), + actions: [ + new actions.IotRepublishMqttAction('${topic()}/republish', { + qualityOfService: actions.MqttQualityOfService.AT_LEAST_ONCE, // optional property, default is MqttQualityOfService.ZERO_OR_MORE_TIMES + }), + ], +}); +``` ## Invoke a Lambda function @@ -35,10 +54,6 @@ The code snippet below creates an AWS IoT Rule that invoke a Lambda function when it is triggered. ```ts -import * as iot from '@aws-cdk/aws-iot'; -import * as actions from '@aws-cdk/aws-iot-actions'; -import * as lambda from '@aws-cdk/aws-lambda'; - const func = new lambda.Function(this, 'MyFunction', { runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', @@ -61,10 +76,6 @@ The code snippet below creates an AWS IoT Rule that put objects to a S3 bucket when it is triggered. ```ts -import * as iot from '@aws-cdk/aws-iot'; -import * as actions from '@aws-cdk/aws-iot-actions'; -import * as s3 from '@aws-cdk/aws-s3'; - const bucket = new s3.Bucket(this, 'MyBucket'); new iot.TopicRule(this, 'TopicRule', { @@ -83,6 +94,8 @@ by the number of the current timestamp in milliseconds as `1636289461203`. So if You can also set specific `key` as following: ```ts +const bucket = new s3.Bucket(this, 'MyBucket'); + new iot.TopicRule(this, 'TopicRule', { sql: iot.IotSql.fromStringAsVer20160323( "SELECT topic(2) as device_id, year, month, day FROM 'device/+/data'", @@ -98,6 +111,8 @@ new iot.TopicRule(this, 'TopicRule', { If you wanna set access control to the S3 bucket object, you can specify `accessControl` as following: ```ts +const bucket = new s3.Bucket(this, 'MyBucket'); + new iot.TopicRule(this, 'TopicRule', { sql: iot.IotSql.fromStringAsVer20160323("SELECT * FROM 'device/+/data'"), actions: [ @@ -114,8 +129,6 @@ The code snippet below creates an AWS IoT Rule that put logs to CloudWatch Logs when it is triggered. ```ts -import * as iot from '@aws-cdk/aws-iot'; -import * as actions from '@aws-cdk/aws-iot-actions'; import * as logs from '@aws-cdk/aws-logs'; const logGroup = new logs.LogGroup(this, 'MyLogGroup'); @@ -132,9 +145,6 @@ The code snippet below creates an AWS IoT Rule that capture CloudWatch metrics when it is triggered. ```ts -import * as iot from '@aws-cdk/aws-iot'; -import * as actions from '@aws-cdk/aws-iot-actions'; - const topicRule = new iot.TopicRule(this, 'TopicRule', { sql: iot.IotSql.fromStringAsVer20160323( "SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'", @@ -157,8 +167,6 @@ The code snippet below creates an AWS IoT Rule that changes the state of an Amaz ```ts import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; -import * as iot from '@aws-cdk/aws-iot'; -import * as actions from '@aws-cdk/aws-iot-actions'; const metric = new cloudwatch.Metric({ namespace: 'MyNamespace', @@ -183,15 +191,32 @@ const topicRule = new iot.TopicRule(this, 'TopicRule', { }); ``` +## Put records to Kinesis Data stream + +The code snippet below creates an AWS IoT Rule that put records to Kinesis Data +stream when it is triggered. + +```ts +import * as kinesis from '@aws-cdk/aws-kinesis'; + +const stream = new kinesis.Stream(this, 'MyStream'); + +const topicRule = new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT * FROM 'device/+/data'"), + actions: [ + new actions.KinesisPutRecordAction(stream, { + partitionKey: '${newuuid()}', + }), + ], +}); +``` + ## Put records to Kinesis Data Firehose stream The code snippet below creates an AWS IoT Rule that put records to Put records to Kinesis Data Firehose stream when it is triggered. ```ts -import * as iot from '@aws-cdk/aws-iot'; -import * as actions from '@aws-cdk/aws-iot-actions'; -import * as s3 from '@aws-cdk/aws-s3'; import * as firehose from '@aws-cdk/aws-kinesisfirehose'; import * as destinations from '@aws-cdk/aws-kinesisfirehose-destinations'; @@ -203,9 +228,9 @@ const stream = new firehose.DeliveryStream(this, 'MyStream', { const topicRule = new iot.TopicRule(this, 'TopicRule', { sql: iot.IotSql.fromStringAsVer20160323("SELECT * FROM 'device/+/data'"), actions: [ - new actions.FirehoseStreamAction(stream, { + new actions.FirehosePutRecordAction(stream, { batchMode: true, - recordSeparator: actions.FirehoseStreamRecordSeparator.NEWLINE, + recordSeparator: actions.FirehoseRecordSeparator.NEWLINE, }), ], }); @@ -217,8 +242,6 @@ The code snippet below creates an AWS IoT Rule that send messages to an SQS queue when it is triggered: ```ts -import * as iot from '@aws-cdk/aws-iot'; -import * as actions from '@aws-cdk/aws-iot-actions'; import * as sqs from '@aws-cdk/aws-sqs'; const queue = new sqs.Queue(this, 'MyQueue'); @@ -231,6 +254,27 @@ const topicRule = new iot.TopicRule(this, 'TopicRule', { new actions.SqsQueueAction(queue, { useBase64: true, // optional property, default is 'false' }), - ] + ], +}); +``` + +## Publish messages on an SNS topic + +The code snippet below creates and AWS IoT Rule that publishes messages to an SNS topic when it is triggered: + +```ts +import * as sns from '@aws-cdk/aws-sns'; + +const topic = new sns.Topic(this, 'MyTopic'); + +const topicRule = new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323( + "SELECT topic(2) as device_id, year, month, day FROM 'device/+/data'", + ), + actions: [ + new actions.SnsTopicAction(topic, { + messageFormat: actions.SnsActionMessageFormat.JSON, // optional property, default is SnsActionMessageFormat.RAW + }), + ], }); ``` diff --git a/packages/@aws-cdk/aws-iot-actions/lib/firehose-stream-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/firehose-put-record-action.ts similarity index 88% rename from packages/@aws-cdk/aws-iot-actions/lib/firehose-stream-action.ts rename to packages/@aws-cdk/aws-iot-actions/lib/firehose-put-record-action.ts index c694bef7cad38..e9583ce1c87e3 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/firehose-stream-action.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/firehose-put-record-action.ts @@ -7,7 +7,7 @@ import { singletonActionRole } from './private/role'; /** * Record Separator to be used to separate records. */ -export enum FirehoseStreamRecordSeparator { +export enum FirehoseRecordSeparator { /** * Separate by a new line */ @@ -32,7 +32,7 @@ export enum FirehoseStreamRecordSeparator { /** * Configuration properties of an action for the Kinesis Data Firehose stream. */ -export interface FirehoseStreamActionProps extends CommonActionProps { +export interface FirehosePutRecordActionProps extends CommonActionProps { /** * Whether to deliver the Kinesis Data Firehose stream as a batch by using `PutRecordBatch`. * When batchMode is true and the rule's SQL statement evaluates to an Array, each Array @@ -48,14 +48,14 @@ export interface FirehoseStreamActionProps extends CommonActionProps { * * @default - none -- the stream does not use a separator */ - readonly recordSeparator?: FirehoseStreamRecordSeparator; + readonly recordSeparator?: FirehoseRecordSeparator; } /** * The action to put the record from an MQTT message to the Kinesis Data Firehose stream. */ -export class FirehoseStreamAction implements iot.IAction { +export class FirehosePutRecordAction implements iot.IAction { private readonly batchMode?: boolean; private readonly recordSeparator?: string; private readonly role?: iam.IRole; @@ -64,7 +64,7 @@ export class FirehoseStreamAction implements iot.IAction { * @param stream The Kinesis Data Firehose stream to which to put records. * @param props Optional properties to not use default */ - constructor(private readonly stream: firehose.IDeliveryStream, props: FirehoseStreamActionProps = {}) { + constructor(private readonly stream: firehose.IDeliveryStream, props: FirehosePutRecordActionProps = {}) { this.batchMode = props.batchMode; this.recordSeparator = props.recordSeparator; this.role = props.role; diff --git a/packages/@aws-cdk/aws-iot-actions/lib/index.ts b/packages/@aws-cdk/aws-iot-actions/lib/index.ts index a817ccb0ca35a..5c214f4143309 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/index.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/index.ts @@ -2,8 +2,10 @@ export * from './cloudwatch-logs-action'; export * from './cloudwatch-put-metric-action'; export * from './cloudwatch-set-alarm-state-action'; export * from './common-action-props'; -export * from './firehose-stream-action'; +export * from './firehose-put-record-action'; +export * from './iot-republish-action'; +export * from './kinesis-put-record-action'; export * from './lambda-function-action'; export * from './s3-put-object-action'; export * from './sqs-queue-action'; - +export * from './sns-topic-action'; diff --git a/packages/@aws-cdk/aws-iot-actions/lib/iot-republish-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/iot-republish-action.ts new file mode 100644 index 0000000000000..77aadb876c4d9 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/lib/iot-republish-action.ts @@ -0,0 +1,72 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import { CommonActionProps } from './common-action-props'; +import { singletonActionRole } from './private/role'; + +/** + * MQTT Quality of Service (QoS) indicates the level of assurance for delivery of an MQTT Message. + * + * @see https://docs.aws.amazon.com/iot/latest/developerguide/mqtt.html#mqtt-qos + */ +export enum MqttQualityOfService { + /** + * QoS level 0. Sent zero or more times. + * This level should be used for messages that are sent over reliable communication links or that can be missed without a problem. + */ + ZERO_OR_MORE_TIMES, + + /** + * QoS level 1. Sent at least one time, and then repeatedly until a PUBACK response is received. + * The message is not considered complete until the sender receives a PUBACK response to indicate successful delivery. + */ + AT_LEAST_ONCE, +} + +/** + * Configuration properties of an action to republish MQTT messages. + */ +export interface IotRepublishMqttActionProps extends CommonActionProps { + /** + * The Quality of Service (QoS) level to use when republishing messages. + * + * @see https://docs.aws.amazon.com/iot/latest/developerguide/mqtt.html#mqtt-qos + * + * @default MqttQualityOfService.ZERO_OR_MORE_TIMES + */ + readonly qualityOfService?: MqttQualityOfService; +} + +/** + * The action to put the record from an MQTT message to republish another MQTT topic. + */ +export class IotRepublishMqttAction implements iot.IAction { + private readonly qualityOfService?: MqttQualityOfService; + private readonly role?: iam.IRole; + + /** + * @param topic The MQTT topic to which to republish the message. + * @param props Optional properties to not use default. + */ + constructor(private readonly topic: string, props: IotRepublishMqttActionProps = {}) { + this.qualityOfService = props.qualityOfService; + this.role = props.role; + } + + bind(rule: iot.ITopicRule): iot.ActionConfig { + const role = this.role ?? singletonActionRole(rule); + role.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['iot:Publish'], + resources: ['*'], + })); + + return { + configuration: { + republish: { + topic: this.topic, + qos: this.qualityOfService, + roleArn: role.roleArn, + }, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-iot-actions/lib/kinesis-put-record-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/kinesis-put-record-action.ts new file mode 100644 index 0000000000000..6baa5976bccf4 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/lib/kinesis-put-record-action.ts @@ -0,0 +1,58 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import * as kinesis from '@aws-cdk/aws-kinesis'; +import { CommonActionProps } from './common-action-props'; +import { singletonActionRole } from './private/role'; + +/** + * Configuration properties of an action for the Kinesis Data stream. + */ +export interface KinesisPutRecordActionProps extends CommonActionProps { + /** + * The partition key used to determine to which shard the data is written. + * The partition key is usually composed of an expression (for example, ${topic()} or ${timestamp()}). + * + * @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html + * + * You can use the expression '${newuuid()}' if your payload does not have a high cardinarity property. + * If you use empty string, this action use no partition key and all records will put same one shard. + * + * @see https://docs.aws.amazon.com/kinesis/latest/APIReference/API_PutRecord.html#API_PutRecord_RequestParameters + */ + readonly partitionKey: string; +} + +/** + * The action to put the record from an MQTT message to the Kinesis Data stream. + */ +export class KinesisPutRecordAction implements iot.IAction { + private readonly partitionKey?: string; + private readonly role?: iam.IRole; + + /** + * @param stream The Kinesis Data stream to which to put records. + * @param props Optional properties to not use default + */ + constructor(private readonly stream: kinesis.IStream, props: KinesisPutRecordActionProps) { + this.partitionKey = props.partitionKey; + this.role = props.role; + } + + bind(rule: iot.ITopicRule): iot.ActionConfig { + const role = this.role ?? singletonActionRole(rule); + role.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['kinesis:PutRecord'], + resources: [this.stream.streamArn], + })); + + return { + configuration: { + kinesis: { + streamName: this.stream.streamName, + partitionKey: this.partitionKey || undefined, + roleArn: role.roleArn, + }, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-iot-actions/lib/sns-topic-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/sns-topic-action.ts new file mode 100644 index 0000000000000..701eeb0d8b39e --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/lib/sns-topic-action.ts @@ -0,0 +1,75 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import * as sns from '@aws-cdk/aws-sns'; +import { CommonActionProps } from '.'; +import { singletonActionRole } from './private/role'; + +/** + * SNS topic action message format options. + */ +export enum SnsActionMessageFormat { + /** + * RAW message format. + */ + RAW = 'RAW', + + /** + * JSON message format. + */ + JSON = 'JSON' +} + +/** + * Configuration options for the SNS topic action. + */ +export interface SnsTopicActionProps extends CommonActionProps { + /** + * The message format of the message to publish. + * + * SNS uses this setting to determine if the payload should be parsed and relevant platform-specific bits of the payload should be extracted. + * @see https://docs.aws.amazon.com/sns/latest/dg/sns-message-and-json-formats.html + * + * @default SnsActionMessageFormat.RAW + */ + readonly messageFormat?: SnsActionMessageFormat; +} + +/** + * The action to write the data from an MQTT message to an Amazon SNS topic. + * + * @see https://docs.aws.amazon.com/iot/latest/developerguide/sns-rule-action.html + */ +export class SnsTopicAction implements iot.IAction { + private readonly role?: iam.IRole; + private readonly topic: sns.ITopic; + private readonly messageFormat?: SnsActionMessageFormat; + + /** + * @param topic The Amazon SNS topic to publish data on. Must not be a FIFO topic. + * @param props Properties to configure the action. + */ + constructor(topic: sns.ITopic, props: SnsTopicActionProps = {}) { + if (topic.fifo) { + throw Error('IoT Rule actions cannot be used with FIFO SNS Topics, please pass a non-FIFO Topic instead'); + } + + this.topic = topic; + this.role = props.role; + this.messageFormat = props.messageFormat; + } + + bind(rule: iot.ITopicRule): iot.ActionConfig { + const role = this.role ?? singletonActionRole(rule); + this.topic.grantPublish(role); + + return { + configuration: { + sns: { + targetArn: this.topic.topicArn, + roleArn: role.roleArn, + messageFormat: this.messageFormat, + }, + }, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/package.json b/packages/@aws-cdk/aws-iot-actions/package.json index d39aa93533a94..a2db6d0529f63 100644 --- a/packages/@aws-cdk/aws-iot-actions/package.json +++ b/packages/@aws-cdk/aws-iot-actions/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -75,18 +82,20 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "constructs": "^3.3.69", - "jest": "^27.4.5" + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-iot": "0.0.0", + "@aws-cdk/aws-kinesis": "0.0.0", "@aws-cdk/aws-kinesisfirehose": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/core": "0.0.0", "case": "1.6.3", @@ -97,10 +106,12 @@ "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-iot": "0.0.0", + "@aws-cdk/aws-kinesis": "0.0.0", "@aws-cdk/aws-kinesisfirehose": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" diff --git a/packages/@aws-cdk/aws-iot-actions/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-iot-actions/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..75c7b4d0eeca0 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/rosetta/default.ts-fixture @@ -0,0 +1,14 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as actions from '@aws-cdk/aws-iot-actions'; +import * as iot from '@aws-cdk/aws-iot'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as s3 from '@aws-cdk/aws-s3'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/iot/integ.iot-republish-action.expected.json b/packages/@aws-cdk/aws-iot-actions/test/iot/integ.iot-republish-action.expected.json new file mode 100644 index 0000000000000..c396017676ac4 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/iot/integ.iot-republish-action.expected.json @@ -0,0 +1,65 @@ +{ + "Resources": { + "TopicRule40A4EA44": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Republish": { + "Qos": 1, + "RoleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + }, + "Topic": "${topic()}/republish" + } + } + ], + "AwsIotSqlVersion": "2016-03-23", + "Sql": "SELECT * FROM 'device/+/data'" + } + } + }, + "TopicRuleTopicRuleActionRole246C4F77": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "iot:Publish", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687", + "Roles": [ + { + "Ref": "TopicRuleTopicRuleActionRole246C4F77" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/iot/integ.iot-republish-action.ts b/packages/@aws-cdk/aws-iot-actions/test/iot/integ.iot-republish-action.ts new file mode 100644 index 0000000000000..d5a71b32f6a0d --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/iot/integ.iot-republish-action.ts @@ -0,0 +1,25 @@ +import * as iot from '@aws-cdk/aws-iot'; +import * as cdk from '@aws-cdk/core'; +import * as actions from '../../lib'; + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const topicRule = new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323( + "SELECT * FROM 'device/+/data'", + ), + }); + + topicRule.addAction( + new actions.IotRepublishMqttAction('${topic()}/republish', { + qualityOfService: actions.MqttQualityOfService.AT_LEAST_ONCE, + }), + ); + } +} + +const app = new cdk.App(); +new TestStack(app, 'iot-republish-action-test-stack'); +app.synth(); diff --git a/packages/@aws-cdk/aws-iot-actions/test/iot/iot-republish-action.test.ts b/packages/@aws-cdk/aws-iot-actions/test/iot/iot-republish-action.test.ts new file mode 100644 index 0000000000000..8a78e272285ef --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/iot/iot-republish-action.test.ts @@ -0,0 +1,107 @@ +import { Template, Match } from '@aws-cdk/assertions'; +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import * as cdk from '@aws-cdk/core'; +import * as actions from '../../lib'; + +let stack: cdk.Stack; +let topicRule:iot.TopicRule; +beforeEach(() => { + stack = new cdk.Stack(); + topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), + }); +}); + +test('Default IoT republish action', () => { + // WHEN + topicRule.addAction( + new actions.IotRepublishMqttAction('test-topic'), + ); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Republish: { + Topic: 'test-topic', + RoleArn: { + 'Fn::GetAtt': ['MyTopicRuleTopicRuleActionRoleCE2D05DA', 'Arn'], + }, + }, + }, + ], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'iot.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'iot:Publish', + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyTopicRuleTopicRuleActionRoleDefaultPolicy54A701F7', + Roles: [ + { Ref: 'MyTopicRuleTopicRuleActionRoleCE2D05DA' }, + ], + }); +}); + +test('can set qualityOfService', () => { + // WHEN + topicRule.addAction( + new actions.IotRepublishMqttAction('test-topic', { qualityOfService: actions.MqttQualityOfService.AT_LEAST_ONCE }), + ); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + Match.objectLike({ Republish: { Qos: 1 } }), + ], + }, + }); +}); + +test('can set role', () => { + // WHEN + const role = iam.Role.fromRoleArn(stack, 'MyRole', 'arn:aws:iam::123456789012:role/ForTest'); + topicRule.addAction( + new actions.IotRepublishMqttAction('test-topic', { role }), + ); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + Match.objectLike({ Republish: { RoleArn: 'arn:aws:iam::123456789012:role/ForTest' } }), + ], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyName: 'MyRolePolicy64AB00A5', + Roles: ['ForTest'], + }); +}); diff --git a/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/firehose-stream-action.test.ts b/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/firehose-put-record-action.test.ts similarity index 93% rename from packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/firehose-stream-action.test.ts rename to packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/firehose-put-record-action.test.ts index 2941cc1db270c..55fd6cbbfe51c 100644 --- a/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/firehose-stream-action.test.ts +++ b/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/firehose-put-record-action.test.ts @@ -15,7 +15,7 @@ test('Default firehose stream action', () => { // WHEN topicRule.addAction( - new actions.FirehoseStreamAction(stream), + new actions.FirehosePutRecordAction(stream), ); // THEN @@ -77,7 +77,7 @@ test('can set batchMode', () => { // WHEN topicRule.addAction( - new actions.FirehoseStreamAction(stream, { batchMode: true }), + new actions.FirehosePutRecordAction(stream, { batchMode: true }), ); // THEN @@ -100,7 +100,7 @@ test('can set separotor', () => { // WHEN topicRule.addAction( - new actions.FirehoseStreamAction(stream, { recordSeparator: actions.FirehoseStreamRecordSeparator.NEWLINE }), + new actions.FirehosePutRecordAction(stream, { recordSeparator: actions.FirehoseRecordSeparator.NEWLINE }), ); // THEN @@ -124,7 +124,7 @@ test('can set role', () => { // WHEN topicRule.addAction( - new actions.FirehoseStreamAction(stream, { role }), + new actions.FirehosePutRecordAction(stream, { role }), ); // THEN diff --git a/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/integ.firehose-stream-action.expected.json b/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/integ.firehose-put-record-action.expected.json similarity index 97% rename from packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/integ.firehose-stream-action.expected.json rename to packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/integ.firehose-put-record-action.expected.json index d1565669b5c04..5c484ba7f5049 100644 --- a/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/integ.firehose-stream-action.expected.json +++ b/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/integ.firehose-put-record-action.expected.json @@ -123,6 +123,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/integ.firehose-stream-action.ts b/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/integ.firehose-put-record-action.ts similarity index 89% rename from packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/integ.firehose-stream-action.ts rename to packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/integ.firehose-put-record-action.ts index 2c6c93cf0460f..d065723d6468e 100644 --- a/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/integ.firehose-stream-action.ts +++ b/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/integ.firehose-put-record-action.ts @@ -25,9 +25,9 @@ class TestStack extends cdk.Stack { destinations: [new destinations.S3Bucket(bucket)], }); topicRule.addAction( - new actions.FirehoseStreamAction(stream, { + new actions.FirehosePutRecordAction(stream, { batchMode: true, - recordSeparator: actions.FirehoseStreamRecordSeparator.NEWLINE, + recordSeparator: actions.FirehoseRecordSeparator.NEWLINE, }), ); } diff --git a/packages/@aws-cdk/aws-iot-actions/test/kinesis-stream/integ.kinesis-put-record-action.expected.json b/packages/@aws-cdk/aws-iot-actions/test/kinesis-stream/integ.kinesis-put-record-action.expected.json new file mode 100644 index 0000000000000..89229bf4731e3 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/kinesis-stream/integ.kinesis-put-record-action.expected.json @@ -0,0 +1,116 @@ +{ + "Resources": { + "TopicRule40A4EA44": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Kinesis": { + "PartitionKey": "${timestamp()}", + "RoleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + }, + "StreamName": { + "Ref": "MyStream5C050E93" + } + } + } + ], + "AwsIotSqlVersion": "2016-03-23", + "Sql": "SELECT * FROM 'device/+/data'" + } + } + }, + "TopicRuleTopicRuleActionRole246C4F77": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "kinesis:PutRecord", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyStream5C050E93", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687", + "Roles": [ + { + "Ref": "TopicRuleTopicRuleActionRole246C4F77" + } + ] + } + }, + "MyStream5C050E93": { + "Type": "AWS::Kinesis::Stream", + "Properties": { + "RetentionPeriodHours": 24, + "ShardCount": 3, + "StreamEncryption": { + "Fn::If": [ + "AwsCdkKinesisEncryptedStreamsUnsupportedRegions", + { + "Ref": "AWS::NoValue" + }, + { + "EncryptionType": "KMS", + "KeyId": "alias/aws/kinesis" + } + ] + }, + "StreamModeDetails": { + "StreamMode": "PROVISIONED" + } + } + } + }, + "Conditions": { + "AwsCdkKinesisEncryptedStreamsUnsupportedRegions": { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-north-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-northwest-1" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/kinesis-stream/integ.kinesis-put-record-action.ts b/packages/@aws-cdk/aws-iot-actions/test/kinesis-stream/integ.kinesis-put-record-action.ts new file mode 100644 index 0000000000000..953f370957ec3 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/kinesis-stream/integ.kinesis-put-record-action.ts @@ -0,0 +1,27 @@ +import * as iot from '@aws-cdk/aws-iot'; +import * as kinesis from '@aws-cdk/aws-kinesis'; +import * as cdk from '@aws-cdk/core'; +import * as actions from '../../lib'; + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const topicRule = new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323( + "SELECT * FROM 'device/+/data'", + ), + }); + + const stream = new kinesis.Stream(this, 'MyStream', { + shardCount: 3, + }); + topicRule.addAction(new actions.KinesisPutRecordAction(stream, { + partitionKey: '${timestamp()}', + })); + } +} + +const app = new cdk.App(); +new TestStack(app, 'test-kinesis-stream-action-stack'); +app.synth(); diff --git a/packages/@aws-cdk/aws-iot-actions/test/kinesis-stream/kinesis-put-record-action.test.ts b/packages/@aws-cdk/aws-iot-actions/test/kinesis-stream/kinesis-put-record-action.test.ts new file mode 100644 index 0000000000000..05a0d603b4046 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/kinesis-stream/kinesis-put-record-action.test.ts @@ -0,0 +1,122 @@ +import { Template, Match } from '@aws-cdk/assertions'; +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import * as kinesis from '@aws-cdk/aws-kinesis'; +import * as cdk from '@aws-cdk/core'; +import * as actions from '../../lib'; + +test('Default kinesis stream action', () => { + // GIVEN + const stack = new cdk.Stack(); + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), + }); + const stream = kinesis.Stream.fromStreamArn(stack, 'MyStream', 'arn:aws:kinesis:xx-west-1:111122223333:stream/my-stream'); + + // WHEN + topicRule.addAction(new actions.KinesisPutRecordAction(stream, { + partitionKey: '${newuuid()}', + })); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Kinesis: { + StreamName: 'my-stream', + PartitionKey: '${newuuid()}', + RoleArn: { + 'Fn::GetAtt': ['MyTopicRuleTopicRuleActionRoleCE2D05DA', 'Arn'], + }, + }, + }, + ], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'iot.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'kinesis:PutRecord', + Effect: 'Allow', + Resource: 'arn:aws:kinesis:xx-west-1:111122223333:stream/my-stream', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyTopicRuleTopicRuleActionRoleDefaultPolicy54A701F7', + Roles: [ + { Ref: 'MyTopicRuleTopicRuleActionRoleCE2D05DA' }, + ], + }); +}); + +test('passes undefined to partitionKey if empty string is given', () => { + // GIVEN + const stack = new cdk.Stack(); + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), + }); + const stream = kinesis.Stream.fromStreamArn(stack, 'MyStream', 'arn:aws:kinesis:xx-west-1:111122223333:stream/my-stream'); + + // WHEN + topicRule.addAction(new actions.KinesisPutRecordAction(stream, { + partitionKey: '', + })); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + Match.objectLike({ Kinesis: { PartitionKey: Match.absent() } }), + ], + }, + }); +}); + +test('can set role', () => { + // GIVEN + const stack = new cdk.Stack(); + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), + }); + const stream = kinesis.Stream.fromStreamArn(stack, 'MyStream', 'arn:aws:kinesis:xx-west-1:111122223333:stream/my-stream'); + const role = iam.Role.fromRoleArn(stack, 'MyRole', 'arn:aws:iam::123456789012:role/ForTest'); + + // WHEN + topicRule.addAction(new actions.KinesisPutRecordAction(stream, { + partitionKey: '${newuuid()}', + role, + })); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + Match.objectLike({ Kinesis: { RoleArn: 'arn:aws:iam::123456789012:role/ForTest' } }), + ], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyName: 'MyRolePolicy64AB00A5', + Roles: ['ForTest'], + }); +}); diff --git a/packages/@aws-cdk/aws-iot-actions/test/sns/integ.sns-topic-action.expected.json b/packages/@aws-cdk/aws-iot-actions/test/sns/integ.sns-topic-action.expected.json new file mode 100644 index 0000000000000..0bf9706012c93 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/sns/integ.sns-topic-action.expected.json @@ -0,0 +1,71 @@ +{ + "Resources": { + "TopicRule40A4EA44": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Sns": { + "RoleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + }, + "TargetArn": { + "Ref": "MyTopic86869434" + } + } + } + ], + "AwsIotSqlVersion": "2016-03-23", + "Sql": "SELECT topic(2) as device_id, year, month, day FROM 'device/+/data'" + } + } + }, + "TopicRuleTopicRuleActionRole246C4F77": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "MyTopic86869434" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687", + "Roles": [ + { + "Ref": "TopicRuleTopicRuleActionRole246C4F77" + } + ] + } + }, + "MyTopic86869434": { + "Type": "AWS::SNS::Topic" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/sns/integ.sns-topic-action.ts b/packages/@aws-cdk/aws-iot-actions/test/sns/integ.sns-topic-action.ts new file mode 100644 index 0000000000000..5243ab2effab4 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/sns/integ.sns-topic-action.ts @@ -0,0 +1,33 @@ +/** + * Stack verification steps: + * * aws sns subscribe --topic-arn "arn:aws:sns:::test-stack-MyTopic86869434-10F6E3DMK3E5P" --protocol email --notification-endpoint + * * confirm subscription from email + * * echo '{"message": "hello world"}' > testfile.txt + * * aws iot-data publish --topic device/mydevice/data --qos 1 --payload fileb://testfile.txt + * * verify that an email was sent from the SNS + * * rm testfile.txt + */ +/// !cdk-integ pragma:ignore-assets +import * as iot from '@aws-cdk/aws-iot'; +import * as sns from '@aws-cdk/aws-sns'; +import * as cdk from '@aws-cdk/core'; +import * as actions from '../../lib'; + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const topicRule = new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323( + "SELECT topic(2) as device_id, year, month, day FROM 'device/+/data'", + ), + }); + + const snsTopic = new sns.Topic(this, 'MyTopic'); + topicRule.addAction(new actions.SnsTopicAction(snsTopic)); + } +} + +const app = new cdk.App(); +new TestStack(app, 'sns-topic-action-test-stack'); +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/sns/sns-topic-action.test.ts b/packages/@aws-cdk/aws-iot-actions/test/sns/sns-topic-action.test.ts new file mode 100644 index 0000000000000..c949b5202c72c --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/sns/sns-topic-action.test.ts @@ -0,0 +1,103 @@ +import { Match, Template } from '@aws-cdk/assertions'; +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import * as sns from '@aws-cdk/aws-sns'; +import * as cdk from '@aws-cdk/core'; +import * as actions from '../../lib'; + +const SNS_TOPIC_ARN = 'arn:aws:sns::123456789012:test-topic'; + +let stack: cdk.Stack; +let topicRule: iot.TopicRule; +let snsTopic: sns.ITopic; + +beforeEach(() => { + stack = new cdk.Stack(); + topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), + }); + snsTopic = sns.Topic.fromTopicArn(stack, 'MySnsTopic', SNS_TOPIC_ARN); +}); + +test('Default SNS topic action', () => { + // WHEN + topicRule.addAction(new actions.SnsTopicAction(snsTopic)); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [{ + Sns: { + RoleArn: { 'Fn::GetAtt': ['MyTopicRuleTopicRuleActionRoleCE2D05DA', 'Arn'] }, + TargetArn: SNS_TOPIC_ARN, + }, + }], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [{ + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { Service: 'iot.amazonaws.com' }, + }], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Action: 'sns:Publish', + Effect: 'Allow', + Resource: SNS_TOPIC_ARN, + }], + }, + Roles: [{ Ref: 'MyTopicRuleTopicRuleActionRoleCE2D05DA' }], + }); +}); + +test('Can set messageFormat', () => { + // WHEN + topicRule.addAction(new actions.SnsTopicAction(snsTopic, { + messageFormat: actions.SnsActionMessageFormat.JSON, + })); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + Match.objectLike({ Sns: { MessageFormat: 'JSON' } }), + ], + }, + }); +}); + +test('Can set role', () => { + // GIVEN + const roleArn = 'arn:aws:iam::123456789012:role/testrole'; + const role = iam.Role.fromRoleArn(stack, 'MyRole', roleArn); + + // WHEN + topicRule.addAction(new actions.SnsTopicAction(snsTopic, { + role, + })); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + Match.objectLike({ Sns: { RoleArn: roleArn } }), + ], + }, + }); +}); + +test('Action with FIFO topic throws error', () => { + // GIVEN + const snsFifoTopic = sns.Topic.fromTopicArn(stack, 'MyFifoTopic', `${SNS_TOPIC_ARN}.fifo`); + + expect(() => { + topicRule.addAction(new actions.SnsTopicAction(snsFifoTopic)); + }).toThrowError('IoT Rule actions cannot be used with FIFO SNS Topics, please pass a non-FIFO Topic instead'); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot/README.md b/packages/@aws-cdk/aws-iot/README.md index cddb8ae91cf5c..410657b9ad71d 100644 --- a/packages/@aws-cdk/aws-iot/README.md +++ b/packages/@aws-cdk/aws-iot/README.md @@ -34,7 +34,7 @@ $ npm i @aws-cdk/aws-iot Import it into your code: -```ts +```ts nofixture import * as iot from '@aws-cdk/aws-iot'; ``` @@ -44,10 +44,6 @@ Create a topic rule that give your devices the ability to interact with AWS serv You can create a topic rule with an action that invoke the Lambda action as following: ```ts -import * as iot from '@aws-cdk/aws-iot'; -import * as actions from '@aws-cdk/aws-iot-actions'; -import * as lambda from '@aws-cdk/aws-lambda'; - const func = new lambda.Function(this, 'MyFunction', { runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', @@ -69,18 +65,18 @@ new iot.TopicRule(this, 'TopicRule', { Or, you can add an action after constructing the `TopicRule` instance as following: ```ts +declare const func: lambda.Function; + const topicRule = new iot.TopicRule(this, 'TopicRule', { sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, timestamp() as timestamp FROM 'device/+/data'"), }); -topicRule.addAction(new actions.LambdaFunctionAction(func)) +topicRule.addAction(new actions.LambdaFunctionAction(func)); ``` You can also supply `errorAction` as following, and the IoT Rule will trigger it if a rule's action is unable to perform: ```ts -import * as iot from '@aws-cdk/aws-iot'; -import * as actions from '@aws-cdk/aws-iot-actions'; import * as logs from '@aws-cdk/aws-logs'; const logGroup = new logs.LogGroup(this, 'MyLogGroup'); diff --git a/packages/@aws-cdk/aws-iot/package.json b/packages/@aws-cdk/aws-iot/package.json index 2c6c4eb64f4bf..e83a310c74109 100644 --- a/packages/@aws-cdk/aws-iot/package.json +++ b/packages/@aws-cdk/aws-iot/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -77,8 +84,8 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-iot/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-iot/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..75c7b4d0eeca0 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/rosetta/default.ts-fixture @@ -0,0 +1,14 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as actions from '@aws-cdk/aws-iot-actions'; +import * as iot from '@aws-cdk/aws-iot'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as s3 from '@aws-cdk/aws-s3'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot1click/package.json b/packages/@aws-cdk/aws-iot1click/package.json index 437477bad2455..d0550686c0a7f 100644 --- a/packages/@aws-cdk/aws-iot1click/package.json +++ b/packages/@aws-cdk/aws-iot1click/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -77,7 +84,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-iotanalytics/package.json b/packages/@aws-cdk/aws-iotanalytics/package.json index 0aca49562ccb7..081930aad26ad 100644 --- a/packages/@aws-cdk/aws-iotanalytics/package.json +++ b/packages/@aws-cdk/aws-iotanalytics/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-iotcoredeviceadvisor/package.json b/packages/@aws-cdk/aws-iotcoredeviceadvisor/package.json index e7882cffb091d..15acc04b2e0f6 100644 --- a/packages/@aws-cdk/aws-iotcoredeviceadvisor/package.json +++ b/packages/@aws-cdk/aws-iotcoredeviceadvisor/package.json @@ -7,6 +7,13 @@ "jsii": { "outdir": "dist", "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + }, "targets": { "dotnet": { "namespace": "Amazon.CDK.AWS.IoTCoreDeviceAdvisor", @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-iotevents-actions/.eslintrc.js b/packages/@aws-cdk/aws-iotevents-actions/.eslintrc.js new file mode 100644 index 0000000000000..2658ee8727166 --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents-actions/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-iotevents-actions/.gitignore b/packages/@aws-cdk/aws-iotevents-actions/.gitignore new file mode 100644 index 0000000000000..266c0684c6844 --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents-actions/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +nyc.config.js +.LAST_PACKAGE +*.snk +!.eslintrc.js +!jest.config.js + +junit.xml diff --git a/packages/@aws-cdk/aws-iotevents-actions/.npmignore b/packages/@aws-cdk/aws-iotevents-actions/.npmignore new file mode 100644 index 0000000000000..9e51b66a1dba9 --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents-actions/.npmignore @@ -0,0 +1,28 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml +test/ +!*.lit.ts diff --git a/packages/@aws-cdk/aws-iotevents-actions/LICENSE b/packages/@aws-cdk/aws-iotevents-actions/LICENSE new file mode 100644 index 0000000000000..82ad00bb02d0b --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents-actions/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-iotevents-actions/NOTICE b/packages/@aws-cdk/aws-iotevents-actions/NOTICE new file mode 100644 index 0000000000000..1b7adbb891265 --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents-actions/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-iotevents-actions/README.md b/packages/@aws-cdk/aws-iotevents-actions/README.md new file mode 100644 index 0000000000000..4ee5362b7cc9b --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents-actions/README.md @@ -0,0 +1,20 @@ +# Actions for AWS::IoTEvents Detector Model + + +--- + +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + +--- + + + +This library contains integration classes to specify actions of state events of Detector Model in `@aws-cdk/aws-iotevents`. +Instances of these classes should be passed to `State` defined in `@aws-cdk/aws-iotevents` +You can define built-in actions to use a timer or set a variable, or send data to other AWS resources. diff --git a/packages/@aws-cdk/aws-iotevents-actions/jest.config.js b/packages/@aws-cdk/aws-iotevents-actions/jest.config.js new file mode 100644 index 0000000000000..3a2fd93a1228a --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents-actions/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-iotevents-actions/lib/index.ts b/packages/@aws-cdk/aws-iotevents-actions/lib/index.ts new file mode 100644 index 0000000000000..3e4b0ef0a73d4 --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents-actions/lib/index.ts @@ -0,0 +1,2 @@ +// this is placeholder for monocdk +export const dummy = true; diff --git a/packages/@aws-cdk/aws-iotevents-actions/package.json b/packages/@aws-cdk/aws-iotevents-actions/package.json new file mode 100644 index 0000000000000..457af7c3df4ae --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents-actions/package.json @@ -0,0 +1,98 @@ +{ + "name": "@aws-cdk/aws-iotevents-actions", + "version": "0.0.0", + "description": "Receipt Detector Model actions for AWS IoT Events", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awscdk.services.iotevents.actions", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "iotevents-actions" + } + }, + "dotnet": { + "namespace": "Amazon.CDK.AWS.IoTEvents.Actions", + "packageId": "Amazon.CDK.AWS.IoTEvents.Actions", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-cdk.aws-iotevents-actions", + "module": "aws_cdk.aws_iotevents_actions", + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 1" + ] + } + }, + "projectReferences": true + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-iotevents-actions" + }, + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "build+test+package": "yarn build+test && yarn package", + "build+test": "yarn build && yarn test", + "compat": "cdk-compat", + "rosetta:extract": "yarn --silent jsii-rosetta extract", + "build+extract": "yarn build && yarn rosetta:extract", + "build+test+extract": "yarn build+test && yarn rosetta:extract" + }, + "cdk-build": { + "env": { + "AWSLINT_BASE_CONSTRUCT": true + } + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "AWS::IoTEvents", + "aws-iotevents-actions" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assertions": "0.0.0", + "@aws-cdk/cdk-build-tools": "0.0.0", + "@aws-cdk/cdk-integ-tools": "0.0.0", + "@aws-cdk/pkglint": "0.0.0", + "@types/jest": "^27.4.1", + "jest": "^27.5.1" + }, + "dependencies": { + "@aws-cdk/core": "0.0.0" + }, + "homepage": "https://github.com/aws/aws-cdk", + "peerDependencies": { + "@aws-cdk/core": "0.0.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "experimental", + "awscdkio": { + "announce": false + }, + "publishConfig": { + "tag": "latest" + } +} diff --git a/packages/@aws-cdk/aws-iotevents-actions/test/iotevents-actions.test.ts b/packages/@aws-cdk/aws-iotevents-actions/test/iotevents-actions.test.ts new file mode 100644 index 0000000000000..465c7bdea0693 --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents-actions/test/iotevents-actions.test.ts @@ -0,0 +1,6 @@ +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-iotevents/README.md b/packages/@aws-cdk/aws-iotevents/README.md index 6dc6a681636cc..809bac071ef7d 100644 --- a/packages/@aws-cdk/aws-iotevents/README.md +++ b/packages/@aws-cdk/aws-iotevents/README.md @@ -40,15 +40,65 @@ Import it into your code: import * as iotevents from '@aws-cdk/aws-iotevents'; ``` -## `Input` +## `DetectorModel` -Add an AWS IoT Events input to your stack: +The following example creates an AWS IoT Events detector model to your stack. +The detector model need a reference to at least one AWS IoT Events input. +AWS IoT Events inputs enable the detector to get MQTT payload values from IoT Core rules. ```ts import * as iotevents from '@aws-cdk/aws-iotevents'; -new iotevents.Input(this, 'MyInput', { - inputName: 'my_input', - attributeJsonPaths: ['payload.temperature'], +const input = new iotevents.Input(this, 'MyInput', { + inputName: 'my_input', // optional + attributeJsonPaths: ['payload.deviceId', 'payload.temperature'], }); + +const warmState = new iotevents.State({ + stateName: 'warm', + onEnter: [{ + eventName: 'test-event', + condition: iotevents.Expression.currentInput(input), + }], +}); +const coldState = new iotevents.State({ + stateName: 'cold', +}); + +// transit to coldState when temperature is 10 +warmState.transitionTo(coldState, { + eventName: 'to_coldState', // optional property, default by combining the names of the States + when: iotevents.Expression.eq( + iotevents.Expression.inputAttribute(input, 'payload.temperature'), + iotevents.Expression.fromString('10'), + ), +}); +// transit to warmState when temperature is 20 +coldState.transitionTo(warmState, { + when: iotevents.Expression.eq( + iotevents.Expression.inputAttribute(input, 'payload.temperature'), + iotevents.Expression.fromString('20'), + ), +}); + +new iotevents.DetectorModel(this, 'MyDetectorModel', { + detectorModelName: 'test-detector-model', // optional + description: 'test-detector-model-description', // optional property, default is none + evaluationMethod: iotevents.EventEvaluation.SERIAL, // optional property, default is iotevents.EventEvaluation.BATCH + detectorKey: 'payload.deviceId', // optional property, default is none and single detector instance will be created and all inputs will be routed to it + initialState: warmState, +}); +``` + +To grant permissions to put messages in the input, +you can use the `grantWrite()` method: + +```ts +import * as iam from '@aws-cdk/aws-iam'; +import * as iotevents from '@aws-cdk/aws-iotevents'; + +declare const grantable: iam.IGrantable; +const input = iotevents.Input.fromInputName(this, 'MyInput', 'my_input'); + +input.grantWrite(grantable); ``` diff --git a/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts b/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts new file mode 100644 index 0000000000000..35128bc4531e6 --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts @@ -0,0 +1,134 @@ +import * as iam from '@aws-cdk/aws-iam'; +import { Resource, IResource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnDetectorModel } from './iotevents.generated'; +import { State } from './state'; + +/** + * Represents an AWS IoT Events detector model. + */ +export interface IDetectorModel extends IResource { + /** + * The name of the detector model. + * + * @attribute + */ + readonly detectorModelName: string; +} + +/** + * Information about the order in which events are evaluated and how actions are executed. + */ +export enum EventEvaluation { + /** + * When setting to BATCH, variables within a state are updated and events within a state are + * performed only after all event conditions are evaluated. + */ + BATCH = 'BATCH', + + /** + * When setting to SERIAL, variables are updated and event conditions are evaluated in the order + * that the events are defined. + */ + SERIAL = 'SERIAL', +} + +/** + * Properties for defining an AWS IoT Events detector model. + */ +export interface DetectorModelProps { + /** + * The name of the detector model. + * + * @default - CloudFormation will generate a unique name of the detector model + */ + readonly detectorModelName?: string; + + /** + * A brief description of the detector model. + * + * @default none + */ + readonly description?: string; + + /** + * Information about the order in which events are evaluated and how actions are executed. + * + * When setting to SERIAL, variables are updated and event conditions are evaluated in the order + * that the events are defined. + * When setting to BATCH, variables within a state are updated and events within a state are + * performed only after all event conditions are evaluated. + * + * @default EventEvaluation.BATCH + */ + readonly evaluationMethod?: EventEvaluation; + + /** + * The value used to identify a detector instance. When a device or system sends input, a new + * detector instance with a unique key value is created. AWS IoT Events can continue to route + * input to its corresponding detector instance based on this identifying information. + * + * This parameter uses a JSON-path expression to select the attribute-value pair in the message + * payload that is used for identification. To route the message to the correct detector instance, + * the device must send a message payload that contains the same attribute-value. + * + * @default - none (single detector instance will be created and all inputs will be routed to it) + */ + readonly detectorKey?: string; + + /** + * The state that is entered at the creation of each detector. + */ + readonly initialState: State; + + /** + * The role that grants permission to AWS IoT Events to perform its operations. + * + * @default - a role will be created with default permissions + */ + readonly role?: iam.IRole; +} + +/** + * Defines an AWS IoT Events detector model in this stack. + */ +export class DetectorModel extends Resource implements IDetectorModel { + /** + * Import an existing detector model. + */ + public static fromDetectorModelName(scope: Construct, id: string, detectorModelName: string): IDetectorModel { + return new class extends Resource implements IDetectorModel { + public readonly detectorModelName = detectorModelName; + }(scope, id); + } + + public readonly detectorModelName: string; + + constructor(scope: Construct, id: string, props: DetectorModelProps) { + super(scope, id, { + physicalName: props.detectorModelName, + }); + + if (!props.initialState._onEnterEventsHaveAtLeastOneCondition()) { + throw new Error('Detector Model must have at least one Input with a condition'); + } + + const role = props.role ?? new iam.Role(this, 'DetectorModelRole', { + assumedBy: new iam.ServicePrincipal('iotevents.amazonaws.com'), + }); + + const resource = new CfnDetectorModel(this, 'Resource', { + detectorModelName: this.physicalName, + detectorModelDescription: props.description, + evaluationMethod: props.evaluationMethod, + key: props.detectorKey, + detectorModelDefinition: { + initialStateName: props.initialState.stateName, + states: props.initialState._collectStateJsons(new Set()), + }, + roleArn: role.roleArn, + }); + + this.detectorModelName = this.getResourceNameAttribute(resource.ref); + } +} diff --git a/packages/@aws-cdk/aws-iotevents/lib/event.ts b/packages/@aws-cdk/aws-iotevents/lib/event.ts new file mode 100644 index 0000000000000..610469db9c32c --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents/lib/event.ts @@ -0,0 +1,18 @@ +import { Expression } from './expression'; + +/** + * Specifies the actions to be performed when the condition evaluates to TRUE. + */ +export interface Event { + /** + * The name of the event. + */ + readonly eventName: string; + + /** + * The Boolean expression that, when TRUE, causes the actions to be performed. + * + * @default - none (the actions are always executed) + */ + readonly condition?: Expression; +} diff --git a/packages/@aws-cdk/aws-iotevents/lib/expression.ts b/packages/@aws-cdk/aws-iotevents/lib/expression.ts new file mode 100644 index 0000000000000..fd686e9761802 --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents/lib/expression.ts @@ -0,0 +1,75 @@ +import { IInput } from './input'; + +/** + * Expression for events in Detector Model state. + * @see https://docs.aws.amazon.com/iotevents/latest/developerguide/iotevents-expressions.html + */ +export abstract class Expression { + /** + * Create a expression from the given string. + */ + public static fromString(value: string): Expression { + return new StringExpression(value); + } + + /** + * Create a expression for function `currentInput()`. + * It is evaluated to true if the specified input message was received. + */ + public static currentInput(input: IInput): Expression { + return this.fromString(`currentInput("${input.inputName}")`); + } + + /** + * Create a expression for get an input attribute as `$input.TemperatureInput.temperatures[2]`. + */ + public static inputAttribute(input: IInput, path: string): Expression { + return this.fromString(`$input.${input.inputName}.${path}`); + } + + /** + * Create a expression for the Equal operator. + */ + public static eq(left: Expression, right: Expression): Expression { + return new BinaryOperationExpression(left, '==', right); + } + + /** + * Create a expression for the AND operator. + */ + public static and(left: Expression, right: Expression): Expression { + return new BinaryOperationExpression(left, '&&', right); + } + + constructor() { + } + + /** + * This is called to evaluate the expression. + */ + public abstract evaluate(): string; +} + +class StringExpression extends Expression { + constructor(private readonly value: string) { + super(); + } + + public evaluate() { + return this.value; + } +} + +class BinaryOperationExpression extends Expression { + constructor( + private readonly left: Expression, + private readonly operator: string, + private readonly right: Expression, + ) { + super(); + } + + public evaluate() { + return `${this.left.evaluate()} ${this.operator} ${this.right.evaluate()}`; + } +} diff --git a/packages/@aws-cdk/aws-iotevents/lib/index.ts b/packages/@aws-cdk/aws-iotevents/lib/index.ts index 3851e30984391..24913635ebe50 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/index.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/index.ts @@ -1,4 +1,8 @@ +export * from './detector-model'; +export * from './event'; +export * from './expression'; export * from './input'; +export * from './state'; // AWS::IoTEvents CloudFormation Resources: export * from './iotevents.generated'; diff --git a/packages/@aws-cdk/aws-iotevents/lib/input.ts b/packages/@aws-cdk/aws-iotevents/lib/input.ts index e4bba5684b7a4..b656af2d4dff6 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/input.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/input.ts @@ -1,24 +1,66 @@ -import { Resource, IResource } from '@aws-cdk/core'; +import * as iam from '@aws-cdk/aws-iam'; +import { Resource, IResource, Aws } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnInput } from './iotevents.generated'; /** - * Represents an AWS IoT Events input + * Represents an AWS IoT Events input. */ export interface IInput extends IResource { /** - * The name of the input + * The name of the input. + * * @attribute */ readonly inputName: string; + + /** + * The ARN of the input. + * + * @attribute + */ + readonly inputArn: string; + + /** + * Grant write permissions on this input and its contents to an IAM principal (Role/Group/User). + * + * @param grantee the principal + */ + grantWrite(grantee: iam.IGrantable): iam.Grant + + /** + * Grant the indicated permissions on this input to the given IAM principal (Role/Group/User). + * + * @param grantee the principal + * @param actions the set of actions to allow (i.e. "iotevents:BatchPutMessage") + */ + grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant +} + +abstract class InputBase extends Resource implements IInput { + public abstract readonly inputName: string; + + public abstract readonly inputArn: string; + + public grantWrite(grantee: iam.IGrantable) { + return this.grant(grantee, 'iotevents:BatchPutMessage'); + } + + public grant(grantee: iam.IGrantable, ...actions: string[]) { + return iam.Grant.addToPrincipal({ + grantee, + actions, + resourceArns: [this.inputArn], + }); + } } /** - * Properties for defining an AWS IoT Events input + * Properties for defining an AWS IoT Events input. */ export interface InputProps { /** - * The name of the input + * The name of the input. * * @default - CloudFormation will generate a unique name of the input */ @@ -37,19 +79,25 @@ export interface InputProps { /** * Defines an AWS IoT Events input in this stack. */ -export class Input extends Resource implements IInput { +export class Input extends InputBase { /** - * Import an existing input + * Import an existing input. */ public static fromInputName(scope: Construct, id: string, inputName: string): IInput { - class Import extends Resource implements IInput { + return new class Import extends InputBase { public readonly inputName = inputName; - } - return new Import(scope, id); + public readonly inputArn = this.stack.formatArn({ + service: 'iotevents', + resource: 'input', + resourceName: inputName, + }); + }(scope, id); } public readonly inputName: string; + public readonly inputArn: string; + constructor(scope: Construct, id: string, props: InputProps) { super(scope, id, { physicalName: props.inputName, @@ -67,5 +115,14 @@ export class Input extends Resource implements IInput { }); this.inputName = this.getResourceNameAttribute(resource.ref); + this.inputArn = this.getResourceArnAttribute(arnForInput(resource.ref), { + service: 'iotevents', + resource: 'input', + resourceName: this.physicalName, + }); } } + +function arnForInput(inputName: string): string { + return `arn:${Aws.PARTITION}:iotevents:${Aws.REGION}:${Aws.ACCOUNT_ID}:input/${inputName}`; +} diff --git a/packages/@aws-cdk/aws-iotevents/lib/state.ts b/packages/@aws-cdk/aws-iotevents/lib/state.ts new file mode 100644 index 0000000000000..67ee6a32802ec --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents/lib/state.ts @@ -0,0 +1,155 @@ +import { Event } from './event'; +import { Expression } from './expression'; +import { CfnDetectorModel } from './iotevents.generated'; + +/** + * Properties for options of state transition. + */ +export interface TransitionOptions { + /** + * The name of the event. + * + * @default string combining the names of the States as `${originStateName}_to_${targetStateName}` + */ + readonly eventName?: string; + + /** + * The condition that is used to determine to cause the state transition and the actions. + * When this was evaluated to TRUE, the state transition and the actions are triggered. + */ + readonly when: Expression; +} + +/** + * Specifies the state transition and the actions to be performed when the condition evaluates to TRUE. + */ +interface TransitionEvent { + /** + * The name of the event. + */ + readonly eventName: string; + + /** + * The Boolean expression that, when TRUE, causes the state transition and the actions to be performed. + */ + readonly condition: Expression; + + /** + * The next state to transit to. When the resuld of condition expression is TRUE, the state is transited. + */ + readonly nextState: State; +} + +/** + * Properties for defining a state of a detector. + */ +export interface StateProps { + /** + * The name of the state. + */ + readonly stateName: string; + + /** + * Specifies the events on enter. the conditions of the events are evaluated when the state is entered. + * If the condition is `TRUE`, the actions of the event are performed. + * + * @default - events on enter will not be set + */ + readonly onEnter?: Event[]; +} + +/** + * Defines a state of a detector. + */ +export class State { + /** + * The name of the state. + */ + public readonly stateName: string; + + private readonly transitionEvents: TransitionEvent[] = []; + + constructor(private readonly props: StateProps) { + this.stateName = props.stateName; + } + + /** + * Add a transition event to the state. + * The transition event will be triggered if condition is evaluated to TRUE. + * + * @param targetState the state that will be transit to when the event triggered + * @param options transition options including the condition that causes the state transition + */ + public transitionTo(targetState: State, options: TransitionOptions) { + const alreadyAdded = this.transitionEvents.some(transitionEvent => transitionEvent.nextState === targetState); + if (alreadyAdded) { + throw new Error(`State '${this.stateName}' already has a transition defined to '${targetState.stateName}'`); + } + + this.transitionEvents.push({ + eventName: options.eventName ?? `${this.stateName}_to_${targetState.stateName}`, + nextState: targetState, + condition: options.when, + }); + } + + /** + * Collect states in dependency gragh that constructed by state transitions, + * and return the JSONs of the states. + * This function is called recursively and collect the states. + * + * @internal + */ + public _collectStateJsons(collectedStates: Set): CfnDetectorModel.StateProperty[] { + if (collectedStates.has(this)) { + return []; + } + collectedStates.add(this); + + return [ + this.toStateJson(), + ...this.transitionEvents.flatMap(transitionEvent => { + return transitionEvent.nextState._collectStateJsons(collectedStates); + }), + ]; + } + + /** + * Returns true if this state has at least one condition via events. + * + * @internal + */ + public _onEnterEventsHaveAtLeastOneCondition(): boolean { + return this.props.onEnter?.some(event => event.condition) ?? false; + } + + private toStateJson(): CfnDetectorModel.StateProperty { + const { onEnter } = this.props; + return { + stateName: this.stateName, + onEnter: onEnter && { events: toEventsJson(onEnter) }, + onInput: { + transitionEvents: toTransitionEventsJson(this.transitionEvents), + }, + }; + } +} + +function toEventsJson(events: Event[]): CfnDetectorModel.EventProperty[] { + return events.map(event => ({ + eventName: event.eventName, + condition: event.condition?.evaluate(), + })); +} + +function toTransitionEventsJson(transitionEvents: TransitionEvent[]): CfnDetectorModel.TransitionEventProperty[] | undefined { + if (transitionEvents.length === 0) { + return undefined; + } + + return transitionEvents.map(transitionEvent => ({ + eventName: transitionEvent.eventName, + condition: transitionEvent.condition.evaluate(), + nextState: transitionEvent.nextState.stateName, + })); +} diff --git a/packages/@aws-cdk/aws-iotevents/package.json b/packages/@aws-cdk/aws-iotevents/package.json index 7b3792c5015b4..3b3b352afe429 100644 --- a/packages/@aws-cdk/aws-iotevents/package.json +++ b/packages/@aws-cdk/aws-iotevents/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -79,14 +86,16 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { + "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, "peerDependencies": { + "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, diff --git a/packages/@aws-cdk/aws-iotevents/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-iotevents/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..50d86e8a055ce --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents/rosetta/default.ts-fixture @@ -0,0 +1,10 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts b/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts new file mode 100644 index 0000000000000..c90a10cf34374 --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts @@ -0,0 +1,425 @@ +import { Match, Template } from '@aws-cdk/assertions'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import * as iotevents from '../lib'; + +let stack: cdk.Stack; +let input: iotevents.IInput; +beforeEach(() => { + stack = new cdk.Stack(); + input = iotevents.Input.fromInputName(stack, 'MyInput', 'test-input'); +}); + +test('Default property', () => { + // WHEN + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName', + condition: iotevents.Expression.fromString('test-eventCondition'), + }], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelDefinition: { + InitialStateName: 'test-state', + States: [{ + StateName: 'test-state', + OnEnter: { + Events: [{ + EventName: 'test-eventName', + Condition: 'test-eventCondition', + }], + }, + }], + }, + RoleArn: { + 'Fn::GetAtt': ['MyDetectorModelDetectorModelRoleF2FB4D88', 'Arn'], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [{ + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { Service: 'iotevents.amazonaws.com' }, + }], + }, + }); +}); + +test('can get detector model name', () => { + // GIVEN + const detectorModel = new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName', + condition: iotevents.Expression.fromString('test-eventCondition'), + }], + }), + }); + + // WHEN + new cdk.CfnResource(stack, 'Res', { + type: 'Test::Resource', + properties: { + DetectorModelName: detectorModel.detectorModelName, + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('Test::Resource', { + DetectorModelName: { Ref: 'MyDetectorModel559C0B0E' }, + }); +}); + +test.each([ + ['physical name', { detectorModelName: 'test-detector-model' }, { DetectorModelName: 'test-detector-model' }], + ['description', { description: 'test-detector-model-description' }, { DetectorModelDescription: 'test-detector-model-description' }], + ['evaluationMethod', { evaluationMethod: iotevents.EventEvaluation.SERIAL }, { EvaluationMethod: 'SERIAL' }], + ['detectorKey', { detectorKey: 'payload.deviceId' }, { Key: 'payload.deviceId' }], +])('can set %s', (_, partialProps, expected) => { + // WHEN + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + ...partialProps, + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName', + condition: iotevents.Expression.fromString('test-eventCondition'), + }], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', expected); +}); + +test('can set multiple events to State', () => { + // WHEN + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [ + { + eventName: 'test-eventName1', + condition: iotevents.Expression.fromString('test-eventCondition'), + }, + { + eventName: 'test-eventName2', + }, + ], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelDefinition: { + States: [ + Match.objectLike({ + OnEnter: { + Events: [ + { + EventName: 'test-eventName1', + Condition: 'test-eventCondition', + }, + { + EventName: 'test-eventName2', + }, + ], + }, + }), + ], + }, + }); +}); + +test('can set states with transitions', () => { + // GIVEN + const firstState = new iotevents.State({ + stateName: 'firstState', + onEnter: [{ + eventName: 'test-eventName', + condition: iotevents.Expression.currentInput(input), + }], + }); + const secondState = new iotevents.State({ + stateName: 'secondState', + }); + const thirdState = new iotevents.State({ + stateName: 'thirdState', + }); + + // WHEN + // transition as 1st -> 2nd + firstState.transitionTo(secondState, { + when: iotevents.Expression.eq( + iotevents.Expression.inputAttribute(input, 'payload.temperature'), + iotevents.Expression.fromString('12'), + ), + }); + // transition as 2nd -> 1st, make circular reference + secondState.transitionTo(firstState, { + eventName: 'secondToFirst', + when: iotevents.Expression.eq( + iotevents.Expression.inputAttribute(input, 'payload.temperature'), + iotevents.Expression.fromString('21'), + ), + }); + // transition as 2nd -> 3rd, to test recursive calling + secondState.transitionTo(thirdState, { + when: iotevents.Expression.eq( + iotevents.Expression.inputAttribute(input, 'payload.temperature'), + iotevents.Expression.fromString('23'), + ), + }); + + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: firstState, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelDefinition: { + States: [ + { + StateName: 'firstState', + OnInput: { + TransitionEvents: [{ + EventName: 'firstState_to_secondState', + NextState: 'secondState', + Condition: '$input.test-input.payload.temperature == 12', + }], + }, + }, + { + StateName: 'secondState', + OnInput: { + TransitionEvents: [ + { + EventName: 'secondToFirst', + NextState: 'firstState', + Condition: '$input.test-input.payload.temperature == 21', + }, + { + EventName: 'secondState_to_thirdState', + NextState: 'thirdState', + Condition: '$input.test-input.payload.temperature == 23', + }, + ], + }, + }, + { + StateName: 'thirdState', + }, + ], + }, + }); +}); + +test('can set role', () => { + // WHEN + const role = iam.Role.fromRoleArn(stack, 'test-role', 'arn:aws:iam::123456789012:role/ForTest'); + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + role, + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName', + condition: iotevents.Expression.fromString('test-eventCondition'), + }], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + RoleArn: 'arn:aws:iam::123456789012:role/ForTest', + }); +}); + +test('can import a DetectorModel by detectorModelName', () => { + // WHEN + const detectorModelName = 'detector-model-name'; + const detectorModel = iotevents.DetectorModel.fromDetectorModelName(stack, 'ExistingDetectorModel', detectorModelName); + + // THEN + expect(detectorModel).toMatchObject({ + detectorModelName: detectorModelName, + }); +}); + +test('cannot create without condition', () => { + expect(() => { + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName', + }], + }), + }); + }).toThrow('Detector Model must have at least one Input with a condition'); +}); + +test('cannot create without event', () => { + expect(() => { + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + }), + }); + }).toThrow('Detector Model must have at least one Input with a condition'); +}); + +test('cannot create transitions that transit to duprecated target state', () => { + const firstState = new iotevents.State({ + stateName: 'firstState', + onEnter: [{ + eventName: 'test-eventName', + }], + }); + const secondState = new iotevents.State({ + stateName: 'secondState', + }); + + firstState.transitionTo(secondState, { + when: iotevents.Expression.eq( + iotevents.Expression.inputAttribute(input, 'payload.temperature'), + iotevents.Expression.fromString('12.1'), + ), + }); + + expect(() => { + firstState.transitionTo(secondState, { + when: iotevents.Expression.eq( + iotevents.Expression.inputAttribute(input, 'payload.temperature'), + iotevents.Expression.fromString('12.2'), + ), + }); + }).toThrow("State 'firstState' already has a transition defined to 'secondState'"); +}); + +describe('Expression', () => { + test('currentInput', () => { + // WHEN + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName', + condition: iotevents.Expression.currentInput(input), + }], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelDefinition: { + States: [ + Match.objectLike({ + OnEnter: { + Events: [Match.objectLike({ + Condition: 'currentInput("test-input")', + })], + }, + }), + ], + }, + }); + }); + + test('inputAttribute', () => { + // WHEN + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName', + condition: iotevents.Expression.inputAttribute(input, 'json.path'), + }], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelDefinition: { + States: [ + Match.objectLike({ + OnEnter: { + Events: [Match.objectLike({ + Condition: '$input.test-input.json.path', + })], + }, + }), + ], + }, + }); + }); + + test('eq', () => { + // WHEN + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName', + condition: iotevents.Expression.eq( + iotevents.Expression.fromString('"aaa"'), + iotevents.Expression.fromString('"bbb"'), + ), + }], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelDefinition: { + States: [ + Match.objectLike({ + OnEnter: { + Events: [Match.objectLike({ + Condition: '"aaa" == "bbb"', + })], + }, + }), + ], + }, + }); + }); + + test('eq', () => { + // WHEN + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName', + condition: iotevents.Expression.and( + iotevents.Expression.fromString('true'), + iotevents.Expression.fromString('false'), + ), + }], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelDefinition: { + States: [ + Match.objectLike({ + OnEnter: { + Events: [Match.objectLike({ + Condition: 'true && false', + })], + }, + }), + ], + }, + }); + }); +}); diff --git a/packages/@aws-cdk/aws-iotevents/test/input.test.ts b/packages/@aws-cdk/aws-iotevents/test/input.test.ts index 11b457bb0cf1b..8907489af928e 100644 --- a/packages/@aws-cdk/aws-iotevents/test/input.test.ts +++ b/packages/@aws-cdk/aws-iotevents/test/input.test.ts @@ -1,10 +1,14 @@ import { Template } from '@aws-cdk/assertions'; +import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as iotevents from '../lib'; -test('Default property', () => { - const stack = new cdk.Stack(); +let stack: cdk.Stack; +beforeEach(() => { + stack = new cdk.Stack(); +}); +test('Default property', () => { // WHEN new iotevents.Input(stack, 'MyInput', { attributeJsonPaths: ['payload.temperature'], @@ -19,7 +23,6 @@ test('Default property', () => { }); test('can get input name', () => { - const stack = new cdk.Stack(); // GIVEN const input = new iotevents.Input(stack, 'MyInput', { attributeJsonPaths: ['payload.temperature'], @@ -39,9 +42,38 @@ test('can get input name', () => { }); }); -test('can set physical name', () => { - const stack = new cdk.Stack(); +test('can get input ARN', () => { + // GIVEN + const input = new iotevents.Input(stack, 'MyInput', { + attributeJsonPaths: ['payload.temperature'], + }); + // WHEN + new cdk.CfnResource(stack, 'Res', { + type: 'Test::Resource', + properties: { + InputArn: input.inputArn, + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('Test::Resource', { + InputArn: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iotevents:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':input/', + { Ref: 'MyInput08947B23' }, + ]], + }, + }); +}); + +test('can set physical name', () => { // WHEN new iotevents.Input(stack, 'MyInput', { inputName: 'test_input', @@ -55,8 +87,6 @@ test('can set physical name', () => { }); test('can import a Input by inputName', () => { - const stack = new cdk.Stack(); - // WHEN const inputName = 'test-input-name'; const topicRule = iotevents.Input.fromInputName(stack, 'InputFromInputName', inputName); @@ -68,11 +98,45 @@ test('can import a Input by inputName', () => { }); test('cannot be created with an empty array of attributeJsonPaths', () => { - const stack = new cdk.Stack(); - expect(() => { new iotevents.Input(stack, 'MyInput', { attributeJsonPaths: [], }); }).toThrow('attributeJsonPaths property cannot be empty'); }); + +test('can grant the permission to put message', () => { + const role = iam.Role.fromRoleArn(stack, 'MyRole', 'arn:aws:iam::account-id:role/role-name'); + const input = new iotevents.Input(stack, 'MyInput', { + attributeJsonPaths: ['payload.temperature'], + }); + + // WHEN + input.grantWrite(role); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'iotevents:BatchPutMessage', + Effect: 'Allow', + Resource: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iotevents:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':input/', + { Ref: 'MyInput08947B23' }, + ]], + }, + }, + ], + }, + PolicyName: 'MyRolePolicy64AB00A5', + Roles: ['role-name'], + }); +}); diff --git a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json index 1f5d452b5475d..888869a41e68e 100644 --- a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json +++ b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json @@ -5,6 +5,9 @@ "Properties": { "InputDefinition": { "Attributes": [ + { + "JsonPath": "payload.deviceId" + }, { "JsonPath": "payload.temperature" } @@ -12,6 +15,112 @@ }, "InputName": "test_input" } + }, + "MyDetectorModelDetectorModelRoleF2FB4D88": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iotevents.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyDetectorModel559C0B0E": { + "Type": "AWS::IoTEvents::DetectorModel", + "Properties": { + "DetectorModelDefinition": { + "InitialStateName": "online", + "States": [ + { + "OnEnter": { + "Events": [ + { + "Condition": { + "Fn::Join": [ + "", + [ + "currentInput(\"", + { + "Ref": "MyInput08947B23" + }, + "\") && $input.", + { + "Ref": "MyInput08947B23" + }, + ".payload.temperature == 31.5" + ] + ] + }, + "EventName": "test-event" + } + ] + }, + "OnInput": { + "TransitionEvents": [ + { + "Condition": { + "Fn::Join": [ + "", + [ + "$input.", + { + "Ref": "MyInput08947B23" + }, + ".payload.temperature == 12" + ] + ] + }, + "EventName": "online_to_offline", + "NextState": "offline" + } + ] + }, + "StateName": "online" + }, + { + "OnInput": { + "TransitionEvents": [ + { + "Condition": { + "Fn::Join": [ + "", + [ + "$input.", + { + "Ref": "MyInput08947B23" + }, + ".payload.temperature == 21" + ] + ] + }, + "EventName": "offline_to_online", + "NextState": "online" + } + ] + }, + "StateName": "offline" + } + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "MyDetectorModelDetectorModelRoleF2FB4D88", + "Arn" + ] + }, + "DetectorModelDescription": "test-detector-model-description", + "DetectorModelName": "test-detector-model", + "EvaluationMethod": "SERIAL", + "Key": "payload.deviceId" + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts index cb900c83a3f44..5f6d2839f3a93 100644 --- a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts +++ b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts @@ -1,18 +1,58 @@ import * as cdk from '@aws-cdk/core'; import * as iotevents from '../lib'; -const app = new cdk.App(); - class TestStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); - new iotevents.Input(this, 'MyInput', { + const input = new iotevents.Input(this, 'MyInput', { inputName: 'test_input', - attributeJsonPaths: ['payload.temperature'], + attributeJsonPaths: ['payload.deviceId', 'payload.temperature'], + }); + + const onlineState = new iotevents.State({ + stateName: 'online', + onEnter: [{ + eventName: 'test-event', + // meaning `condition: 'currentInput("test_input") && $input.test_input.payload.temperature == 31.5'` + condition: iotevents.Expression.and( + iotevents.Expression.currentInput(input), + iotevents.Expression.eq( + iotevents.Expression.inputAttribute(input, 'payload.temperature'), + iotevents.Expression.fromString('31.5'), + ), + ), + }], + }); + const offlineState = new iotevents.State({ + stateName: 'offline', + }); + + // 1st => 2nd + onlineState.transitionTo(offlineState, { + when: iotevents.Expression.eq( + iotevents.Expression.inputAttribute(input, 'payload.temperature'), + iotevents.Expression.fromString('12'), + ), + }); + // 2st => 1st + offlineState.transitionTo(onlineState, { + when: iotevents.Expression.eq( + iotevents.Expression.inputAttribute(input, 'payload.temperature'), + iotevents.Expression.fromString('21'), + ), + }); + + new iotevents.DetectorModel(this, 'MyDetectorModel', { + detectorModelName: 'test-detector-model', + description: 'test-detector-model-description', + evaluationMethod: iotevents.EventEvaluation.SERIAL, + detectorKey: 'payload.deviceId', + initialState: onlineState, }); } } -new TestStack(app, 'test-stack'); +const app = new cdk.App(); +new TestStack(app, 'detector-model-test-stack'); app.synth(); diff --git a/packages/@aws-cdk/aws-iotfleethub/package.json b/packages/@aws-cdk/aws-iotfleethub/package.json index 14ebd656515af..1908aae1aa512 100644 --- a/packages/@aws-cdk/aws-iotfleethub/package.json +++ b/packages/@aws-cdk/aws-iotfleethub/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-iotfleethub", "module": "aws_cdk.aws_iotfleethub" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-iotsitewise/package.json b/packages/@aws-cdk/aws-iotsitewise/package.json index 11d005fe1ece6..fabfefa58f1e9 100644 --- a/packages/@aws-cdk/aws-iotsitewise/package.json +++ b/packages/@aws-cdk/aws-iotsitewise/package.json @@ -28,6 +28,13 @@ "distName": "aws-cdk.aws-iotsitewise", "module": "aws_cdk.aws_iotsitewise" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-iotthingsgraph/package.json b/packages/@aws-cdk/aws-iotthingsgraph/package.json index 4df6b533355e9..62af6298210fa 100644 --- a/packages/@aws-cdk/aws-iotthingsgraph/package.json +++ b/packages/@aws-cdk/aws-iotthingsgraph/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-iotwireless/package.json b/packages/@aws-cdk/aws-iotwireless/package.json index b347d3d8f778c..684d07c5bd136 100644 --- a/packages/@aws-cdk/aws-iotwireless/package.json +++ b/packages/@aws-cdk/aws-iotwireless/package.json @@ -28,6 +28,13 @@ "distName": "aws-cdk.aws-iotwireless", "module": "aws_cdk.aws_iotwireless" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-ivs/package.json b/packages/@aws-cdk/aws-ivs/package.json index 845d20b12229e..4f8235a849a03 100644 --- a/packages/@aws-cdk/aws-ivs/package.json +++ b/packages/@aws-cdk/aws-ivs/package.json @@ -28,6 +28,13 @@ "distName": "aws-cdk.aws-ivs", "module": "aws_cdk.aws_ivs" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "awslint": { @@ -89,7 +96,7 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-kafkaconnect/.eslintrc.js b/packages/@aws-cdk/aws-kafkaconnect/.eslintrc.js new file mode 100644 index 0000000000000..2658ee8727166 --- /dev/null +++ b/packages/@aws-cdk/aws-kafkaconnect/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-kafkaconnect/.gitignore b/packages/@aws-cdk/aws-kafkaconnect/.gitignore new file mode 100644 index 0000000000000..62ebc95d75ce6 --- /dev/null +++ b/packages/@aws-cdk/aws-kafkaconnect/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk +nyc.config.js +!.eslintrc.js +!jest.config.js +junit.xml diff --git a/packages/@aws-cdk/aws-kafkaconnect/.npmignore b/packages/@aws-cdk/aws-kafkaconnect/.npmignore new file mode 100644 index 0000000000000..f931fede67c44 --- /dev/null +++ b/packages/@aws-cdk/aws-kafkaconnect/.npmignore @@ -0,0 +1,29 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json + +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml +test/ +!*.lit.ts diff --git a/packages/@aws-cdk/aws-kafkaconnect/LICENSE b/packages/@aws-cdk/aws-kafkaconnect/LICENSE new file mode 100644 index 0000000000000..82ad00bb02d0b --- /dev/null +++ b/packages/@aws-cdk/aws-kafkaconnect/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-kafkaconnect/NOTICE b/packages/@aws-cdk/aws-kafkaconnect/NOTICE new file mode 100644 index 0000000000000..1b7adbb891265 --- /dev/null +++ b/packages/@aws-cdk/aws-kafkaconnect/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-kafkaconnect/README.md b/packages/@aws-cdk/aws-kafkaconnect/README.md new file mode 100644 index 0000000000000..79f7c2fc4b807 --- /dev/null +++ b/packages/@aws-cdk/aws-kafkaconnect/README.md @@ -0,0 +1,31 @@ +# AWS::KafkaConnect Construct Library + + +--- + +![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) + +> All classes with the `Cfn` prefix in this module ([CFN Resources]) are always stable and safe to use. +> +> [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib + +--- + + + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +```ts nofixture +import * as kafkaconnect from '@aws-cdk/aws-kafkaconnect'; +``` + + + +There are no hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet. +However, you can still use the automatically generated [L1](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_l1_using) constructs, and use this service exactly as you would using CloudFormation directly. + +For more information on the resources and properties available for this service, see the [CloudFormation documentation for AWS::KafkaConnect](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_KafkaConnect.html). + +(Read the [CDK Contributing Guide](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md) if you are interested in contributing to this construct library.) + + diff --git a/packages/@aws-cdk/aws-kafkaconnect/jest.config.js b/packages/@aws-cdk/aws-kafkaconnect/jest.config.js new file mode 100644 index 0000000000000..3a2fd93a1228a --- /dev/null +++ b/packages/@aws-cdk/aws-kafkaconnect/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-kafkaconnect/lib/index.ts b/packages/@aws-cdk/aws-kafkaconnect/lib/index.ts new file mode 100644 index 0000000000000..5745f9372ea21 --- /dev/null +++ b/packages/@aws-cdk/aws-kafkaconnect/lib/index.ts @@ -0,0 +1,2 @@ +// AWS::KafkaConnect CloudFormation Resources: +export * from './kafkaconnect.generated'; diff --git a/packages/@aws-cdk/aws-kafkaconnect/package.json b/packages/@aws-cdk/aws-kafkaconnect/package.json new file mode 100644 index 0000000000000..ec3eb36faaa42 --- /dev/null +++ b/packages/@aws-cdk/aws-kafkaconnect/package.json @@ -0,0 +1,110 @@ +{ + "name": "@aws-cdk/aws-kafkaconnect", + "version": "0.0.0", + "description": "AWS::KafkaConnect Construct Library", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "projectReferences": true, + "targets": { + "dotnet": { + "namespace": "Amazon.CDK.AWS.KafkaConnect", + "packageId": "Amazon.CDK.AWS.KafkaConnect", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "java": { + "package": "software.amazon.awscdk.services.kafkaconnect", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "kafkaconnect" + } + }, + "python": { + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 1" + ], + "distName": "aws-cdk.aws-kafkaconnect", + "module": "aws_cdk.aws_kafkaconnect" + } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-kafkaconnect" + }, + "homepage": "https://github.com/aws/aws-cdk", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "cfn2ts": "cfn2ts", + "build+test": "yarn build && yarn test", + "build+test+package": "yarn build+test && yarn package", + "compat": "cdk-compat", + "gen": "cfn2ts", + "rosetta:extract": "yarn --silent jsii-rosetta extract", + "build+extract": "yarn build && yarn rosetta:extract", + "build+test+extract": "yarn build+test && yarn rosetta:extract" + }, + "cdk-build": { + "cloudformation": "AWS::KafkaConnect", + "jest": true, + "env": { + "AWSLINT_BASE_CONSTRUCT": "true" + } + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "AWS::KafkaConnect", + "aws-kafkaconnect" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assertions": "0.0.0", + "@aws-cdk/cdk-build-tools": "0.0.0", + "@aws-cdk/cfn2ts": "0.0.0", + "@aws-cdk/pkglint": "0.0.0", + "@types/jest": "^27.4.1" + }, + "dependencies": { + "@aws-cdk/core": "0.0.0" + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "cfn-only", + "awscdkio": { + "announce": false + }, + "publishConfig": { + "tag": "latest" + } +} diff --git a/packages/@aws-cdk/aws-kafkaconnect/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-kafkaconnect/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..e208762bca03c --- /dev/null +++ b/packages/@aws-cdk/aws-kafkaconnect/rosetta/default.ts-fixture @@ -0,0 +1,8 @@ +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; + +class MyStack extends Stack { + constructor(scope: Construct, id: string) { + /// here + } +} diff --git a/packages/@aws-cdk/aws-kafkaconnect/test/kafkaconnect.test.ts b/packages/@aws-cdk/aws-kafkaconnect/test/kafkaconnect.test.ts new file mode 100644 index 0000000000000..465c7bdea0693 --- /dev/null +++ b/packages/@aws-cdk/aws-kafkaconnect/test/kafkaconnect.test.ts @@ -0,0 +1,6 @@ +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-kendra/package.json b/packages/@aws-cdk/aws-kendra/package.json index 3a5eb91dada5f..805b53e719899 100644 --- a/packages/@aws-cdk/aws-kendra/package.json +++ b/packages/@aws-cdk/aws-kendra/package.json @@ -7,6 +7,13 @@ "jsii": { "outdir": "dist", "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + }, "targets": { "dotnet": { "namespace": "Amazon.CDK.AWS.Kendra", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-kinesis/lib/stream.ts b/packages/@aws-cdk/aws-kinesis/lib/stream.ts index 364cb4436f59c..fb1c345a9a844 100644 --- a/packages/@aws-cdk/aws-kinesis/lib/stream.ts +++ b/packages/@aws-cdk/aws-kinesis/lib/stream.ts @@ -673,6 +673,9 @@ export interface StreamProps { /** * The number of shards for the stream. + * + * Can only be provided if streamMode is Provisioned. + * * @default 1 */ readonly shardCount?: number; @@ -752,9 +755,15 @@ export class Stream extends StreamBase { physicalName: props.streamName, }); - const shardCount = props.shardCount || 1; + let shardCount = props.shardCount; + const streamMode = props.streamMode ?? StreamMode.PROVISIONED; - const streamMode = props.streamMode; + if (streamMode === StreamMode.ON_DEMAND && shardCount !== undefined) { + throw new Error(`streamMode must be set to ${StreamMode.PROVISIONED} (default) when specifying shardCount`); + } + if (streamMode === StreamMode.PROVISIONED && shardCount === undefined) { + shardCount = 1; + } const retentionPeriodHours = props.retentionPeriod?.toHours() ?? 24; if (!Token.isUnresolved(retentionPeriodHours)) { diff --git a/packages/@aws-cdk/aws-kinesis/package.json b/packages/@aws-cdk/aws-kinesis/package.json index 5c41d6378214e..6cc0092590712 100644 --- a/packages/@aws-cdk/aws-kinesis/package.json +++ b/packages/@aws-cdk/aws-kinesis/package.json @@ -79,13 +79,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-cloudwatch": "0.0.0", diff --git a/packages/@aws-cdk/aws-kinesis/test/integ.stream-dashboard.expected.json b/packages/@aws-cdk/aws-kinesis/test/integ.stream-dashboard.expected.json index 28a166e76fd1f..19b702e60830d 100644 --- a/packages/@aws-cdk/aws-kinesis/test/integ.stream-dashboard.expected.json +++ b/packages/@aws-cdk/aws-kinesis/test/integ.stream-dashboard.expected.json @@ -4,6 +4,9 @@ "Type": "AWS::Kinesis::Stream", "Properties": { "ShardCount": 1, + "StreamModeDetails": { + "StreamMode": "PROVISIONED" + }, "RetentionPeriodHours": 24, "StreamEncryption": { "Fn::If": [ @@ -203,4 +206,4 @@ ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-kinesis/test/integ.stream.expected.json b/packages/@aws-cdk/aws-kinesis/test/integ.stream.expected.json index 41230acc599a2..e4e0a7b73bd68 100644 --- a/packages/@aws-cdk/aws-kinesis/test/integ.stream.expected.json +++ b/packages/@aws-cdk/aws-kinesis/test/integ.stream.expected.json @@ -72,6 +72,9 @@ "Type": "AWS::Kinesis::Stream", "Properties": { "ShardCount": 1, + "StreamModeDetails": { + "StreamMode": "PROVISIONED" + }, "RetentionPeriodHours": 24, "StreamEncryption": { "Fn::If": [ @@ -110,4 +113,4 @@ ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-kinesis/test/stream.test.ts b/packages/@aws-cdk/aws-kinesis/test/stream.test.ts index cad461b3250ca..e1fce397cce4c 100644 --- a/packages/@aws-cdk/aws-kinesis/test/stream.test.ts +++ b/packages/@aws-cdk/aws-kinesis/test/stream.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { arrayWith } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import { testFutureBehavior, testLegacyBehavior } from '@aws-cdk/cdk-build-tools'; @@ -16,12 +15,15 @@ describe('Kinesis data streams', () => { new Stream(stack, 'MyStream'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyStream5C050E93: { Type: 'AWS::Kinesis::Stream', Properties: { ShardCount: 1, + StreamModeDetails: { + StreamMode: StreamMode.PROVISIONED, + }, RetentionPeriodHours: 24, StreamEncryption: { 'Fn::If': [ @@ -69,12 +71,15 @@ describe('Kinesis data streams', () => { new Stream(stack, 'MyStream'); new Stream(stack, 'MyOtherStream'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyStream5C050E93: { Type: 'AWS::Kinesis::Stream', Properties: { ShardCount: 1, + StreamModeDetails: { + StreamMode: StreamMode.PROVISIONED, + }, RetentionPeriodHours: 24, StreamEncryption: { 'Fn::If': [ @@ -94,6 +99,9 @@ describe('Kinesis data streams', () => { Type: 'AWS::Kinesis::Stream', Properties: { ShardCount: 1, + StreamModeDetails: { + StreamMode: StreamMode.PROVISIONED, + }, RetentionPeriodHours: 24, StreamEncryption: { 'Fn::If': [ @@ -152,12 +160,15 @@ describe('Kinesis data streams', () => { shardCount: 2, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyStream5C050E93: { Type: 'AWS::Kinesis::Stream', Properties: { ShardCount: 2, + StreamModeDetails: { + StreamMode: StreamMode.PROVISIONED, + }, RetentionPeriodHours: 24, StreamEncryption: { 'Fn::If': [ @@ -206,12 +217,15 @@ describe('Kinesis data streams', () => { retentionPeriod: Duration.hours(168), }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyStream5C050E93: { Type: 'AWS::Kinesis::Stream', Properties: { ShardCount: 1, + StreamModeDetails: { + StreamMode: StreamMode.PROVISIONED, + }, RetentionPeriodHours: 168, StreamEncryption: { 'Fn::If': [ @@ -277,12 +291,15 @@ describe('Kinesis data streams', () => { }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyStream5C050E93: { Type: 'AWS::Kinesis::Stream', Properties: { ShardCount: 1, + StreamModeDetails: { + StreamMode: StreamMode.PROVISIONED, + }, RetentionPeriodHours: 24, StreamEncryption: { EncryptionType: 'KMS', @@ -317,8 +334,11 @@ describe('Kinesis data streams', () => { }); // THEN - expect(stack).toHaveResource('AWS::Kinesis::Stream', { + Template.fromStack(stack).hasResourceProperties('AWS::Kinesis::Stream', { ShardCount: 1, + StreamModeDetails: { + StreamMode: StreamMode.PROVISIONED, + }, RetentionPeriodHours: 24, StreamEncryption: { EncryptionType: 'KMS', @@ -336,11 +356,11 @@ describe('Kinesis data streams', () => { encryption: StreamEncryption.KMS, }); - expect(stack).toHaveResource('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { Description: 'Created by Default/MyStream', }); - expect(stack).toHaveResource('AWS::Kinesis::Stream', { + Template.fromStack(stack).hasResourceProperties('AWS::Kinesis::Stream', { StreamEncryption: { EncryptionType: 'KMS', KeyId: stack.resolve(stream.encryptionKey?.keyArn), @@ -360,13 +380,16 @@ describe('Kinesis data streams', () => { encryptionKey: explicitKey, }); - expect(stack).toHaveResource('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { Description: 'Explicit Key', }); - expect(stack).toHaveResource('AWS::Kinesis::Stream', { - RetentionPeriodHours: 24, + Template.fromStack(stack).hasResourceProperties('AWS::Kinesis::Stream', { ShardCount: 1, + StreamModeDetails: { + StreamMode: StreamMode.PROVISIONED, + }, + RetentionPeriodHours: 24, StreamEncryption: { EncryptionType: 'KMS', KeyId: stack.resolve(explicitKey.keyArn), @@ -374,22 +397,78 @@ describe('Kinesis data streams', () => { }); }), - test.each([StreamMode.ON_DEMAND, StreamMode.PROVISIONED])('uses explicit capacity mode %s', (mode: StreamMode) => { + test('uses explicit provisioned streamMode', () => { const stack = new Stack(); new Stream(stack, 'MyStream', { - streamMode: mode, + streamMode: StreamMode.PROVISIONED, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyStream5C050E93: { Type: 'AWS::Kinesis::Stream', Properties: { + RetentionPeriodHours: 24, ShardCount: 1, + StreamModeDetails: { + StreamMode: StreamMode.PROVISIONED, + }, + StreamEncryption: { + 'Fn::If': [ + 'AwsCdkKinesisEncryptedStreamsUnsupportedRegions', + { + Ref: 'AWS::NoValue', + }, + { + EncryptionType: 'KMS', + KeyId: 'alias/aws/kinesis', + }, + ], + }, + }, + }, + }, + Conditions: { + AwsCdkKinesisEncryptedStreamsUnsupportedRegions: { + 'Fn::Or': [ + { + 'Fn::Equals': [ + { + Ref: 'AWS::Region', + }, + 'cn-north-1', + ], + }, + { + 'Fn::Equals': [ + { + Ref: 'AWS::Region', + }, + 'cn-northwest-1', + ], + }, + ], + }, + }, + }); + }); + + test('uses explicit on-demand streamMode', () => { + const stack = new Stack(); + + new Stream(stack, 'MyStream', { + streamMode: StreamMode.ON_DEMAND, + }); + + Template.fromStack(stack).templateMatches({ + Resources: { + MyStream5C050E93: { + Type: 'AWS::Kinesis::Stream', + Properties: { RetentionPeriodHours: 24, StreamModeDetails: { - StreamMode: StreamMode[mode], + StreamMode: StreamMode.ON_DEMAND, }, StreamEncryption: { 'Fn::If': [ @@ -431,6 +510,17 @@ describe('Kinesis data streams', () => { }); }); + test('throws when using shardCount with on-demand streamMode', () => { + const stack = new Stack(); + + expect(() => { + new Stream(stack, 'MyStream', { + shardCount: 2, + streamMode: StreamMode.ON_DEMAND, + }); + }).toThrow(`streamMode must be set to ${StreamMode.PROVISIONED} (default) when specifying shardCount`); + }); + test('grantRead creates and attaches a policy with read only access to the principal', () => { const stack = new Stack(); const stream = new Stream(stack, 'MyStream', { @@ -440,17 +530,17 @@ describe('Kinesis data streams', () => { const user = new iam.User(stack, 'MyUser'); stream.grantRead(user); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Action: 'kms:Decrypt', Effect: 'Allow', Resource: stack.resolve(stream.encryptionKey?.keyArn), - }), + }]), }, }); - expect(stack).toHaveResourceLike('AWS::Kinesis::Stream', { + Template.fromStack(stack).hasResourceProperties('AWS::Kinesis::Stream', { StreamEncryption: { KeyId: stack.resolve(stream.encryptionKey?.keyArn), }, @@ -468,7 +558,7 @@ describe('Kinesis data streams', () => { const user = new iam.User(stack, 'MyUser'); stream.grantRead(user); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyStreamKey76F3300E: { Type: 'AWS::KMS::Key', @@ -536,6 +626,9 @@ describe('Kinesis data streams', () => { Type: 'AWS::Kinesis::Stream', Properties: { ShardCount: 1, + StreamModeDetails: { + StreamMode: StreamMode.PROVISIONED, + }, RetentionPeriodHours: 24, StreamEncryption: { EncryptionType: 'KMS', @@ -599,17 +692,17 @@ describe('Kinesis data streams', () => { const user = new iam.User(stack, 'MyUser'); stream.grantWrite(user); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Action: ['kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*'], Effect: 'Allow', Resource: stack.resolve(stream.encryptionKey?.keyArn), - }), + }]), }, }); - expect(stack).toHaveResourceLike('AWS::Kinesis::Stream', { + Template.fromStack(stack).hasResourceProperties('AWS::Kinesis::Stream', { StreamEncryption: { KeyId: stack.resolve(stream.encryptionKey?.keyArn), }, @@ -627,7 +720,7 @@ describe('Kinesis data streams', () => { const user = new iam.User(stack, 'MyUser'); stream.grantWrite(user); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyStreamKey76F3300E: { Type: 'AWS::KMS::Key', @@ -695,6 +788,9 @@ describe('Kinesis data streams', () => { Type: 'AWS::Kinesis::Stream', Properties: { ShardCount: 1, + StreamModeDetails: { + StreamMode: StreamMode.PROVISIONED, + }, RetentionPeriodHours: 24, StreamEncryption: { EncryptionType: 'KMS', @@ -750,17 +846,17 @@ describe('Kinesis data streams', () => { const user = new iam.User(stack, 'MyUser'); stream.grantReadWrite(user); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Action: ['kms:Decrypt', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*'], Effect: 'Allow', Resource: stack.resolve(stream.encryptionKey?.keyArn), - }), + }]), }, }); - expect(stack).toHaveResourceLike('AWS::Kinesis::Stream', { + Template.fromStack(stack).hasResourceProperties('AWS::Kinesis::Stream', { StreamEncryption: { KeyId: stack.resolve(stream.encryptionKey?.keyArn), }, @@ -778,7 +874,7 @@ describe('Kinesis data streams', () => { const user = new iam.User(stack, 'MyUser'); stream.grantReadWrite(user); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyStreamKey76F3300E: { Type: 'AWS::KMS::Key', @@ -845,8 +941,11 @@ describe('Kinesis data streams', () => { MyStream5C050E93: { Type: 'AWS::Kinesis::Stream', Properties: { - RetentionPeriodHours: 24, ShardCount: 1, + StreamModeDetails: { + StreamMode: StreamMode.PROVISIONED, + }, + RetentionPeriodHours: 24, StreamEncryption: { EncryptionType: 'KMS', KeyId: { @@ -909,12 +1008,15 @@ describe('Kinesis data streams', () => { const user = new iam.User(stack, 'MyUser'); stream.grantRead(user); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyStream5C050E93: { Type: 'AWS::Kinesis::Stream', Properties: { ShardCount: 1, + StreamModeDetails: { + StreamMode: StreamMode.PROVISIONED, + }, RetentionPeriodHours: 24, StreamEncryption: { 'Fn::If': [ @@ -997,12 +1099,15 @@ describe('Kinesis data streams', () => { const user = new iam.User(stack, 'MyUser'); stream.grantWrite(user); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyStream5C050E93: { Type: 'AWS::Kinesis::Stream', Properties: { ShardCount: 1, + StreamModeDetails: { + StreamMode: StreamMode.PROVISIONED, + }, RetentionPeriodHours: 24, StreamEncryption: { 'Fn::If': [ @@ -1077,12 +1182,15 @@ describe('Kinesis data streams', () => { const user = new iam.User(stack, 'MyUser'); stream.grantReadWrite(user); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyStream5C050E93: { Type: 'AWS::Kinesis::Stream', Properties: { ShardCount: 1, + StreamModeDetails: { + StreamMode: StreamMode.PROVISIONED, + }, RetentionPeriodHours: 24, StreamEncryption: { 'Fn::If': [ @@ -1167,12 +1275,15 @@ describe('Kinesis data streams', () => { const user = new iam.User(stack, 'MyUser'); stream.grant(user, 'kinesis:DescribeStream'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyStream5C050E93: { Type: 'AWS::Kinesis::Stream', Properties: { ShardCount: 1, + StreamModeDetails: { + StreamMode: StreamMode.PROVISIONED, + }, RetentionPeriodHours: 24, StreamEncryption: { 'Fn::If': [ @@ -1249,12 +1360,15 @@ describe('Kinesis data streams', () => { const user = new iam.User(stackB, 'UserWhoNeedsAccess'); streamFromStackA.grantRead(user); - expect(stackA).toMatchTemplate({ + Template.fromStack(stackA).templateMatches({ Resources: { MyStream5C050E93: { Type: 'AWS::Kinesis::Stream', Properties: { ShardCount: 1, + StreamModeDetails: { + StreamMode: StreamMode.PROVISIONED, + }, RetentionPeriodHours: 24, StreamEncryption: { 'Fn::If': [ @@ -1331,15 +1445,15 @@ describe('Kinesis data streams', () => { const user = new iam.User(stackB, 'UserWhoNeedsAccess'); streamFromStackA.grantRead(user); - expect(stackB).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stackB).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Action: 'kms:Decrypt', Effect: 'Allow', Resource: { 'Fn::ImportValue': 'stackA:ExportsOutputFnGetAttMyStreamKey76F3300EArn190947B4', }, - }), + }]), }, }); }); @@ -1358,7 +1472,7 @@ describe('Kinesis data streams', () => { retentionPeriod: Duration.hours(parameter.valueAsNumber), }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Parameters: { myretentionperiod: { Type: 'Number', @@ -1372,6 +1486,9 @@ describe('Kinesis data streams', () => { Type: 'AWS::Kinesis::Stream', Properties: { ShardCount: 1, + StreamModeDetails: { + StreamMode: StreamMode.PROVISIONED, + }, RetentionPeriodHours: { Ref: 'myretentionperiod', }, diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/package.json b/packages/@aws-cdk/aws-kinesisanalytics-flink/package.json index 1823d08a81e8d..fb639112e35c2 100644 --- a/packages/@aws-cdk/aws-kinesisanalytics-flink/package.json +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/package.json @@ -77,8 +77,8 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/assets": "0.0.0", diff --git a/packages/@aws-cdk/aws-kinesisanalytics/package.json b/packages/@aws-cdk/aws-kinesisanalytics/package.json index 0c0804c9727a6..bbdb446edb571 100644 --- a/packages/@aws-cdk/aws-kinesisanalytics/package.json +++ b/packages/@aws-cdk/aws-kinesisanalytics/package.json @@ -86,7 +86,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-kinesisanalyticsv2/.eslintrc.js b/packages/@aws-cdk/aws-kinesisanalyticsv2/.eslintrc.js new file mode 100644 index 0000000000000..2658ee8727166 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalyticsv2/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-kinesisanalyticsv2/.gitignore b/packages/@aws-cdk/aws-kinesisanalyticsv2/.gitignore new file mode 100644 index 0000000000000..62ebc95d75ce6 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalyticsv2/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk +nyc.config.js +!.eslintrc.js +!jest.config.js +junit.xml diff --git a/packages/@aws-cdk/aws-kinesisanalyticsv2/.npmignore b/packages/@aws-cdk/aws-kinesisanalyticsv2/.npmignore new file mode 100644 index 0000000000000..f931fede67c44 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalyticsv2/.npmignore @@ -0,0 +1,29 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json + +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml +test/ +!*.lit.ts diff --git a/packages/@aws-cdk/aws-kinesisanalyticsv2/LICENSE b/packages/@aws-cdk/aws-kinesisanalyticsv2/LICENSE new file mode 100644 index 0000000000000..82ad00bb02d0b --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalyticsv2/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-kinesisanalyticsv2/NOTICE b/packages/@aws-cdk/aws-kinesisanalyticsv2/NOTICE new file mode 100644 index 0000000000000..1b7adbb891265 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalyticsv2/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-kinesisanalyticsv2/README.md b/packages/@aws-cdk/aws-kinesisanalyticsv2/README.md new file mode 100644 index 0000000000000..86915437e77c6 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalyticsv2/README.md @@ -0,0 +1,31 @@ +# AWS::KinesisAnalyticsV2 Construct Library + + +--- + +![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) + +> All classes with the `Cfn` prefix in this module ([CFN Resources]) are always stable and safe to use. +> +> [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib + +--- + + + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +```ts nofixture +import * as kinesisanalytics from '@aws-cdk/aws-kinesisanalyticsv2'; +``` + + + +There are no hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet. +However, you can still use the automatically generated [L1](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_l1_using) constructs, and use this service exactly as you would using CloudFormation directly. + +For more information on the resources and properties available for this service, see the [CloudFormation documentation for AWS::KinesisAnalyticsV2](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_KinesisAnalyticsV2.html). + +(Read the [CDK Contributing Guide](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md) if you are interested in contributing to this construct library.) + + diff --git a/packages/@aws-cdk/aws-kinesisanalyticsv2/jest.config.js b/packages/@aws-cdk/aws-kinesisanalyticsv2/jest.config.js new file mode 100644 index 0000000000000..3a2fd93a1228a --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalyticsv2/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-kinesisanalyticsv2/lib/index.ts b/packages/@aws-cdk/aws-kinesisanalyticsv2/lib/index.ts new file mode 100644 index 0000000000000..d164240b822cf --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalyticsv2/lib/index.ts @@ -0,0 +1,2 @@ +// AWS::KinesisAnalyticsV2 CloudFormation Resources: +export * from './kinesisanalyticsv2.generated'; diff --git a/packages/@aws-cdk/aws-kinesisanalyticsv2/package.json b/packages/@aws-cdk/aws-kinesisanalyticsv2/package.json new file mode 100644 index 0000000000000..5e618783474d5 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalyticsv2/package.json @@ -0,0 +1,110 @@ +{ + "name": "@aws-cdk/aws-kinesisanalyticsv2", + "version": "0.0.0", + "description": "AWS::KinesisAnalyticsV2 Construct Library", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "projectReferences": true, + "targets": { + "dotnet": { + "namespace": "Amazon.CDK.AWS.KinesisAnalyticsV2", + "packageId": "Amazon.CDK.AWS.KinesisAnalyticsV2", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "java": { + "package": "software.amazon.awscdk.services.kinesisanalyticsv2", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "kinesisanalyticsv2" + } + }, + "python": { + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 1" + ], + "distName": "aws-cdk.aws-kinesisanalyticsv2", + "module": "aws_cdk.aws_kinesisanalyticsv2" + } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-kinesisanalyticsv2" + }, + "homepage": "https://github.com/aws/aws-cdk", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "cfn2ts": "cfn2ts", + "build+test": "yarn build && yarn test", + "build+test+package": "yarn build+test && yarn package", + "compat": "cdk-compat", + "gen": "cfn2ts", + "rosetta:extract": "yarn --silent jsii-rosetta extract", + "build+extract": "yarn build && yarn rosetta:extract", + "build+test+extract": "yarn build+test && yarn rosetta:extract" + }, + "cdk-build": { + "cloudformation": "AWS::KinesisAnalyticsV2", + "jest": true, + "env": { + "AWSLINT_BASE_CONSTRUCT": "true" + } + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "AWS::KinesisAnalyticsV2", + "aws-kinesisanalyticsv2" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assertions": "0.0.0", + "@aws-cdk/cdk-build-tools": "0.0.0", + "@aws-cdk/cfn2ts": "0.0.0", + "@aws-cdk/pkglint": "0.0.0", + "@types/jest": "^27.4.1" + }, + "dependencies": { + "@aws-cdk/core": "0.0.0" + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "cfn-only", + "awscdkio": { + "announce": false + }, + "publishConfig": { + "tag": "latest" + } +} diff --git a/packages/@aws-cdk/aws-kinesisanalyticsv2/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-kinesisanalyticsv2/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..e208762bca03c --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalyticsv2/rosetta/default.ts-fixture @@ -0,0 +1,8 @@ +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; + +class MyStack extends Stack { + constructor(scope: Construct, id: string) { + /// here + } +} diff --git a/packages/@aws-cdk/aws-kinesisanalyticsv2/test/kinesisanalyticsv2.test.ts b/packages/@aws-cdk/aws-kinesisanalyticsv2/test/kinesisanalyticsv2.test.ts new file mode 100644 index 0000000000000..465c7bdea0693 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalyticsv2/test/kinesisanalyticsv2.test.ts @@ -0,0 +1,6 @@ +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-kinesisfirehose-destinations/package.json b/packages/@aws-cdk/aws-kinesisfirehose-destinations/package.json index aaea214e1f3bc..ad74666cd287a 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose-destinations/package.json +++ b/packages/@aws-cdk/aws-kinesisfirehose-destinations/package.json @@ -78,8 +78,8 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-iam": "0.0.0", 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 5ae3347a50989..913dba1638ec3 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 @@ -481,6 +481,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -527,6 +531,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-kinesisfirehose/package.json b/packages/@aws-cdk/aws-kinesisfirehose/package.json index 468daf9d47aa0..066bfe78a218c 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose/package.json +++ b/packages/@aws-cdk/aws-kinesisfirehose/package.json @@ -85,7 +85,7 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-cloudwatch": "0.0.0", diff --git a/packages/@aws-cdk/aws-kinesisfirehose/test/integ.delivery-stream.expected.json b/packages/@aws-cdk/aws-kinesisfirehose/test/integ.delivery-stream.expected.json index 1c23bbcee1092..65ac018add362 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose/test/integ.delivery-stream.expected.json +++ b/packages/@aws-cdk/aws-kinesisfirehose/test/integ.delivery-stream.expected.json @@ -34,6 +34,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-kinesisfirehose/test/integ.delivery-stream.source-stream.expected.json b/packages/@aws-cdk/aws-kinesisfirehose/test/integ.delivery-stream.source-stream.expected.json index 896d0487a091c..ccbca77c32829 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose/test/integ.delivery-stream.source-stream.expected.json +++ b/packages/@aws-cdk/aws-kinesisfirehose/test/integ.delivery-stream.source-stream.expected.json @@ -34,6 +34,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -74,8 +78,8 @@ "SourceStream95FF52BE": { "Type": "AWS::Kinesis::Stream", "Properties": { - "ShardCount": 1, "RetentionPeriodHours": 24, + "ShardCount": 1, "StreamEncryption": { "Fn::If": [ "AwsCdkKinesisEncryptedStreamsUnsupportedRegions", @@ -87,6 +91,9 @@ "KeyId": "alias/aws/kinesis" } ] + }, + "StreamModeDetails": { + "StreamMode": "PROVISIONED" } } }, diff --git a/packages/@aws-cdk/aws-kinesisvideo/.eslintrc.js b/packages/@aws-cdk/aws-kinesisvideo/.eslintrc.js new file mode 100644 index 0000000000000..2658ee8727166 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisvideo/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-kinesisvideo/.gitignore b/packages/@aws-cdk/aws-kinesisvideo/.gitignore new file mode 100644 index 0000000000000..62ebc95d75ce6 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisvideo/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk +nyc.config.js +!.eslintrc.js +!jest.config.js +junit.xml diff --git a/packages/@aws-cdk/aws-kinesisvideo/.npmignore b/packages/@aws-cdk/aws-kinesisvideo/.npmignore new file mode 100644 index 0000000000000..f931fede67c44 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisvideo/.npmignore @@ -0,0 +1,29 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json + +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml +test/ +!*.lit.ts diff --git a/packages/@aws-cdk/aws-kinesisvideo/LICENSE b/packages/@aws-cdk/aws-kinesisvideo/LICENSE new file mode 100644 index 0000000000000..82ad00bb02d0b --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisvideo/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-kinesisvideo/NOTICE b/packages/@aws-cdk/aws-kinesisvideo/NOTICE new file mode 100644 index 0000000000000..1b7adbb891265 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisvideo/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-kinesisvideo/README.md b/packages/@aws-cdk/aws-kinesisvideo/README.md new file mode 100644 index 0000000000000..2d0d2df9733fb --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisvideo/README.md @@ -0,0 +1,31 @@ +# AWS::KinesisVideo Construct Library + + +--- + +![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) + +> All classes with the `Cfn` prefix in this module ([CFN Resources]) are always stable and safe to use. +> +> [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib + +--- + + + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +```ts nofixture +import * as kinesisvideo from '@aws-cdk/aws-kinesisvideo'; +``` + + + +There are no hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet. +However, you can still use the automatically generated [L1](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_l1_using) constructs, and use this service exactly as you would using CloudFormation directly. + +For more information on the resources and properties available for this service, see the [CloudFormation documentation for AWS::KinesisVideo](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_KinesisVideo.html). + +(Read the [CDK Contributing Guide](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md) if you are interested in contributing to this construct library.) + + diff --git a/packages/@aws-cdk/aws-kinesisvideo/jest.config.js b/packages/@aws-cdk/aws-kinesisvideo/jest.config.js new file mode 100644 index 0000000000000..3a2fd93a1228a --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisvideo/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-kinesisvideo/lib/index.ts b/packages/@aws-cdk/aws-kinesisvideo/lib/index.ts new file mode 100644 index 0000000000000..d5742336aa2a3 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisvideo/lib/index.ts @@ -0,0 +1,2 @@ +// AWS::KinesisVideo CloudFormation Resources: +export * from './kinesisvideo.generated'; diff --git a/packages/@aws-cdk/aws-kinesisvideo/package.json b/packages/@aws-cdk/aws-kinesisvideo/package.json new file mode 100644 index 0000000000000..40faddbc9de2f --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisvideo/package.json @@ -0,0 +1,110 @@ +{ + "name": "@aws-cdk/aws-kinesisvideo", + "version": "0.0.0", + "description": "AWS::KinesisVideo Construct Library", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "projectReferences": true, + "targets": { + "dotnet": { + "namespace": "Amazon.CDK.AWS.KinesisVideo", + "packageId": "Amazon.CDK.AWS.KinesisVideo", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "java": { + "package": "software.amazon.awscdk.services.kinesisvideo", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "kinesisvideo" + } + }, + "python": { + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 1" + ], + "distName": "aws-cdk.aws-kinesisvideo", + "module": "aws_cdk.aws_kinesisvideo" + } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-kinesisvideo" + }, + "homepage": "https://github.com/aws/aws-cdk", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "cfn2ts": "cfn2ts", + "build+test": "yarn build && yarn test", + "build+test+package": "yarn build+test && yarn package", + "compat": "cdk-compat", + "gen": "cfn2ts", + "rosetta:extract": "yarn --silent jsii-rosetta extract", + "build+extract": "yarn build && yarn rosetta:extract", + "build+test+extract": "yarn build+test && yarn rosetta:extract" + }, + "cdk-build": { + "cloudformation": "AWS::KinesisVideo", + "jest": true, + "env": { + "AWSLINT_BASE_CONSTRUCT": "true" + } + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "AWS::KinesisVideo", + "aws-kinesisvideo" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assertions": "0.0.0", + "@aws-cdk/cdk-build-tools": "0.0.0", + "@aws-cdk/cfn2ts": "0.0.0", + "@aws-cdk/pkglint": "0.0.0", + "@types/jest": "^27.4.1" + }, + "dependencies": { + "@aws-cdk/core": "0.0.0" + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "cfn-only", + "awscdkio": { + "announce": false + }, + "publishConfig": { + "tag": "latest" + } +} diff --git a/packages/@aws-cdk/aws-kinesisvideo/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-kinesisvideo/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..e208762bca03c --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisvideo/rosetta/default.ts-fixture @@ -0,0 +1,8 @@ +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; + +class MyStack extends Stack { + constructor(scope: Construct, id: string) { + /// here + } +} diff --git a/packages/@aws-cdk/aws-kinesisvideo/test/kinesisvideo.test.ts b/packages/@aws-cdk/aws-kinesisvideo/test/kinesisvideo.test.ts new file mode 100644 index 0000000000000..465c7bdea0693 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisvideo/test/kinesisvideo.test.ts @@ -0,0 +1,6 @@ +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-kms/package.json b/packages/@aws-cdk/aws-kms/package.json index 1d2162e38683c..333bedc7b8a93 100644 --- a/packages/@aws-cdk/aws-kms/package.json +++ b/packages/@aws-cdk/aws-kms/package.json @@ -79,13 +79,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-iam": "0.0.0", diff --git a/packages/@aws-cdk/aws-kms/test/alias.test.ts b/packages/@aws-cdk/aws-kms/test/alias.test.ts index 094d2a3e7dbed..01e0b6f353b62 100644 --- a/packages/@aws-cdk/aws-kms/test/alias.test.ts +++ b/packages/@aws-cdk/aws-kms/test/alias.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { ArnPrincipal, PolicyStatement } from '@aws-cdk/aws-iam'; import { App, CfnOutput, Stack } from '@aws-cdk/core'; import { Alias } from '../lib/alias'; @@ -15,7 +15,7 @@ test('default alias', () => { new Alias(stack, 'Alias', { targetKey: key, aliasName: 'alias/foo' }); - expect(stack).toHaveResource('AWS::KMS::Alias', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Alias', { AliasName: 'alias/foo', TargetKeyId: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, }); @@ -35,7 +35,7 @@ test('add "alias/" prefix if not given.', () => { targetKey: key, }); - expect(stack).toHaveResource('AWS::KMS::Alias', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Alias', { AliasName: 'alias/foo', TargetKeyId: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, }); @@ -51,7 +51,7 @@ test('can create alias directly while creating the key', () => { alias: 'foo', }); - expect(stack).toHaveResource('AWS::KMS::Alias', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Alias', { AliasName: 'alias/foo', TargetKeyId: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, }); @@ -140,13 +140,11 @@ test('can be used wherever a key is expected', () => { new MyConstruct(stack, 'MyConstruct', myAlias); /* eslint-enable @aws-cdk/no-core-construct */ - expect(stack).toHaveOutput({ - outputName: 'OutId', - outputValue: 'alias/myAlias', + Template.fromStack(stack).hasOutput('OutId', { + Value: 'alias/myAlias', }); - expect(stack).toHaveOutput({ - outputName: 'OutArn', - outputValue: { + Template.fromStack(stack).hasOutput('OutArn', { + Value: { 'Fn::Join': ['', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -181,13 +179,11 @@ test('imported alias by name - can be used where a key is expected', () => { new MyConstruct(stack, 'MyConstruct', myAlias); /* eslint-enable @aws-cdk/no-core-construct */ - expect(stack).toHaveOutput({ - outputName: 'OutId', - outputValue: 'alias/myAlias', + Template.fromStack(stack).hasOutput('OutId', { + Value: 'alias/myAlias', }); - expect(stack).toHaveOutput({ - outputName: 'OutArn', - outputValue: { + Template.fromStack(stack).hasOutput('OutArn', { + Value: { 'Fn::Join': ['', [ 'arn:', { Ref: 'AWS::Partition' }, diff --git a/packages/@aws-cdk/aws-kms/test/key.from-lookup.test.ts b/packages/@aws-cdk/aws-kms/test/key.from-lookup.test.ts index f7af3c8c0dd14..72598e01708f1 100644 --- a/packages/@aws-cdk/aws-kms/test/key.from-lookup.test.ts +++ b/packages/@aws-cdk/aws-kms/test/key.from-lookup.test.ts @@ -3,7 +3,6 @@ import { ContextProvider, GetContextValueOptions, GetContextValueResult, Lazy, S import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { Key } from '../lib'; -import '@aws-cdk/assert-internal/jest'; test('requires concrete values', () => { expect(() => { diff --git a/packages/@aws-cdk/aws-kms/test/key.test.ts b/packages/@aws-cdk/aws-kms/test/key.test.ts index 5a42d030bf36a..a73d0b0cabf97 100644 --- a/packages/@aws-cdk/aws-kms/test/key.test.ts +++ b/packages/@aws-cdk/aws-kms/test/key.test.ts @@ -1,5 +1,4 @@ -import { arrayWith, countResources, expect as expectCdk, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import { describeDeprecated, testFutureBehavior, testLegacyBehavior } from '@aws-cdk/cdk-build-tools'; import * as cdk from '@aws-cdk/core'; @@ -47,7 +46,7 @@ testFutureBehavior('default key', flags, cdk.App, (app) => { const stack = new cdk.Stack(app); new kms.Key(stack, 'MyKey'); - expect(stack).toHaveResource('AWS::KMS::Key', { + Template.fromStack(stack).hasResource('AWS::KMS::Key', { Properties: { KeyPolicy: { Statement: [ @@ -65,14 +64,14 @@ testFutureBehavior('default key', flags, cdk.App, (app) => { }, DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition); + }); }); testFutureBehavior('default with no retention', flags, cdk.App, (app) => { const stack = new cdk.Stack(app); new kms.Key(stack, 'MyKey', { removalPolicy: cdk.RemovalPolicy.DESTROY }); - expect(stack).toHaveResource('AWS::KMS::Key', { DeletionPolicy: 'Delete', UpdateReplacePolicy: 'Delete' }, ResourcePart.CompleteDefinition); + Template.fromStack(stack).hasResource('AWS::KMS::Key', { DeletionPolicy: 'Delete', UpdateReplacePolicy: 'Delete' }); }); describe('key policies', () => { @@ -85,7 +84,7 @@ describe('key policies', () => { new kms.Key(stack, 'MyKey', { policy }); - expect(stack).toHaveResource('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -110,7 +109,7 @@ describe('key policies', () => { const key = new kms.Key(stack, 'MyKey'); key.addToResourcePolicy(statement); - expect(stack).toHaveResource('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -146,7 +145,7 @@ describe('key policies', () => { // THEN // Key policy should be unmodified by the grant. - expect(stack).toHaveResource('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -160,7 +159,7 @@ describe('key policies', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -185,7 +184,7 @@ describe('key policies', () => { // THEN // Key policy should be unmodified by the grant. - expect(stack).toHaveResource('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -199,7 +198,7 @@ describe('key policies', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -226,7 +225,7 @@ describe('key policies', () => { key.grantEncrypt(principal); - expect(principalStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(principalStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -258,9 +257,9 @@ describe('key policies', () => { key.grantEncrypt(principal); - expect(keyStack).toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(keyStack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { - Statement: arrayWith( + Statement: Match.arrayWith([ { Action: [ 'kms:Encrypt', @@ -271,11 +270,11 @@ describe('key policies', () => { Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::', { Ref: 'AWS::AccountId' }, ':role/MyRolePhysicalName']] } }, Resource: '*', }, - ), + ]), Version: '2012-10-17', }, }); - expect(principalStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(principalStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -305,27 +304,22 @@ describe('key policies', () => { key.grantEncrypt(principal); - expect(keyStack).toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(keyStack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { - Statement: [ - { - // Default policy, unmodified - }, - { - Action: [ - 'kms:Encrypt', - 'kms:ReEncrypt*', - 'kms:GenerateDataKey*', - ], - Effect: 'Allow', - Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::0123456789012:role/MyRolePhysicalName']] } }, - Resource: '*', - }, - ], + Statement: Match.arrayWith([{ + Action: [ + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', + ], + Effect: 'Allow', + Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::0123456789012:role/MyRolePhysicalName']] } }, + Resource: '*', + }]), Version: '2012-10-17', }, }); - expect(principalStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(principalStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -348,7 +342,7 @@ describe('key policies', () => { const adminRole = iam.Role.fromRoleArn(stack, 'Admin', 'arn:aws:iam::123456789012:role/TrustedAdmin'); new kms.Key(stack, 'MyKey', { admins: [adminRole] }); - expect(stack).toHaveResource('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -380,7 +374,7 @@ describe('key policies', () => { }); new kms.Key(stack, 'MyKey', { admins: [adminRole] }); - expect(stack).toHaveResource('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { // Unmodified - default key policy Statement: [ @@ -396,7 +390,7 @@ describe('key policies', () => { Version: '2012-10-17', }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -423,7 +417,7 @@ testFutureBehavior('key with some options', flags, cdk.App, (app) => { cdk.Tags.of(key).add('tag2', 'value2'); cdk.Tags.of(key).add('tag3', ''); - expect(stack).toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { Enabled: false, EnableKeyRotation: true, PendingWindowInDays: 7, @@ -460,7 +454,7 @@ describeDeprecated('trustAccountIdentities is deprecated', () => { testLegacyBehavior('trustAccountIdentities changes key policy to allow IAM control', cdk.App, (app) => { const stack = new cdk.Stack(app); new kms.Key(stack, 'MyKey', { trustAccountIdentities: true }); - expect(stack).toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -487,8 +481,8 @@ testFutureBehavior('addAlias creates an alias', flags, cdk.App, (app) => { const alias = key.addAlias('alias/xoo'); expect(alias.aliasName).toBeDefined(); - expect(stack).toCountResources('AWS::KMS::Alias', 1); - expect(stack).toHaveResource('AWS::KMS::Alias', { + Template.fromStack(stack).resourceCountIs('AWS::KMS::Alias', 1); + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Alias', { AliasName: 'alias/xoo', TargetKeyId: { 'Fn::GetAtt': [ @@ -511,8 +505,8 @@ testFutureBehavior('can run multiple addAlias', flags, cdk.App, (app) => { expect(alias1.aliasName).toBeDefined(); expect(alias2.aliasName).toBeDefined(); - expect(stack).toCountResources('AWS::KMS::Alias', 2); - expect(stack).toHaveResource('AWS::KMS::Alias', { + Template.fromStack(stack).resourceCountIs('AWS::KMS::Alias', 2); + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Alias', { AliasName: 'alias/alias1', TargetKeyId: { 'Fn::GetAtt': [ @@ -521,7 +515,7 @@ testFutureBehavior('can run multiple addAlias', flags, cdk.App, (app) => { ], }, }); - expect(stack).toHaveResource('AWS::KMS::Alias', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Alias', { AliasName: 'alias/alias2', TargetKeyId: { 'Fn::GetAtt': [ @@ -540,9 +534,8 @@ testFutureBehavior('keyId resolves to a Ref', flags, cdk.App, (app) => { value: key.keyId, }); - expect(stack).toHaveOutput({ - outputName: 'Out', - outputValue: { Ref: 'MyKey6AB29FA6' }, + Template.fromStack(stack).hasOutput('Out', { + Value: { Ref: 'MyKey6AB29FA6' }, }); }); @@ -589,7 +582,7 @@ describe('imported keys', () => { expect(myKeyImported.keyId).toEqual('12345678-1234-1234-1234-123456789012'); - expect(stack2).toMatchTemplate({ + Template.fromStack(stack2).templateMatches({ Resources: { MyKeyImportedAliasB1C5269F: { Type: 'AWS::KMS::Alias', @@ -647,7 +640,7 @@ describe('fromCfnKey()', () => { }); test('preserves the KMS Key resource', () => { - expectCdk(stack).to(haveResource('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -669,9 +662,8 @@ describe('fromCfnKey()', () => { ], Version: '2012-10-17', }, - })); - - expectCdk(stack).to(countResources('AWS::KMS::Key', 1)); + }); + Template.fromStack(stack).resourceCountIs('AWS::KMS::Key', 1); }); describe("calling 'addToResourcePolicy()' on the returned Key", () => { @@ -690,7 +682,7 @@ describe('fromCfnKey()', () => { }); test('preserves the mutating call in the resulting template', () => { - expectCdk(stack).to(haveResource('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -718,7 +710,7 @@ describe('fromCfnKey()', () => { ], Version: '2012-10-17', }, - })); + }); }); }); @@ -736,7 +728,7 @@ describe('fromCfnKey()', () => { }); test('creates the correct IAM Policy', () => { - expectCdk(stack).to(haveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -748,11 +740,11 @@ describe('fromCfnKey()', () => { }, ], }, - })); + }); }); test('correctly mutates the Policy of the underlying CfnKey', () => { - expectCdk(stack).to(haveResourceLike('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -784,7 +776,7 @@ describe('fromCfnKey()', () => { ], Version: '2012-10-17', }, - })); + }); }); }); }); @@ -948,7 +940,7 @@ describe('when the defaultKeyPolicies feature flag is disabled', () => { const stack = new cdk.Stack(app); new kms.Key(stack, 'MyKey'); - expect(stack).toHaveResource('AWS::KMS::Key', { + Template.fromStack(stack).hasResource('AWS::KMS::Key', { Properties: { KeyPolicy: { Statement: [ @@ -966,7 +958,7 @@ describe('when the defaultKeyPolicies feature flag is disabled', () => { }, DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition); + }); }); testLegacyBehavior('policy if specified appends to the default key policy', cdk.App, (app) => { @@ -976,7 +968,7 @@ describe('when the defaultKeyPolicies feature flag is disabled', () => { p.addArnPrincipal('arn:aws:iam::111122223333:root'); key.addToResourcePolicy(p); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyKey6AB29FA6: { Type: 'AWS::KMS::Key', @@ -1015,7 +1007,7 @@ describe('when the defaultKeyPolicies feature flag is disabled', () => { const adminRole = iam.Role.fromRoleArn(stack, 'Admin', 'arn:aws:iam::123456789012:role/TrustedAdmin'); new kms.Key(stack, 'MyKey', { admins: [adminRole] }); - expect(stack).toHaveResource('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -1047,7 +1039,7 @@ describe('when the defaultKeyPolicies feature flag is disabled', () => { }); new kms.Key(stack, 'MyKey', { admins: [adminRole] }); - expect(stack).toHaveResource('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -1070,7 +1062,7 @@ describe('when the defaultKeyPolicies feature flag is disabled', () => { Version: '2012-10-17', }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -1095,7 +1087,7 @@ describe('when the defaultKeyPolicies feature flag is disabled', () => { key.grantDecrypt(user); // THEN - expect(stack).toHaveResource('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { Statement: [ // This one is there by default @@ -1117,7 +1109,7 @@ describe('when the defaultKeyPolicies feature flag is disabled', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -1144,9 +1136,9 @@ describe('when the defaultKeyPolicies feature flag is disabled', () => { key.grantEncrypt(principal); - expect(keyStack).toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(keyStack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Action: [ 'kms:Encrypt', 'kms:ReEncrypt*', @@ -1157,7 +1149,7 @@ describe('when the defaultKeyPolicies feature flag is disabled', () => { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::', { Ref: 'AWS::AccountId' }, ':root']] }, }, Resource: '*', - }), + }]), }, }); }); @@ -1169,7 +1161,7 @@ describe('key specs and key usages', () => { const stack = new cdk.Stack(app); new kms.Key(stack, 'Key', { keySpec: kms.KeySpec.ECC_SECG_P256K1, keyUsage: kms.KeyUsage.SIGN_VERIFY }); - expect(stack).toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeySpec: 'ECC_SECG_P256K1', KeyUsage: 'SIGN_VERIFY', }); @@ -1179,7 +1171,7 @@ describe('key specs and key usages', () => { const stack = new cdk.Stack(app); new kms.Key(stack, 'Key', { keyUsage: kms.KeyUsage.ENCRYPT_DECRYPT }); - expect(stack).toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyUsage: 'ENCRYPT_DECRYPT', }); }); @@ -1188,7 +1180,7 @@ describe('key specs and key usages', () => { const stack = new cdk.Stack(app); new kms.Key(stack, 'Key', { keySpec: kms.KeySpec.RSA_4096 }); - expect(stack).toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeySpec: 'RSA_4096', }); }); diff --git a/packages/@aws-cdk/aws-kms/test/via-service-principal.test.ts b/packages/@aws-cdk/aws-kms/test/via-service-principal.test.ts index 1e5eeb95f28ff..b00640f675378 100644 --- a/packages/@aws-cdk/aws-kms/test/via-service-principal.test.ts +++ b/packages/@aws-cdk/aws-kms/test/via-service-principal.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '../lib'; diff --git a/packages/@aws-cdk/aws-lakeformation/package.json b/packages/@aws-cdk/aws-lakeformation/package.json index e0dd166743305..c7ceef39cb56d 100644 --- a/packages/@aws-cdk/aws-lakeformation/package.json +++ b/packages/@aws-cdk/aws-lakeformation/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda-destinations/package.json b/packages/@aws-cdk/aws-lambda-destinations/package.json index 0dec134064d18..8feb76b2523f3 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/package.json +++ b/packages/@aws-cdk/aws-lambda-destinations/package.json @@ -71,13 +71,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-events": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda-destinations/test/destinations.test.ts b/packages/@aws-cdk/aws-lambda-destinations/test/destinations.test.ts index 3be1f3d94f37f..d30d292c7e510 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/test/destinations.test.ts +++ b/packages/@aws-cdk/aws-lambda-destinations/test/destinations.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as events from '@aws-cdk/aws-events'; import * as lambda from '@aws-cdk/aws-lambda'; import * as sns from '@aws-cdk/aws-sns'; @@ -28,7 +28,7 @@ test('event bus as destination', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::EventInvokeConfig', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventInvokeConfig', { DestinationConfig: { OnSuccess: { Destination: { @@ -41,7 +41,7 @@ test('event bus as destination', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -71,7 +71,7 @@ test('lambda as destination', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::EventInvokeConfig', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventInvokeConfig', { DestinationConfig: { OnSuccess: { Destination: { @@ -84,7 +84,7 @@ test('lambda as destination', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -116,7 +116,7 @@ test('lambda payload as destination', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::EventInvokeConfig', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventInvokeConfig', { DestinationConfig: { OnSuccess: { Destination: { @@ -165,7 +165,7 @@ test('lambda payload as destination', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -197,7 +197,7 @@ test('lambda payload as destination', () => { }, }); - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { EventPattern: { 'detail-type': [ 'Lambda Function Invocation Result - Success', @@ -236,7 +236,7 @@ test('lambda payload as destination', () => { ], }); - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { EventPattern: { 'detail-type': [ 'Lambda Function Invocation Result - Failure', @@ -287,7 +287,7 @@ test('sns as destination', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::EventInvokeConfig', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventInvokeConfig', { DestinationConfig: { OnSuccess: { Destination: { @@ -297,7 +297,7 @@ test('sns as destination', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -324,7 +324,7 @@ test('sqs as destination', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::EventInvokeConfig', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventInvokeConfig', { DestinationConfig: { OnSuccess: { Destination: { @@ -337,7 +337,7 @@ test('sqs as destination', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/kafka.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/kafka.ts index e31fac89100cb..a781b3a17e7e9 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/kafka.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/kafka.ts @@ -4,7 +4,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import { Stack, Names } from '@aws-cdk/core'; -import { StreamEventSource, StreamEventSourceProps } from './stream'; +import { StreamEventSource, BaseStreamEventSourceProps } from './stream'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main // eslint-disable-next-line no-duplicate-imports, import/order @@ -13,7 +13,7 @@ import { Construct } from '@aws-cdk/core'; /** * Properties for a Kafka event source */ -export interface KafkaEventSourceProps extends StreamEventSourceProps { +export interface KafkaEventSourceProps extends BaseStreamEventSourceProps{ /** * The Kafka topic to subscribe to */ @@ -53,6 +53,10 @@ export enum AuthenticationMethod { * BASIC_AUTH (SASL/PLAIN) authentication method for your Kafka cluster */ BASIC_AUTH = 'BASIC_AUTH', + /** + * CLIENT_CERTIFICATE_TLS_AUTH (mTLS) authentication method for your Kafka cluster + */ + CLIENT_CERTIFICATE_TLS_AUTH = 'CLIENT_CERTIFICATE_TLS_AUTH', } /** @@ -213,6 +217,9 @@ export class SelfManagedKafkaEventSource extends StreamEventSource { case AuthenticationMethod.BASIC_AUTH: authType = lambda.SourceAccessConfigurationType.BASIC_AUTH; break; + case AuthenticationMethod.CLIENT_CERTIFICATE_TLS_AUTH: + authType = lambda.SourceAccessConfigurationType.CLIENT_CERTIFICATE_TLS_AUTH; + break; case AuthenticationMethod.SASL_SCRAM_256_AUTH: authType = lambda.SourceAccessConfigurationType.SASL_SCRAM_256_AUTH; break; diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/stream.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/stream.ts index 01288efb21a6c..462387397b629 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/stream.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/stream.ts @@ -5,7 +5,7 @@ import { Duration } from '@aws-cdk/core'; * The set of properties for event sources that follow the streaming model, * such as, Dynamo, Kinesis and Kafka. */ -export interface StreamEventSourceProps { +export interface BaseStreamEventSourceProps{ /** * The largest number of records that AWS Lambda will retrieve from your event * source at the time of invoking your function. Your function receives an @@ -15,25 +15,51 @@ export interface StreamEventSourceProps { * * Minimum value of 1 * * Maximum value of: * * 1000 for {@link DynamoEventSource} - * * 10000 for {@link KinesisEventSource} + * * 10000 for {@link KinesisEventSource}, {@link ManagedKafkaEventSource} and {@link SelfManagedKafkaEventSource} * * @default 100 */ readonly batchSize?: number; /** - * If the function returns an error, split the batch in two and retry. + * An Amazon SQS queue or Amazon SNS topic destination for discarded records. * - * @default false + * @default discarded records are ignored */ - readonly bisectBatchOnError?: boolean; + readonly onFailure?: lambda.IEventSourceDlq; /** - * An Amazon SQS queue or Amazon SNS topic destination for discarded records. + * Where to begin consuming the stream. + */ + readonly startingPosition: lambda.StartingPosition; + + /** + * The maximum amount of time to gather records before invoking the function. + * Maximum of Duration.minutes(5) * - * @default discarded records are ignored + * @default Duration.seconds(0) */ - readonly onFailure?: lambda.IEventSourceDlq; + readonly maxBatchingWindow?: Duration; + + /** + * If the stream event source mapping should be enabled. + * + * @default true + */ + readonly enabled?: boolean; +} + +/** + * The set of properties for event sources that follow the streaming model, + * such as, Dynamo, Kinesis. + */ +export interface StreamEventSourceProps extends BaseStreamEventSourceProps { + /** + * If the function returns an error, split the batch in two and retry. + * + * @default false + */ + readonly bisectBatchOnError?: boolean; /** * The maximum age of a record that Lambda sends to a function for processing. @@ -65,11 +91,6 @@ export interface StreamEventSourceProps { */ readonly parallelizationFactor?: number; - /** - * Where to begin consuming the stream. - */ - readonly startingPosition: lambda.StartingPosition; - /** * Allow functions to return partially successful responses for a batch of records. * @@ -79,14 +100,6 @@ export interface StreamEventSourceProps { */ readonly reportBatchItemFailures?: boolean; - /** - * The maximum amount of time to gather records before invoking the function. - * Maximum of Duration.minutes(5) - * - * @default Duration.seconds(0) - */ - readonly maxBatchingWindow?: Duration; - /** * The size of the tumbling windows to group records sent to DynamoDB or Kinesis * Valid Range: 0 - 15 minutes @@ -94,13 +107,6 @@ export interface StreamEventSourceProps { * @default - None */ readonly tumblingWindow?: Duration; - - /** - * If the stream event source mapping should be enabled. - * - * @default true - */ - readonly enabled?: boolean; } /** diff --git a/packages/@aws-cdk/aws-lambda-event-sources/package.json b/packages/@aws-cdk/aws-lambda-event-sources/package.json index 42038a72f8566..c7253423991e4 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/package.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/package.json @@ -75,8 +75,8 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-apigateway": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesis.expected.json b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesis.expected.json index c1690f2f03aac..d14d727e34999 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesis.expected.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesis.expected.json @@ -116,6 +116,9 @@ "Type": "AWS::Kinesis::Stream", "Properties": { "ShardCount": 1, + "StreamModeDetails": { + "StreamMode": "PROVISIONED" + }, "RetentionPeriodHours": 24, "StreamEncryption": { "Fn::If": [ @@ -154,4 +157,4 @@ ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesiswithdlq.expected.json b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesiswithdlq.expected.json index 616adaef6a86a..a16660f565e76 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesiswithdlq.expected.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesiswithdlq.expected.json @@ -140,6 +140,9 @@ "Type": "AWS::Kinesis::Stream", "Properties": { "ShardCount": 1, + "StreamModeDetails": { + "StreamMode": "PROVISIONED" + }, "RetentionPeriodHours": 24, "StreamEncryption": { "Fn::If": [ @@ -203,4 +206,4 @@ ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/kafka.test.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/kafka.test.ts index 1455931278129..43fc533e93770 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/kafka.test.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/kafka.test.ts @@ -489,6 +489,41 @@ describe('KafkaEventSource', () => { }); }); + test('using CLIENT_CERTIFICATE_TLS_AUTH', () => { + // GIVEN + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const kafkaTopic = 'some-topic'; + const secret = new Secret(stack, 'Secret', { secretName: 'AmazonMSK_KafkaSecret' }); + const bootstrapServers = ['kafka-broker:9092']; + const sg = SecurityGroup.fromSecurityGroupId(stack, 'SecurityGroup', 'sg-0123456789'); + const vpc = new Vpc(stack, 'Vpc'); + + // WHEN + fn.addEventSource(new sources.SelfManagedKafkaEventSource( + { + bootstrapServers: bootstrapServers, + topic: kafkaTopic, + secret: secret, + startingPosition: lambda.StartingPosition.TRIM_HORIZON, + vpc: vpc, + vpcSubnets: { subnetType: SubnetType.PRIVATE_WITH_NAT }, + securityGroup: sg, + authenticationMethod: sources.AuthenticationMethod.CLIENT_CERTIFICATE_TLS_AUTH, + })); + + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { + SourceAccessConfigurations: Match.arrayWith([ + { + Type: 'CLIENT_CERTIFICATE_TLS_AUTH', + URI: { + Ref: 'SecretA720EF05', + }, + }, + ]), + }); + }); + test('ManagedKafkaEventSource name conforms to construct id rules', () => { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-lambda-go/package.json b/packages/@aws-cdk/aws-lambda-go/package.json index 6c82e15eed727..0383c7c0bbdd2 100644 --- a/packages/@aws-cdk/aws-lambda-go/package.json +++ b/packages/@aws-cdk/aws-lambda-go/package.json @@ -78,7 +78,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-lambda": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda-nodejs/README.md b/packages/@aws-cdk/aws-lambda-nodejs/README.md index 81fb45b3b1f4a..95d7559cf25f3 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/README.md +++ b/packages/@aws-cdk/aws-lambda-nodejs/README.md @@ -48,7 +48,7 @@ Alternatively, an entry file and handler can be specified: ```ts new lambda.NodejsFunction(this, 'MyFunction', { - entry: '/path/to/my/file.ts', // accepts .js, .jsx, .ts and .tsx files + entry: '/path/to/my/file.ts', // accepts .js, .jsx, .ts, .tsx and .mjs files handler: 'myExportedFunc', // defaults to 'handler' }); ``` @@ -116,6 +116,11 @@ OR $ yarn add --dev esbuild@0 ``` +If you're using a monorepo layout, the `esbuild` dependency needs to be installed in the "root" `package.json` file, +not in the workspace. From the reference architecture described [above](#reference-project-architecture), the `esbuild` +dev dependency needs to be in `./package.json`, not `packages/cool-package/package.json` or +`packages/super-package/package.json`. + To force bundling in a Docker container even if `esbuild` is available in your environment, set `bundling.forceDockerBundling` to `true`. This is useful if your function relies on node modules that should be installed (`nodeModules` prop, see [below](#install-modules)) in a Lambda @@ -191,6 +196,8 @@ new lambda.NodejsFunction(this, 'my-handler', { banner: '/* comments */', // requires esbuild >= 0.9.0, defaults to none footer: '/* comments */', // requires esbuild >= 0.9.0, defaults to none charset: lambda.Charset.UTF8, // do not escape non-ASCII characters, defaults to Charset.ASCII + format: lambda.OutputFormat.ESM, // ECMAScript module output format, defaults to OutputFormat.CJS (OutputFormat.ESM requires Node.js 14.x) + mainFields: ['module', 'main'], // prefer ECMAScript versions of dependencies }, }); ``` @@ -251,7 +258,7 @@ new lambda.NodejsFunction(this, 'my-handler', { }); ``` -Note: A [`tsconfig.json` file](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) is required +Note: A [`tsconfig.json` file](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) is required ## Customizing Docker bundling diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts index 7994859706379..8a251bf8dda24 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts @@ -4,8 +4,8 @@ import { Architecture, AssetCode, Code, Runtime } from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; import { PackageInstallation } from './package-installation'; import { PackageManager } from './package-manager'; -import { BundlingOptions, SourceMapMode } from './types'; -import { exec, extractDependencies, findUp } from './util'; +import { BundlingOptions, OutputFormat, SourceMapMode } from './types'; +import { exec, extractDependencies, findUp, getTsconfigCompilerOptions } from './util'; const ESBUILD_MAJOR_VERSION = '0'; @@ -68,13 +68,8 @@ export class Bundling implements cdk.BundlingOptions { this.tscInstallation = undefined; } - public static clearTscCompilationCache(): void { - this.tscCompiled = false; - } - private static esbuildInstallation?: PackageInstallation; private static tscInstallation?: PackageInstallation; - private static tscCompiled = false // Core bundling options public readonly image: cdk.DockerImage; @@ -112,6 +107,11 @@ export class Bundling implements cdk.BundlingOptions { throw new Error('preCompilation can only be used with typescript files'); } + if (props.format === OutputFormat.ESM + && (props.runtime === Runtime.NODEJS_10_X || props.runtime === Runtime.NODEJS_12_X)) { + throw new Error(`ECMAScript module output format is not supported by the ${props.runtime.name} runtime`); + } + this.externals = [ ...props.externalModules ?? ['aws-sdk'], // Mark aws-sdk as external by default (available in the runtime) ...props.nodeModules ?? [], // Mark the modules that we are going to install as externals also @@ -151,26 +151,17 @@ export class Bundling implements cdk.BundlingOptions { private createBundlingCommand(options: BundlingCommandOptions): string { const pathJoin = osPathJoin(options.osPlatform); - let tscCommand: string = ''; - let relativeEntryPath = this.relativeEntryPath; + let relativeEntryPath = pathJoin(options.inputDir, this.relativeEntryPath); + let tscCommand = ''; if (this.props.preCompilation) { - - let tsconfig = this.relativeTsconfigPath; + const tsconfig = this.props.tsconfig ?? findUp('tsconfig.json', path.dirname(this.props.entry)); if (!tsconfig) { - const findConfig = findUp('tsconfig.json', path.dirname(this.props.entry)); - if (!findConfig) { - throw new Error('Cannot find a tsconfig.json, please specify the prop: tsconfig'); - } - tsconfig = path.relative(this.projectRoot, findConfig); + throw new Error('Cannot find a `tsconfig.json` but `preCompilation` is set to `true`, please specify it via `tsconfig`'); } - + const compilerOptions = getTsconfigCompilerOptions(tsconfig); + tscCommand = `${options.tscRunner} "${relativeEntryPath}" ${compilerOptions}`; relativeEntryPath = relativeEntryPath.replace(/\.ts(x?)$/, '.js$1'); - if (!Bundling.tscCompiled) { - // Intentionally Setting rootDir and outDir, so that the compiled js file always end up next ts file. - tscCommand = `${options.tscRunner} --project ${pathJoin(options.inputDir, tsconfig)} --rootDir ./ --outDir ./`; - Bundling.tscCompiled = true; - } } const loaders = Object.entries(this.props.loader ?? {}); @@ -185,12 +176,14 @@ export class Bundling implements cdk.BundlingOptions { const sourceMapValue = sourceMapMode === SourceMapMode.DEFAULT ? '' : `=${this.props.sourceMapMode}`; const sourcesContent = this.props.sourcesContent ?? true; + const outFile = this.props.format === OutputFormat.ESM ? 'index.mjs' : 'index.js'; const esbuildCommand: string[] = [ options.esbuildRunner, - '--bundle', `"${pathJoin(options.inputDir, relativeEntryPath)}"`, + '--bundle', `"${relativeEntryPath}"`, `--target=${this.props.target ?? toTarget(this.props.runtime)}`, '--platform=node', - `--outfile="${pathJoin(options.outputDir, 'index.js')}"`, + ...this.props.format ? [`--format=${this.props.format}`] : [], + `--outfile="${pathJoin(options.outputDir, outFile)}"`, ...this.props.minify ? ['--minify'] : [], ...sourceMapEnabled ? [`--sourcemap${sourceMapValue}`] : [], ...sourcesContent ? [] : [`--sources-content=${sourcesContent}`], @@ -204,6 +197,7 @@ export class Bundling implements cdk.BundlingOptions { ...this.props.banner ? [`--banner:js=${JSON.stringify(this.props.banner)}`] : [], ...this.props.footer ? [`--footer:js=${JSON.stringify(this.props.footer)}`] : [], ...this.props.charset ? [`--charset=${this.props.charset}`] : [], + ...this.props.mainFields ? [`--main-fields=${this.props.mainFields.join(',')}`] : [], ]; let depsCommand = ''; diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts index 171df8ccbf385..83f135a12a97b 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts @@ -158,10 +158,11 @@ function findLockFile(depsLockFilePath?: string): string { * 1. Given entry file * 2. A .ts file named as the defining file with id as suffix (defining-file.id.ts) * 3. A .js file name as the defining file with id as suffix (defining-file.id.js) + * 4. A .mjs file name as the defining file with id as suffix (defining-file.id.mjs) */ function findEntry(id: string, entry?: string): string { if (entry) { - if (!/\.(jsx?|tsx?)$/.test(entry)) { + if (!/\.(jsx?|tsx?|mjs)$/.test(entry)) { throw new Error('Only JavaScript or TypeScript entry files are supported.'); } if (!fs.existsSync(entry)) { @@ -183,7 +184,12 @@ function findEntry(id: string, entry?: string): string { return jsHandlerFile; } - throw new Error(`Cannot find handler file ${tsHandlerFile} or ${jsHandlerFile}`); + const mjsHandlerFile = definingFile.replace(new RegExp(`${extname}$`), `.${id}.mjs`); + if (fs.existsSync(mjsHandlerFile)) { + return mjsHandlerFile; + } + + throw new Error(`Cannot find handler file ${tsHandlerFile}, ${jsHandlerFile} or ${mjsHandlerFile}`); } /** diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts index e16e9db8120b6..e43dc6d41be1e 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts @@ -263,6 +263,38 @@ export interface BundlingOptions { * @default - asset hash is calculated based on the bundled output */ readonly assetHash?: string; + + /** + * Output format for the generated JavaScript files + * + * @default OutputFormat.CJS + */ + readonly format?: OutputFormat; + + /** + * How to determine the entry point for modules. + * Try ['module', 'main'] to default to ES module versions. + * + * @default ['main', 'module'] + */ + readonly mainFields?: string[]; +} + +/** + * Output format for the generated JavaScript files + */ +export enum OutputFormat { + /** + * CommonJS + */ + CJS = 'cjs', + + /** + * ECMAScript module + * + * Requires a running environment that supports `import` and `export` syntax. + */ + ESM = 'esm' } /** diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/util.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/util.ts index 0ececb74ab95f..cc3d314c32416 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/util.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/util.ts @@ -144,3 +144,65 @@ export function extractDependencies(pkgPath: string, modules: string[]): { [key: return dependencies; } + +export function getTsconfigCompilerOptions(tsconfigPath: string): string { + const compilerOptions = extractTsConfig(tsconfigPath); + const excludedCompilerOptions = [ + 'composite', + 'tsBuildInfoFile', + ]; + + const options: Record = { + ...compilerOptions, + // Overrides + incremental: false, + // Intentionally Setting rootDir and outDir, so that the compiled js file always end up next to .ts file. + rootDir: './', + outDir: './', + }; + + let compilerOptionsString = ''; + Object.keys(options).forEach((key: string) => { + + if (excludedCompilerOptions.includes(key)) { + return; + } + + const value = options[key]; + const option = '--' + key; + const type = typeof value; + + if (type === 'boolean') { + if (value) { + compilerOptionsString += option + ' '; + } + } else if (type === 'string') { + compilerOptionsString += option + ' ' + value + ' '; + } else if (type === 'object') { + if (Array.isArray(value)) { + compilerOptionsString += option + ' ' + value.join(',') + ' '; + } + } else { + throw new Error(`Missing support for compilerOption: [${key}]: { ${type}, ${value}} \n`); + } + }); + + return compilerOptionsString.trim(); +} + + +function extractTsConfig(tsconfigPath: string, previousCompilerOptions?: Record): Record | undefined { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { extends: extendedConfig, compilerOptions } = require(tsconfigPath); + const updatedCompilerOptions = { + ...(previousCompilerOptions ?? {}), + ...compilerOptions, + }; + if (extendedConfig) { + return extractTsConfig( + path.resolve(tsconfigPath.replace(/[^\/]+$/, ''), extendedConfig), + updatedCompilerOptions, + ); + } + return updatedCompilerOptions; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-nodejs/package.json b/packages/@aws-cdk/aws-lambda-nodejs/package.json index bbb6b09e99693..76f4bdbbd1753 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/package.json +++ b/packages/@aws-cdk/aws-lambda-nodejs/package.json @@ -71,14 +71,14 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "delay": "5.0.0", - "esbuild": "^0.14.9" + "esbuild": "^0.14.23" }, "dependencies": { "@aws-cdk/aws-lambda": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts index 2f8b823dcce45..3224d1afbd014 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts @@ -6,7 +6,7 @@ import { AssetHashType, DockerImage } from '@aws-cdk/core'; import { version as delayVersion } from 'delay/package.json'; import { Bundling } from '../lib/bundling'; import { PackageInstallation } from '../lib/package-installation'; -import { Charset, LogLevel, SourceMapMode } from '../lib/types'; +import { Charset, LogLevel, OutputFormat, SourceMapMode } from '../lib/types'; import * as util from '../lib/util'; @@ -16,7 +16,6 @@ beforeEach(() => { jest.resetAllMocks(); jest.restoreAllMocks(); Bundling.clearEsbuildInstallationCache(); - Bundling.clearTscInstallationCache(); jest.spyOn(Code, 'fromAsset'); @@ -184,7 +183,7 @@ test('esbuild bundling with esbuild options', () => { entry, projectRoot, depsLockFilePath, - runtime: Runtime.NODEJS_12_X, + runtime: Runtime.NODEJS_14_X, architecture: Architecture.X86_64, minify: true, sourceMap: true, @@ -201,12 +200,14 @@ test('esbuild bundling with esbuild options', () => { footer: '/* comments */', charset: Charset.UTF8, forceDockerBundling: true, + mainFields: ['module', 'main'], define: { 'process.env.KEY': JSON.stringify('VALUE'), 'process.env.BOOL': 'true', 'process.env.NUMBER': '7777', 'process.env.STRING': JSON.stringify('this is a "test"'), }, + format: OutputFormat.ESM, }); // Correctly bundles with esbuild @@ -218,12 +219,12 @@ test('esbuild bundling with esbuild options', () => { 'bash', '-c', [ 'esbuild --bundle "/asset-input/lib/handler.ts"', - '--target=es2020 --platform=node --outfile="/asset-output/index.js"', + '--target=es2020 --platform=node --format=esm --outfile="/asset-output/index.mjs"', '--minify --sourcemap --sources-content=false --external:aws-sdk --loader:.png=dataurl', defineInstructions, '--log-level=silent --keep-names --tsconfig=/asset-input/lib/custom-tsconfig.ts', '--metafile=/asset-output/index.meta.json --banner:js="/* comments */" --footer:js="/* comments */"', - '--charset=utf8', + '--charset=utf8 --main-fields=module,main', ].join(' '), ], }), @@ -234,6 +235,17 @@ test('esbuild bundling with esbuild options', () => { expect(bundleProcess.stdout.toString()).toMatchSnapshot(); }); +test('throws with ESM and NODEJS_12_X', () => { + expect(() => Bundling.bundle({ + entry, + projectRoot, + depsLockFilePath, + runtime: Runtime.NODEJS_12_X, + architecture: Architecture.X86_64, + format: OutputFormat.ESM, + })).toThrow(/ECMAScript module output format is not supported by the nodejs12.x runtime/); +}); + test('esbuild bundling source map default', () => { Bundling.bundle({ entry, @@ -561,61 +573,6 @@ test('esbuild bundling with projectRoot and externals and dependencies', () => { }); test('esbuild bundling with pre compilations', () => { - Bundling.bundle({ - entry, - projectRoot, - depsLockFilePath, - runtime: Runtime.NODEJS_14_X, - forceDockerBundling: true, - tsconfig, - preCompilation: true, - architecture: Architecture.X86_64, - }); - - // Correctly bundles with esbuild - expect(Code.fromAsset).toHaveBeenCalledWith(path.dirname(depsLockFilePath), { - assetHashType: AssetHashType.OUTPUT, - bundling: expect.objectContaining({ - command: [ - 'bash', '-c', - [ - 'tsc --project /asset-input/lib/custom-tsconfig.ts --rootDir ./ --outDir ./ &&', - 'esbuild --bundle \"/asset-input/lib/handler.js\" --target=node14 --platform=node --outfile=\"/asset-output/index.js\"', - '--external:aws-sdk --tsconfig=/asset-input/lib/custom-tsconfig.ts', - ].join(' '), - ], - }), - }); - - Bundling.bundle({ - entry, - projectRoot, - depsLockFilePath, - runtime: Runtime.NODEJS_14_X, - forceDockerBundling: true, - tsconfig, - preCompilation: true, - architecture: Architecture.X86_64, - }); - - // Correctly bundles with esbuild - expect(Code.fromAsset).toHaveBeenCalledWith(path.dirname(depsLockFilePath), { - assetHashType: AssetHashType.OUTPUT, - bundling: expect.objectContaining({ - command: [ - 'bash', '-c', - [ - 'esbuild --bundle \"/asset-input/lib/handler.js\" --target=node14 --platform=node --outfile=\"/asset-output/index.js\"', - '--external:aws-sdk --tsconfig=/asset-input/lib/custom-tsconfig.ts', - ].join(' '), - ], - }), - }); - -}); - -test('esbuild bundling with pre compilations with undefined tsconfig ( Should find in root directory )', () => { - Bundling.clearTscCompilationCache(); const packageLock = path.join(__dirname, '..', 'package-lock.json'); Bundling.bundle({ @@ -623,11 +580,13 @@ test('esbuild bundling with pre compilations with undefined tsconfig ( Should fi projectRoot: path.dirname(packageLock), depsLockFilePath: packageLock, runtime: Runtime.NODEJS_14_X, - forceDockerBundling: true, preCompilation: true, + forceDockerBundling: true, architecture: Architecture.X86_64, }); + const compilerOptions = util.getTsconfigCompilerOptions(path.join(__dirname, '..', 'tsconfig.json')); + // Correctly bundles with esbuild expect(Code.fromAsset).toHaveBeenCalledWith(path.dirname(packageLock), { assetHashType: AssetHashType.OUTPUT, @@ -635,16 +594,15 @@ test('esbuild bundling with pre compilations with undefined tsconfig ( Should fi command: [ 'bash', '-c', [ - 'tsc --project /asset-input/tsconfig.json --rootDir ./ --outDir ./ &&', - 'esbuild --bundle \"/asset-input/test/bundling.test.js\" --target=node14 --platform=node --outfile=\"/asset-output/index.js\"', - '--external:aws-sdk', + `tsc \"/asset-input/test/bundling.test.ts\" ${compilerOptions} &&`, + 'esbuild --bundle \"/asset-input/test/bundling.test.js\" --target=node14 --platform=node --outfile=\"/asset-output/index.js\" --external:aws-sdk', ].join(' '), ], }), }); }); -test('esbuild bundling with pre compilations and undefined tsconfig ( Should throw) ', () => { +test('throws with pre compilation and not found tsconfig', () => { expect(() => { Bundling.bundle({ entry, @@ -655,7 +613,7 @@ test('esbuild bundling with pre compilations and undefined tsconfig ( Should thr preCompilation: true, architecture: Architecture.X86_64, }); - }).toThrow('Cannot find a tsconfig.json, please specify the prop: tsconfig'); + }).toThrow('Cannot find a `tsconfig.json` but `preCompilation` is set to `true`, please specify it via `tsconfig`'); }); diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.handler3.mjs b/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.handler3.mjs new file mode 100644 index 0000000000000..33af638be9b99 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.handler3.mjs @@ -0,0 +1 @@ +// Dummy for test purposes diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts index 83ea2131c043a..f197bb34b60da 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts @@ -1,7 +1,6 @@ -import '@aws-cdk/assert-internal/jest'; import * as fs from 'fs'; import * as path from 'path'; -import { ABSENT } from '@aws-cdk/assert-internal'; +import { Template, Match } from '@aws-cdk/assertions'; import { Vpc } from '@aws-cdk/aws-ec2'; import { CodeConfig, Runtime } from '@aws-cdk/aws-lambda'; import { Stack } from '@aws-cdk/core'; @@ -40,7 +39,7 @@ test('NodejsFunction with .ts handler', () => { entry: expect.stringContaining('function.test.handler1.ts'), // Automatically finds .ts handler file })); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Handler: 'index.handler', Runtime: 'nodejs14.x', }); @@ -56,6 +55,17 @@ test('NodejsFunction with .js handler', () => { })); }); +test('NodejsFunction with .mjs handler', () => { + // WHEN + new NodejsFunction(stack, 'handler3'); + + + // THEN + expect(Bundling.bundle).toHaveBeenCalledWith(expect.objectContaining({ + entry: expect.stringContaining('function.test.handler3.mjs'), // Automatically finds .mjs handler file + })); +}); + test('NodejsFunction with container env vars', () => { // WHEN new NodejsFunction(stack, 'handler1', { @@ -98,7 +108,7 @@ test('throws when entry does not exist', () => { }); test('throws when entry cannot be automatically found', () => { - expect(() => new NodejsFunction(stack, 'Fn')).toThrow(/Cannot find handler file .*function.test.Fn.ts or .*function.test.Fn.js/); + expect(() => new NodejsFunction(stack, 'Fn')).toThrow(/Cannot find handler file .*function.test.Fn.ts, .*function.test.Fn.js or .*function.test.Fn.mjs/); }); test('throws with the wrong runtime family', () => { @@ -144,7 +154,7 @@ test('configures connection reuse for aws sdk', () => { // WHEN new NodejsFunction(stack, 'handler1'); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Environment: { Variables: { AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1', @@ -159,8 +169,8 @@ test('can opt-out of connection reuse for aws sdk', () => { awsSdkConnectionReuse: false, }); - expect(stack).toHaveResource('AWS::Lambda::Function', { - Environment: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { + Environment: Match.absent(), }); }); @@ -172,7 +182,7 @@ test('NodejsFunction in a VPC', () => { new NodejsFunction(stack, 'handler1', { vpc }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { VpcConfig: { SecurityGroupIds: [ { diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/integ-handlers/esm.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/integ-handlers/esm.ts new file mode 100644 index 0000000000000..29f247f3942ab --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/integ-handlers/esm.ts @@ -0,0 +1,6 @@ +/* eslint-disable no-console */ +import * as crypto from 'crypto'; + +export async function handler() { + console.log(crypto.createHash('sha512').update('cdk').digest('hex')); +} diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/integ.esm.expected.json b/packages/@aws-cdk/aws-lambda-nodejs/test/integ.esm.expected.json new file mode 100644 index 0000000000000..8e6b8cabf01c6 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/integ.esm.expected.json @@ -0,0 +1,108 @@ +{ + "Resources": { + "esmServiceRole84AC2522": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "esm9B397D27": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParametersa111e7aee76f0a755b83f3d35098efc1659ba3915bd52dc401cb3a972573d616S3BucketD8FC0ACA" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersa111e7aee76f0a755b83f3d35098efc1659ba3915bd52dc401cb3a972573d616S3VersionKeyF7C65CF0" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersa111e7aee76f0a755b83f3d35098efc1659ba3915bd52dc401cb3a972573d616S3VersionKeyF7C65CF0" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "esmServiceRole84AC2522", + "Arn" + ] + }, + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1" + } + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x" + }, + "DependsOn": [ + "esmServiceRole84AC2522" + ] + } + }, + "Parameters": { + "AssetParametersa111e7aee76f0a755b83f3d35098efc1659ba3915bd52dc401cb3a972573d616S3BucketD8FC0ACA": { + "Type": "String", + "Description": "S3 bucket for asset \"a111e7aee76f0a755b83f3d35098efc1659ba3915bd52dc401cb3a972573d616\"" + }, + "AssetParametersa111e7aee76f0a755b83f3d35098efc1659ba3915bd52dc401cb3a972573d616S3VersionKeyF7C65CF0": { + "Type": "String", + "Description": "S3 key for asset version \"a111e7aee76f0a755b83f3d35098efc1659ba3915bd52dc401cb3a972573d616\"" + }, + "AssetParametersa111e7aee76f0a755b83f3d35098efc1659ba3915bd52dc401cb3a972573d616ArtifactHashDDFE4A88": { + "Type": "String", + "Description": "Artifact hash for asset \"a111e7aee76f0a755b83f3d35098efc1659ba3915bd52dc401cb3a972573d616\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/integ.esm.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/integ.esm.ts new file mode 100644 index 0000000000000..acf0ac363489b --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/integ.esm.ts @@ -0,0 +1,21 @@ +import * as path from 'path'; +import { App, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as lambda from '../lib'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + new lambda.NodejsFunction(this, 'esm', { + entry: path.join(__dirname, 'integ-handlers/esm.ts'), + bundling: { + format: lambda.OutputFormat.ESM, + }, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-lambda-nodejs-esm'); +app.synth(); diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/util.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/util.test.ts index d2249f4f59118..085c333579933 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/util.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/util.test.ts @@ -1,7 +1,7 @@ import * as child_process from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; -import { callsites, exec, extractDependencies, findUp, findUpMultiple } from '../lib/util'; +import { callsites, exec, extractDependencies, findUp, findUpMultiple, getTsconfigCompilerOptions } from '../lib/util'; beforeEach(() => { jest.clearAllMocks(); @@ -179,3 +179,18 @@ describe('extractDependencies', () => { fs.unlinkSync(pkgPath); }); }); + +describe('getTsconfigCompilerOptions', () => { + test('should extract compiler options and returns as string', () => { + const tsconfig = path.join(__dirname, '..', 'tsconfig.json'); + const compilerOptions = getTsconfigCompilerOptions(tsconfig); + expect(compilerOptions).toEqual([ + '--alwaysStrict --charset utf8 --declaration --experimentalDecorators', + '--inlineSourceMap --inlineSources --lib es2019 --module CommonJS', + '--newLine lf --noEmitOnError --noFallthroughCasesInSwitch --noImplicitAny', + '--noImplicitReturns --noImplicitThis --noUnusedLocals --noUnusedParameters', + '--resolveJsonModule --strict --strictNullChecks --strictPropertyInitialization', + '--target ES2019 --rootDir ./ --outDir ./', + ].join(' ')); + }); +}); diff --git a/packages/@aws-cdk/aws-lambda-python/README.md b/packages/@aws-cdk/aws-lambda-python/README.md index 2a8de2d34c4de..ef52db457ef2b 100644 --- a/packages/@aws-cdk/aws-lambda-python/README.md +++ b/packages/@aws-cdk/aws-lambda-python/README.md @@ -167,9 +167,34 @@ new lambda.PythonFunction(this, 'function', { entry, runtime: Runtime.PYTHON_3_8, bundling: { - buildArgs: { PIP_INDEX_URL: indexUrl }, + environment: { PIP_INDEX_URL: indexUrl }, }, }); ``` -This type of an example should work for `pip` and `poetry` based dependencies, but will not work for `pipenv`. +The index URL or the token are only used during bundling and thus not included in the final asset. Setting only environment variable for `PIP_INDEX_URL` or `PIP_EXTRA_INDEX_URL` should work for accesing private Python repositories with `pip`, `pipenv` and `poetry` based dependencies. + +If you also want to use the Code Artifact repo for building the base Docker image for bundling, use `buildArgs`. However, note that setting custom build args for bundling will force the base bundling image to be rebuilt every time (i.e. skip the Docker cache). Build args can be customized as: + +```ts +import { execSync } from 'child_process'; + +const entry = '/path/to/function'; +const image = DockerImage.fromBuild(entry); + +const domain = 'my-domain'; +const domainOwner = '111122223333'; +const repoName = 'my_repo'; +const region = 'us-east-1'; +const codeArtifactAuthToken = execSync(`aws codeartifact get-authorization-token --domain ${domain} --domain-owner ${domainOwner} --query authorizationToken --output text`).toString().trim(); + +const indexUrl = `https://aws:${codeArtifactAuthToken}@${domain}-${domainOwner}.d.codeartifact.${region}.amazonaws.com/pypi/${repoName}/simple/`; + +new lambda.PythonFunction(this, 'function', { + entry, + runtime: Runtime.PYTHON_3_8, + bundling: { + buildArgs: { PIP_INDEX_URL: indexUrl }, + }, +}); +``` diff --git a/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts b/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts index 37e52ac13435b..bd2020a5a228b 100644 --- a/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts +++ b/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts @@ -51,6 +51,7 @@ export class Bundling implements CdkBundlingOptions { public readonly image: DockerImage; public readonly command: string[]; + public readonly environment?: { [key: string]: string }; constructor(props: BundlingProps) { const { @@ -78,6 +79,7 @@ export class Bundling implements CdkBundlingOptions { }); this.image = image ?? defaultImage; this.command = ['bash', '-c', chain(bundlingCommands)]; + this.environment = props.environment; } private createBundlingCommand(options: BundlingCommandOptions): string[] { @@ -86,8 +88,8 @@ export class Bundling implements CdkBundlingOptions { bundlingCommands.push(packaging.exportCommand ?? ''); if (packaging.dependenciesFile) { bundlingCommands.push(`python -m pip install -r ${DependenciesFile.PIP} -t ${options.outputDir}`); - }; - bundlingCommands.push(`cp -R ${options.inputDir} ${options.outputDir}`); + } + bundlingCommands.push(`cp -rT ${options.inputDir}/ ${options.outputDir}`); return bundlingCommands; } } diff --git a/packages/@aws-cdk/aws-lambda-python/lib/types.ts b/packages/@aws-cdk/aws-lambda-python/lib/types.ts index 1f2b1e8c7aabf..e818eadc4401b 100644 --- a/packages/@aws-cdk/aws-lambda-python/lib/types.ts +++ b/packages/@aws-cdk/aws-lambda-python/lib/types.ts @@ -30,6 +30,13 @@ export interface BundlingOptions { */ readonly buildArgs?: { [key: string]: string }; + /** + * Environment variables defined when bundling runs. + * + * @default - no environment variables are defined. + */ + readonly environment?: { [key: string]: string; }; + /** * Determines how asset hash is calculated. Assets will get rebuild and * uploaded only if their hash has changed. diff --git a/packages/@aws-cdk/aws-lambda-python/package.json b/packages/@aws-cdk/aws-lambda-python/package.json index d2ae8bfb8d4e7..74a9d8a8ffc0c 100644 --- a/packages/@aws-cdk/aws-lambda-python/package.json +++ b/packages/@aws-cdk/aws-lambda-python/package.json @@ -75,7 +75,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-ec2": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts b/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts index 5e4cf194f5151..a75ecc8625960 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts @@ -1,3 +1,4 @@ +import * as fs from 'fs'; import * as path from 'path'; import { Architecture, Code, Runtime } from '@aws-cdk/aws-lambda'; import { DockerImage } from '@aws-cdk/core'; @@ -25,7 +26,7 @@ beforeEach(() => { test('Bundling a function without dependencies', () => { const entry = path.join(__dirname, 'lambda-handler-nodeps'); - Bundling.bundle({ + const assetCode = Bundling.bundle({ entry: entry, runtime: Runtime.PYTHON_3_7, architecture: Architecture.X86_64, @@ -36,7 +37,7 @@ test('Bundling a function without dependencies', () => { bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'cp -R /asset-input /asset-output', + 'cp -rT /asset-input/ /asset-output', ], }), })); @@ -47,11 +48,14 @@ test('Bundling a function without dependencies', () => { }), platform: 'linux/amd64', })); + + const files = fs.readdirSync(assetCode.path); + expect(files).toContain('index.py'); }); test('Bundling a function with requirements.txt', () => { const entry = path.join(__dirname, 'lambda-handler'); - Bundling.bundle({ + const assetCode = Bundling.bundle({ entry: entry, runtime: Runtime.PYTHON_3_7, architecture: Architecture.X86_64, @@ -62,10 +66,14 @@ test('Bundling a function with requirements.txt', () => { bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'python -m pip install -r requirements.txt -t /asset-output && cp -R /asset-input /asset-output', + 'python -m pip install -r requirements.txt -t /asset-output && cp -rT /asset-input/ /asset-output', ], }), })); + + const files = fs.readdirSync(assetCode.path); + expect(files).toContain('index.py'); + expect(files).toContain('requirements.txt'); }); test('Bundling Python 2.7 with requirements.txt installed', () => { @@ -81,7 +89,7 @@ test('Bundling Python 2.7 with requirements.txt installed', () => { bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'python -m pip install -r requirements.txt -t /asset-output && cp -R /asset-input /asset-output', + 'python -m pip install -r requirements.txt -t /asset-output && cp -rT /asset-input/ /asset-output', ], }), })); @@ -101,7 +109,7 @@ test('Bundling a layer with dependencies', () => { bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'python -m pip install -r requirements.txt -t /asset-output/python && cp -R /asset-input /asset-output/python', + 'python -m pip install -r requirements.txt -t /asset-output/python && cp -rT /asset-input/ /asset-output/python', ], }), })); @@ -121,7 +129,7 @@ test('Bundling a python code layer', () => { bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'cp -R /asset-input /asset-output/python', + 'cp -rT /asset-input/ /asset-output/python', ], }), })); @@ -130,7 +138,7 @@ test('Bundling a python code layer', () => { test('Bundling a function with pipenv dependencies', () => { const entry = path.join(__dirname, 'lambda-handler-pipenv'); - Bundling.bundle({ + const assetCode = Bundling.bundle({ entry: path.join(entry, '.'), runtime: Runtime.PYTHON_3_9, architecture: Architecture.X86_64, @@ -141,16 +149,23 @@ test('Bundling a function with pipenv dependencies', () => { bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'PIPENV_VENV_IN_PROJECT=1 pipenv lock -r > requirements.txt && rm -rf .venv && python -m pip install -r requirements.txt -t /asset-output/python && cp -R /asset-input /asset-output/python', + 'PIPENV_VENV_IN_PROJECT=1 pipenv lock -r > requirements.txt && rm -rf .venv && python -m pip install -r requirements.txt -t /asset-output/python && cp -rT /asset-input/ /asset-output/python', ], }), })); + + const files = fs.readdirSync(assetCode.path); + expect(files).toContain('index.py'); + expect(files).toContain('Pipfile'); + expect(files).toContain('Pipfile.lock'); + // Contains hidden files. + expect(files).toContain('.gitignore'); }); test('Bundling a function with poetry dependencies', () => { const entry = path.join(__dirname, 'lambda-handler-poetry'); - Bundling.bundle({ + const assetCode = Bundling.bundle({ entry: path.join(entry, '.'), runtime: Runtime.PYTHON_3_9, architecture: Architecture.X86_64, @@ -161,10 +176,17 @@ test('Bundling a function with poetry dependencies', () => { bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'poetry export --with-credentials --format requirements.txt --output requirements.txt && python -m pip install -r requirements.txt -t /asset-output/python && cp -R /asset-input /asset-output/python', + 'poetry export --with-credentials --format requirements.txt --output requirements.txt && python -m pip install -r requirements.txt -t /asset-output/python && cp -rT /asset-input/ /asset-output/python', ], }), })); + + const files = fs.readdirSync(assetCode.path); + expect(files).toContain('index.py'); + expect(files).toContain('pyproject.toml'); + expect(files).toContain('poetry.lock'); + // Contains hidden files. + expect(files).toContain('.gitignore'); }); test('Bundling a function with custom bundling image', () => { @@ -184,7 +206,7 @@ test('Bundling a function with custom bundling image', () => { image, command: [ 'bash', '-c', - 'python -m pip install -r requirements.txt -t /asset-output/python && cp -R /asset-input /asset-output/python', + 'python -m pip install -r requirements.txt -t /asset-output/python && cp -rT /asset-input/ /asset-output/python', ], }), })); @@ -207,3 +229,22 @@ test('Bundling with custom build args', () => { }), })); }); + +test('Bundling with custom environment vars`', () => { + const entry = path.join(__dirname, 'lambda-handler'); + Bundling.bundle({ + entry: entry, + runtime: Runtime.PYTHON_3_7, + environment: { + KEY: 'value', + }, + }); + + expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({ + bundling: expect.objectContaining({ + environment: { + KEY: 'value', + }, + }), + })); +}); diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-custom-build/index.py b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-custom-build/index.py index c033f37560534..04f99eb108b30 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-custom-build/index.py +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-custom-build/index.py @@ -1,11 +1,8 @@ import requests -from PIL import Image def handler(event, context): response = requests.get('https://a0.awsstatic.com/main/images/logos/aws_smile-header-desktop-en-white_59x35.png', stream=True) - img = Image.open(response.raw) print(response.status_code) - print(img.size) return response.status_code diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-custom-build/requirements.txt b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-custom-build/requirements.txt index c636db83b8c9e..4fcd85719fe3a 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-custom-build/requirements.txt +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-custom-build/requirements.txt @@ -5,5 +5,3 @@ idna==2.10 urllib3==1.26.7 # Requests used by this lambda requests==2.26.0 -# Pillow 6.x so that python 2.7 and 3.x can both use this fixture -Pillow==8.4.0 diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/Pipfile b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/Pipfile index a309b821c5801..78d783bc4b9b0 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/Pipfile +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/Pipfile @@ -5,4 +5,3 @@ verify_ssl = true [packages] requests = "==2.26.0" -Pillow = "==8.4.0" diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/Pipfile.lock b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/Pipfile.lock index f92befb9e3dd6..441acc679505f 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/Pipfile.lock +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "fe29bbb3f12db421fd27678820291d33cf6b3dce6bb189274449dee89cf434e8" + "sha256": "6cfaa5a495be5cf47942a14b04d50e639f14743101e621684e86449dbac8da61" }, "pipfile-spec": 6, "requires": {}, @@ -23,11 +23,11 @@ }, "charset-normalizer": { "hashes": [ - "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721", - "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c" + "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd", + "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455" ], "markers": "python_version >= '3'", - "version": "==2.0.9" + "version": "==2.0.10" }, "idna": { "hashes": [ @@ -37,53 +37,6 @@ "markers": "python_version >= '3'", "version": "==3.3" }, - "pillow": { - "hashes": [ - "sha256:066f3999cb3b070a95c3652712cffa1a748cd02d60ad7b4e485c3748a04d9d76", - "sha256:0a0956fdc5defc34462bb1c765ee88d933239f9a94bc37d132004775241a7585", - "sha256:0b052a619a8bfcf26bd8b3f48f45283f9e977890263e4571f2393ed8898d331b", - "sha256:1394a6ad5abc838c5cd8a92c5a07535648cdf6d09e8e2d6df916dfa9ea86ead8", - "sha256:1bc723b434fbc4ab50bb68e11e93ce5fb69866ad621e3c2c9bdb0cd70e345f55", - "sha256:244cf3b97802c34c41905d22810846802a3329ddcb93ccc432870243211c79fc", - "sha256:25a49dc2e2f74e65efaa32b153527fc5ac98508d502fa46e74fa4fd678ed6645", - "sha256:2e4440b8f00f504ee4b53fe30f4e381aae30b0568193be305256b1462216feff", - "sha256:3862b7256046fcd950618ed22d1d60b842e3a40a48236a5498746f21189afbbc", - "sha256:3eb1ce5f65908556c2d8685a8f0a6e989d887ec4057326f6c22b24e8a172c66b", - "sha256:3f97cfb1e5a392d75dd8b9fd274d205404729923840ca94ca45a0af57e13dbe6", - "sha256:493cb4e415f44cd601fcec11c99836f707bb714ab03f5ed46ac25713baf0ff20", - "sha256:4acc0985ddf39d1bc969a9220b51d94ed51695d455c228d8ac29fcdb25810e6e", - "sha256:5503c86916d27c2e101b7f71c2ae2cddba01a2cf55b8395b0255fd33fa4d1f1a", - "sha256:5b7bb9de00197fb4261825c15551adf7605cf14a80badf1761d61e59da347779", - "sha256:5e9ac5f66616b87d4da618a20ab0a38324dbe88d8a39b55be8964eb520021e02", - "sha256:620582db2a85b2df5f8a82ddeb52116560d7e5e6b055095f04ad828d1b0baa39", - "sha256:62cc1afda735a8d109007164714e73771b499768b9bb5afcbbee9d0ff374b43f", - "sha256:70ad9e5c6cb9b8487280a02c0ad8a51581dcbbe8484ce058477692a27c151c0a", - "sha256:72b9e656e340447f827885b8d7a15fc8c4e68d410dc2297ef6787eec0f0ea409", - "sha256:72cbcfd54df6caf85cc35264c77ede902452d6df41166010262374155947460c", - "sha256:792e5c12376594bfcb986ebf3855aa4b7c225754e9a9521298e460e92fb4a488", - "sha256:7b7017b61bbcdd7f6363aeceb881e23c46583739cb69a3ab39cb384f6ec82e5b", - "sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d", - "sha256:82aafa8d5eb68c8463b6e9baeb4f19043bb31fefc03eb7b216b51e6a9981ae09", - "sha256:84c471a734240653a0ec91dec0996696eea227eafe72a33bd06c92697728046b", - "sha256:8c803ac3c28bbc53763e6825746f05cc407b20e4a69d0122e526a582e3b5e153", - "sha256:93ce9e955cc95959df98505e4608ad98281fff037350d8c2671c9aa86bcf10a9", - "sha256:9a3e5ddc44c14042f0844b8cf7d2cd455f6cc80fd7f5eefbe657292cf601d9ad", - "sha256:a4901622493f88b1a29bd30ec1a2f683782e57c3c16a2dbc7f2595ba01f639df", - "sha256:a5a4532a12314149d8b4e4ad8ff09dde7427731fcfa5917ff16d0291f13609df", - "sha256:b8831cb7332eda5dc89b21a7bce7ef6ad305548820595033a4b03cf3091235ed", - "sha256:b8e2f83c56e141920c39464b852de3719dfbfb6e3c99a2d8da0edf4fb33176ed", - "sha256:c70e94281588ef053ae8998039610dbd71bc509e4acbc77ab59d7d2937b10698", - "sha256:c8a17b5d948f4ceeceb66384727dde11b240736fddeda54ca740b9b8b1556b29", - "sha256:d82cdb63100ef5eedb8391732375e6d05993b765f72cb34311fab92103314649", - "sha256:d89363f02658e253dbd171f7c3716a5d340a24ee82d38aab9183f7fdf0cdca49", - "sha256:d99ec152570e4196772e7a8e4ba5320d2d27bf22fdf11743dd882936ed64305b", - "sha256:ddc4d832a0f0b4c52fff973a0d44b6c99839a9d016fe4e6a1cb8f3eea96479c2", - "sha256:e3dacecfbeec9a33e932f00c6cd7996e62f53ad46fbe677577394aaa90ee419a", - "sha256:eb9fc393f3c61f9054e1ed26e6fe912c7321af2f41ff49d3f83d05bacf22cc78" - ], - "index": "pypi", - "version": "==8.4.0" - }, "requests": { "hashes": [ "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", @@ -94,11 +47,11 @@ }, "urllib3": { "hashes": [ - "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", - "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" + "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", + "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.7" + "version": "==1.26.8" } }, "develop": {} diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/index.py b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/index.py index c033f37560534..04f99eb108b30 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/index.py +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/index.py @@ -1,11 +1,8 @@ import requests -from PIL import Image def handler(event, context): response = requests.get('https://a0.awsstatic.com/main/images/logos/aws_smile-header-desktop-en-white_59x35.png', stream=True) - img = Image.open(response.raw) print(response.status_code) - print(img.size) return response.status_code diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-poetry/index.py b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-poetry/index.py index c033f37560534..04f99eb108b30 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-poetry/index.py +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-poetry/index.py @@ -1,11 +1,8 @@ import requests -from PIL import Image def handler(event, context): response = requests.get('https://a0.awsstatic.com/main/images/logos/aws_smile-header-desktop-en-white_59x35.png', stream=True) - img = Image.open(response.raw) print(response.status_code) - print(img.size) return response.status_code diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-poetry/poetry.lock b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-poetry/poetry.lock index d07a92e9ef100..6b59241f10c2d 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-poetry/poetry.lock +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-poetry/poetry.lock @@ -25,14 +25,6 @@ category = "main" optional = false python-versions = ">=3.5" -[[package]] -name = "pillow" -version = "8.4.0" -description = "Python Imaging Library (Fork)" -category = "main" -optional = false -python-versions = ">=3.6" - [[package]] name = "requests" version = "2.26.0" @@ -82,49 +74,6 @@ idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] -pillow = [ - {file = "Pillow-8.4.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d"}, - {file = "Pillow-8.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f97cfb1e5a392d75dd8b9fd274d205404729923840ca94ca45a0af57e13dbe6"}, - {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb9fc393f3c61f9054e1ed26e6fe912c7321af2f41ff49d3f83d05bacf22cc78"}, - {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d82cdb63100ef5eedb8391732375e6d05993b765f72cb34311fab92103314649"}, - {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc1afda735a8d109007164714e73771b499768b9bb5afcbbee9d0ff374b43f"}, - {file = "Pillow-8.4.0-cp310-cp310-win32.whl", hash = "sha256:e3dacecfbeec9a33e932f00c6cd7996e62f53ad46fbe677577394aaa90ee419a"}, - {file = "Pillow-8.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:620582db2a85b2df5f8a82ddeb52116560d7e5e6b055095f04ad828d1b0baa39"}, - {file = "Pillow-8.4.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:1bc723b434fbc4ab50bb68e11e93ce5fb69866ad621e3c2c9bdb0cd70e345f55"}, - {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72cbcfd54df6caf85cc35264c77ede902452d6df41166010262374155947460c"}, - {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70ad9e5c6cb9b8487280a02c0ad8a51581dcbbe8484ce058477692a27c151c0a"}, - {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25a49dc2e2f74e65efaa32b153527fc5ac98508d502fa46e74fa4fd678ed6645"}, - {file = "Pillow-8.4.0-cp36-cp36m-win32.whl", hash = "sha256:93ce9e955cc95959df98505e4608ad98281fff037350d8c2671c9aa86bcf10a9"}, - {file = "Pillow-8.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2e4440b8f00f504ee4b53fe30f4e381aae30b0568193be305256b1462216feff"}, - {file = "Pillow-8.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8c803ac3c28bbc53763e6825746f05cc407b20e4a69d0122e526a582e3b5e153"}, - {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a17b5d948f4ceeceb66384727dde11b240736fddeda54ca740b9b8b1556b29"}, - {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1394a6ad5abc838c5cd8a92c5a07535648cdf6d09e8e2d6df916dfa9ea86ead8"}, - {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:792e5c12376594bfcb986ebf3855aa4b7c225754e9a9521298e460e92fb4a488"}, - {file = "Pillow-8.4.0-cp37-cp37m-win32.whl", hash = "sha256:d99ec152570e4196772e7a8e4ba5320d2d27bf22fdf11743dd882936ed64305b"}, - {file = "Pillow-8.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7b7017b61bbcdd7f6363aeceb881e23c46583739cb69a3ab39cb384f6ec82e5b"}, - {file = "Pillow-8.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:d89363f02658e253dbd171f7c3716a5d340a24ee82d38aab9183f7fdf0cdca49"}, - {file = "Pillow-8.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a0956fdc5defc34462bb1c765ee88d933239f9a94bc37d132004775241a7585"}, - {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b7bb9de00197fb4261825c15551adf7605cf14a80badf1761d61e59da347779"}, - {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72b9e656e340447f827885b8d7a15fc8c4e68d410dc2297ef6787eec0f0ea409"}, - {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5a4532a12314149d8b4e4ad8ff09dde7427731fcfa5917ff16d0291f13609df"}, - {file = "Pillow-8.4.0-cp38-cp38-win32.whl", hash = "sha256:82aafa8d5eb68c8463b6e9baeb4f19043bb31fefc03eb7b216b51e6a9981ae09"}, - {file = "Pillow-8.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:066f3999cb3b070a95c3652712cffa1a748cd02d60ad7b4e485c3748a04d9d76"}, - {file = "Pillow-8.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:5503c86916d27c2e101b7f71c2ae2cddba01a2cf55b8395b0255fd33fa4d1f1a"}, - {file = "Pillow-8.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4acc0985ddf39d1bc969a9220b51d94ed51695d455c228d8ac29fcdb25810e6e"}, - {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b052a619a8bfcf26bd8b3f48f45283f9e977890263e4571f2393ed8898d331b"}, - {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:493cb4e415f44cd601fcec11c99836f707bb714ab03f5ed46ac25713baf0ff20"}, - {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8831cb7332eda5dc89b21a7bce7ef6ad305548820595033a4b03cf3091235ed"}, - {file = "Pillow-8.4.0-cp39-cp39-win32.whl", hash = "sha256:5e9ac5f66616b87d4da618a20ab0a38324dbe88d8a39b55be8964eb520021e02"}, - {file = "Pillow-8.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:3eb1ce5f65908556c2d8685a8f0a6e989d887ec4057326f6c22b24e8a172c66b"}, - {file = "Pillow-8.4.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ddc4d832a0f0b4c52fff973a0d44b6c99839a9d016fe4e6a1cb8f3eea96479c2"}, - {file = "Pillow-8.4.0-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3e5ddc44c14042f0844b8cf7d2cd455f6cc80fd7f5eefbe657292cf601d9ad"}, - {file = "Pillow-8.4.0-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70e94281588ef053ae8998039610dbd71bc509e4acbc77ab59d7d2937b10698"}, - {file = "Pillow-8.4.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:3862b7256046fcd950618ed22d1d60b842e3a40a48236a5498746f21189afbbc"}, - {file = "Pillow-8.4.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4901622493f88b1a29bd30ec1a2f683782e57c3c16a2dbc7f2595ba01f639df"}, - {file = "Pillow-8.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84c471a734240653a0ec91dec0996696eea227eafe72a33bd06c92697728046b"}, - {file = "Pillow-8.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:244cf3b97802c34c41905d22810846802a3329ddcb93ccc432870243211c79fc"}, - {file = "Pillow-8.4.0.tar.gz", hash = "sha256:b8e2f83c56e141920c39464b852de3719dfbfb6e3c99a2d8da0edf4fb33176ed"}, -] requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-poetry/pyproject.toml b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-poetry/pyproject.toml index c4dd461c007a7..6d90c4b4fec9b 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-poetry/pyproject.toml +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-poetry/pyproject.toml @@ -7,7 +7,6 @@ authors = ["Your Name "] [tool.poetry.dependencies] python = "^3.6" requests = "2.26.0" -Pillow = "8.4.0" [tool.poetry.dev-dependencies] diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/lambda/index.py b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/lambda/index.py index 6ac592242c8fb..fb1e8bb1ce0ab 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/lambda/index.py +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/lambda/index.py @@ -1,12 +1,9 @@ import requests -from PIL import Image import shared def handler(event, context): response = requests.get(shared.get_url(), stream=True) - img = Image.open(response.raw) print(response.status_code) - print(img.size) return response.status_code diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/shared/requirements.txt b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/shared/requirements.txt index d87aff1f66a75..eff24435fa632 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/shared/requirements.txt +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/shared/requirements.txt @@ -4,5 +4,3 @@ chardet==3.0.4 idna==2.10 urllib3==1.26.7 # Requests used by this lambda -requests==2.26.0 -Pillow==8.4.0 diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/index.py b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/index.py index c033f37560534..04f99eb108b30 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/index.py +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/index.py @@ -1,11 +1,8 @@ import requests -from PIL import Image def handler(event, context): response = requests.get('https://a0.awsstatic.com/main/images/logos/aws_smile-header-desktop-en-white_59x35.png', stream=True) - img = Image.open(response.raw) print(response.status_code) - print(img.size) return response.status_code diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/requirements.txt b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/requirements.txt index c636db83b8c9e..4fcd85719fe3a 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/requirements.txt +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/requirements.txt @@ -5,5 +5,3 @@ idna==2.10 urllib3==1.26.7 # Requests used by this lambda requests==2.26.0 -# Pillow 6.x so that python 2.7 and 3.x can both use this fixture -Pillow==8.4.0 diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index f6c95dfe0d68f..9c9182b174550 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -480,10 +480,48 @@ fn.addEventSource(new eventsources.S3EventSource(bucket, { See the documentation for the __@aws-cdk/aws-lambda-event-sources__ module for more details. +## Imported Lambdas + +When referencing an imported lambda in the CDK, use `fromFunctionArn()` for most use cases: + +```ts +const fn = lambda.Function.fromFunctionArn( + this, + 'Function', + 'arn:aws:lambda:us-east-1:123456789012:function:MyFn', +); +``` + +The `fromFunctionAttributes()` API is available for more specific use cases: + +```ts +const fn = lambda.Function.fromFunctionAttributes(this, 'Function', { + functionArn: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn', + // The following are optional properties for specific use cases and should be used with caution: + + // Use Case: imported function is in the same account as the stack. This tells the CDK that it + // can modify the function's permissions. + sameEnvironment: true, + + // Use Case: imported function is in a different account and user commits to ensuring that the + // imported function has the correct permissions outside the CDK. + skipPermissions: true, +}); +``` + +If `fromFunctionArn()` causes an error related to having to provide an account and/or region in a different construct, +and the lambda is in the same account and region as the stack you're importing it into, +you can use `Function.fromFunctionName()` instead: + +```ts +const fn = lambda.Function.fromFunctionName(this, 'Function', 'MyFn'); +``` + ## Lambda with DLQ A dead-letter queue can be automatically created for a Lambda function by -setting the `deadLetterQueueEnabled: true` configuration. +setting the `deadLetterQueueEnabled: true` configuration. In such case CDK creates +a `sqs.Queue` as `deadLetterQueue`. ```ts const fn = new lambda.Function(this, 'MyFunction', { @@ -508,6 +546,20 @@ const fn = new lambda.Function(this, 'MyFunction', { }); ``` +You can also use a `sns.Topic` instead of an `sqs.Queue` as dead-letter queue: + +```ts +import * as sns from '@aws-cdk/aws-sns'; + +const dlt = new sns.Topic(this, 'DLQ'); +const fn = new lambda.Function(this, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.fromInline('// your code here'), + deadLetterTopic: dlt, +}); +``` + See [the AWS documentation](https://docs.aws.amazon.com/lambda/latest/dg/dlq.html) to learn more about AWS Lambdas and DLQs. diff --git a/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts b/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts index 94059945b0b03..172d681d1fef9 100644 --- a/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts +++ b/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts @@ -35,6 +35,12 @@ export class SourceAccessConfigurationType { */ public static readonly SASL_SCRAM_512_AUTH = new SourceAccessConfigurationType('SASL_SCRAM_512_AUTH'); + /** + * The Secrets Manager ARN of your secret key containing the certificate chain (X.509 PEM), private key (PKCS#8 PEM), + * and private key password (optional) used for mutual TLS authentication of your MSK/Apache Kafka brokers. + */ + public static readonly CLIENT_CERTIFICATE_TLS_AUTH = new SourceAccessConfigurationType('CLIENT_CERTIFICATE_TLS_AUTH'); + /** A custom source access configuration property */ public static of(name: string): SourceAccessConfigurationType { return new SourceAccessConfigurationType(name); diff --git a/packages/@aws-cdk/aws-lambda/lib/function-base.ts b/packages/@aws-cdk/aws-lambda/lib/function-base.ts index 1256383c471bd..b259476efad9d 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function-base.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function-base.ts @@ -180,6 +180,20 @@ export interface FunctionAttributes { */ readonly sameEnvironment?: boolean; + /** + * Setting this property informs the CDK that the imported function ALREADY HAS the necessary permissions + * for what you are trying to do. When not configured, the CDK attempts to auto-determine whether or not + * additional permissions are necessary on the function when grant APIs are used. If the CDK tried to add + * permissions on an imported lambda, it will fail. + * + * Set this property *ONLY IF* you are committing to manage the imported function's permissions outside of + * CDK. You are acknowledging that your CDK code alone will have insufficient permissions to access the + * imported function. + * + * @default false + */ + readonly skipPermissions?: boolean; + /** * The architecture of this Lambda Function (this is an optional attribute and defaults to X86_64). * @default - Architecture.X86_64 @@ -228,6 +242,15 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC */ protected abstract readonly canCreatePermissions: boolean; + /** + * Whether the user decides to skip adding permissions. + * The only use case is for cross-account, imported lambdas + * where the user commits to modifying the permisssions + * on the imported lambda outside CDK. + * @internal + */ + protected readonly _skipPermissions?: boolean; + /** * Actual connections object for this Lambda * @@ -342,9 +365,10 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC }); const permissionNode = this._functionNode().tryFindChild(identifier); - if (!permissionNode) { - throw new Error('Cannot modify permission to lambda function. Function is either imported or $LATEST version. ' - + 'If the function is imported from the same account use `fromFunctionAttributes()` API with the `sameEnvironment` flag.'); + if (!permissionNode && !this._skipPermissions) { + throw new Error('Cannot modify permission to lambda function. Function is either imported or $LATEST version.\n' + + 'If the function is imported from the same account use `fromFunctionAttributes()` API with the `sameEnvironment` flag.\n' + + 'If the function is imported from a different account and already has the correct permissions use `fromFunctionAttributes()` API with the `skipPermissions` flag.'); } return { statementAdded: true, policyDependable: permissionNode }; }, diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index 2b47aa8d7bca3..5bff57e1d43bc 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -4,8 +4,9 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as logs from '@aws-cdk/aws-logs'; +import * as sns from '@aws-cdk/aws-sns'; import * as sqs from '@aws-cdk/aws-sqs'; -import { Annotations, ArnFormat, CfnResource, Duration, Fn, Lazy, Names, Stack } from '@aws-cdk/core'; +import { Annotations, ArnFormat, CfnResource, Duration, Fn, Lazy, Names, Stack, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Architecture } from './architecture'; import { Code, CodeConfig } from './code'; @@ -188,11 +189,21 @@ export interface FunctionOptions extends EventInvokeConfigOptions { /** * The SQS queue to use if DLQ is enabled. + * If SNS topic is desired, specify `deadLetterTopic` property instead. * * @default - SQS queue with 14 day retention period if `deadLetterQueueEnabled` is `true` */ readonly deadLetterQueue?: sqs.IQueue; + /** + * The SNS topic to use as a DLQ. + * Note that if `deadLetterQueueEnabled` is set to `true`, an SQS queue will be created + * rather than an SNS topic. Using an SNS topic as a DLQ requires this property to be set explicitly. + * + * @default - no SNS topic + */ + readonly deadLetterTopic?: sns.ITopic; + /** * Enable AWS X-Ray Tracing for Lambda Function. * @@ -424,6 +435,20 @@ export class Function extends FunctionBase { this._VER_PROPS[propertyName] = locked; } + /** + * Import a lambda function into the CDK using its name + */ + public static fromFunctionName(scope: Construct, id: string, functionName: string): IFunction { + return Function.fromFunctionAttributes(scope, id, { + functionArn: Stack.of(scope).formatArn({ + service: 'lambda', + resource: 'function', + resourceName: functionName, + arnFormat: ArnFormat.COLON_RESOURCE_NAME, + }), + }); + } + /** * Import a lambda function into the CDK using its ARN */ @@ -453,6 +478,7 @@ export class Function extends FunctionBase { public readonly architecture = attrs.architecture ?? Architecture.X86_64; protected readonly canCreatePermissions = attrs.sameEnvironment ?? this._isStackAccount(); + protected readonly _skipPermissions = attrs.skipPermissions ?? false; constructor(s: Construct, i: string) { super(s, i, { @@ -572,10 +598,15 @@ export class Function extends FunctionBase { public readonly grantPrincipal: iam.IPrincipal; /** - * The DLQ associated with this Lambda Function (this is an optional attribute). + * The DLQ (as queue) associated with this Lambda Function (this is an optional attribute). */ public readonly deadLetterQueue?: sqs.IQueue; + /** + * The DLQ (as topic) associated with this Lambda Function (this is an optional attribute). + */ + public readonly deadLetterTopic?: sns.ITopic; + /** * The architecture of this Lambda Function (this is an optional attribute and defaults to X86_64). */ @@ -610,6 +641,15 @@ export class Function extends FunctionBase { physicalName: props.functionName, }); + if (props.functionName && !Token.isUnresolved(props.functionName)) { + if (props.functionName.length > 64) { + throw new Error(`Function name can not be longer than 64 characters but has ${props.functionName.length} characters.`); + } + if (!/^[a-zA-Z0-9-_]+$/.test(props.functionName)) { + throw new Error(`Function name ${props.functionName} can contain only letters, numbers, hyphens, or underscores with no spaces.`); + } + } + const managedPolicies = new Array(); // the arn is in the form of - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole @@ -672,7 +712,15 @@ export class Function extends FunctionBase { this.addEnvironment(key, value); } - this.deadLetterQueue = this.buildDeadLetterQueue(props); + // DLQ can be either sns.ITopic or sqs.IQueue + const dlqTopicOrQueue = this.buildDeadLetterQueue(props); + if (dlqTopicOrQueue !== undefined) { + if (this.isQueue(dlqTopicOrQueue)) { + this.deadLetterQueue = dlqTopicOrQueue; + } else { + this.deadLetterTopic = dlqTopicOrQueue; + } + } let fileSystemConfigs: CfnFunction.FileSystemConfigProperty[] | undefined = undefined; if (props.filesystem) { @@ -711,7 +759,7 @@ export class Function extends FunctionBase { environment: Lazy.uncachedAny({ produce: () => this.renderEnvironment() }), memorySize: props.memorySize, vpcConfig: this.configureVpc(props), - deadLetterConfig: this.buildDeadLetterConfig(this.deadLetterQueue), + deadLetterConfig: this.buildDeadLetterConfig(dlqTopicOrQueue), tracingConfig: this.buildTracingConfig(props), reservedConcurrentExecutions: props.reservedConcurrentExecutions, imageConfig: undefinedIfNoKeys({ @@ -1030,31 +1078,45 @@ Environment variables can be marked for removal when used in Lambda@Edge by sett }; } - private buildDeadLetterQueue(props: FunctionProps) { + private isQueue(deadLetterQueue: sqs.IQueue | sns.ITopic): deadLetterQueue is sqs.IQueue { + return (deadLetterQueue).queueArn !== undefined; + } + + private buildDeadLetterQueue(props: FunctionProps): sqs.IQueue | sns.ITopic | undefined { + if (!props.deadLetterQueue && !props.deadLetterQueueEnabled && !props.deadLetterTopic) { + return undefined; + } if (props.deadLetterQueue && props.deadLetterQueueEnabled === false) { throw Error('deadLetterQueue defined but deadLetterQueueEnabled explicitly set to false'); } - - if (!props.deadLetterQueue && !props.deadLetterQueueEnabled) { - return undefined; + if (props.deadLetterTopic && (props.deadLetterQueue || props.deadLetterQueueEnabled !== undefined)) { + throw new Error('deadLetterQueue and deadLetterTopic cannot be specified together at the same time'); } - const deadLetterQueue = props.deadLetterQueue || new sqs.Queue(this, 'DeadLetterQueue', { - retentionPeriod: Duration.days(14), - }); - - this.addToRolePolicy(new iam.PolicyStatement({ - actions: ['sqs:SendMessage'], - resources: [deadLetterQueue.queueArn], - })); + let deadLetterQueue: sqs.IQueue | sns.ITopic; + if (props.deadLetterTopic) { + deadLetterQueue = props.deadLetterTopic; + this.addToRolePolicy(new iam.PolicyStatement({ + actions: ['sns:Publish'], + resources: [deadLetterQueue.topicArn], + })); + } else { + deadLetterQueue = props.deadLetterQueue || new sqs.Queue(this, 'DeadLetterQueue', { + retentionPeriod: Duration.days(14), + }); + this.addToRolePolicy(new iam.PolicyStatement({ + actions: ['sqs:SendMessage'], + resources: [deadLetterQueue.queueArn], + })); + } return deadLetterQueue; } - private buildDeadLetterConfig(deadLetterQueue?: sqs.IQueue) { + private buildDeadLetterConfig(deadLetterQueue?: sqs.IQueue | sns.ITopic) { if (deadLetterQueue) { return { - targetArn: deadLetterQueue.queueArn, + targetArn: this.isQueue(deadLetterQueue) ? deadLetterQueue.queueArn : deadLetterQueue.topicArn, }; } else { return undefined; diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index b5c73d139aa42..1b9f58b6fcf21 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -83,16 +83,16 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/cfnspec": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/aws-lambda": "^8.10.89", - "@types/jest": "^27.0.3", + "@types/aws-lambda": "^8.10.92", + "@types/jest": "^27.4.1", "@types/lodash": "^4.14.178", - "jest": "^27.4.5", + "jest": "^27.5.1", "lodash": "^4.17.21" }, "dependencies": { @@ -110,6 +110,7 @@ "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/aws-signer": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cx-api": "0.0.0", @@ -132,6 +133,7 @@ "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/aws-signer": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cx-api": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda/test/alias.test.ts b/packages/@aws-cdk/aws-lambda/test/alias.test.ts index a470ace2a366a..2a37ffb285060 100644 --- a/packages/@aws-cdk/aws-lambda/test/alias.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/alias.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { arrayWith, objectLike } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; @@ -22,11 +21,11 @@ describe('alias', () => { version, }); - expect(stack).toHaveResource('AWS::Lambda::Version', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Version', { FunctionName: { Ref: 'MyLambdaCCE802FB' }, }); - expect(stack).toHaveResource('AWS::Lambda::Alias', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Alias', { FunctionName: { Ref: 'MyLambdaCCE802FB' }, FunctionVersion: stack.resolve(version.version), Name: 'prod', @@ -46,12 +45,12 @@ describe('alias', () => { version: fn.latestVersion, }); - expect(stack).toHaveResource('AWS::Lambda::Alias', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Alias', { FunctionName: { Ref: 'MyLambdaCCE802FB' }, FunctionVersion: '$LATEST', Name: 'latest', }); - expect(stack).not.toHaveResource('AWS::Lambda::Version'); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Version', 0); }); testDeprecated('can use newVersion to create a new Version', () => { @@ -69,11 +68,11 @@ describe('alias', () => { version, }); - expect(stack).toHaveResourceLike('AWS::Lambda::Version', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Version', { FunctionName: { Ref: 'MyLambdaCCE802FB' }, }); - expect(stack).toHaveResourceLike('AWS::Lambda::Alias', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Alias', { FunctionName: { Ref: 'MyLambdaCCE802FB' }, Name: 'prod', }); @@ -97,7 +96,7 @@ describe('alias', () => { additionalVersions: [{ version: version2, weight: 0.1 }], }); - expect(stack).toHaveResource('AWS::Lambda::Alias', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Alias', { FunctionVersion: stack.resolve(version1.version), RoutingConfig: { AdditionalVersionWeights: [ @@ -127,13 +126,13 @@ describe('alias', () => { provisionedConcurrentExecutions: pce, }); - expect(stack).toHaveResource('AWS::Lambda::Version', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Version', { ProvisionedConcurrencyConfig: { ProvisionedConcurrentExecutions: 5, }, }); - expect(stack).toHaveResource('AWS::Lambda::Alias', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Alias', { FunctionVersion: stack.resolve(version.version), Name: 'prod', ProvisionedConcurrencyConfig: { @@ -194,7 +193,7 @@ describe('alias', () => { }); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { Dimensions: [{ Name: 'FunctionName', Value: { @@ -325,7 +324,7 @@ describe('alias', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::EventInvokeConfig', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventInvokeConfig', { FunctionName: { Ref: 'fn5FF616E3', }, @@ -384,7 +383,7 @@ describe('alias', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::EventInvokeConfig', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventInvokeConfig', { FunctionName: 'function-name', Qualifier: 'alias-name', MaximumRetryAttempts: 1, @@ -409,22 +408,23 @@ describe('alias', () => { alias.addAutoScaling({ maxCapacity: 5 }); // THEN - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { MinCapacity: 1, MaxCapacity: 5, - ResourceId: objectLike({ - 'Fn::Join': arrayWith(arrayWith( + ResourceId: Match.objectLike({ + 'Fn::Join': Match.arrayWith([Match.arrayWith([ 'function:', - objectLike({ - 'Fn::Select': arrayWith( + Match.objectLike({ + 'Fn::Select': Match.arrayWith([ { - 'Fn::Split': arrayWith( - { Ref: 'Alias325C5727' }), + 'Fn::Split': Match.arrayWith([ + { Ref: 'Alias325C5727' }, + ]), }, - ), + ]), }), ':prod', - )), + ])]), }), }); }); @@ -448,26 +448,27 @@ describe('alias', () => { alias.addAutoScaling({ maxCapacity: 5 }); // THEN - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { MinCapacity: 1, MaxCapacity: 5, - ResourceId: objectLike({ - 'Fn::Join': arrayWith(arrayWith( + ResourceId: Match.objectLike({ + 'Fn::Join': Match.arrayWith([Match.arrayWith([ 'function:', - objectLike({ - 'Fn::Select': arrayWith( + Match.objectLike({ + 'Fn::Select': Match.arrayWith([ { - 'Fn::Split': arrayWith( - { Ref: 'Alias325C5727' }), + 'Fn::Split': Match.arrayWith([ + { Ref: 'Alias325C5727' }, + ]), }, - ), + ]), }), ':prod', - )), + ])]), }), }); - expect(stack).toHaveResourceLike('AWS::Lambda::Alias', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Alias', { ProvisionedConcurrencyConfig: { ProvisionedConcurrentExecutions: 10, }, @@ -495,7 +496,7 @@ describe('alias', () => { target.scaleOnUtilization({ utilizationTarget: Lazy.number({ produce: () => 0.95 }) }); // THEN: no exception - expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'LambdaProvisionedConcurrencyUtilization' }, @@ -568,7 +569,7 @@ describe('alias', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ApplicationAutoScaling::ScalableTarget', { + Template.fromStack(stack).hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', { ScheduledActions: [ { ScalableTargetAction: { MaxCapacity: 10 }, diff --git a/packages/@aws-cdk/aws-lambda/test/code-signing-config.test.ts b/packages/@aws-cdk/aws-lambda/test/code-signing-config.test.ts index 7b38e2cb7e178..68674b6f4bf6a 100644 --- a/packages/@aws-cdk/aws-lambda/test/code-signing-config.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/code-signing-config.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as signer from '@aws-cdk/aws-signer'; import * as cdk from '@aws-cdk/core'; import * as lambda from '../lib'; @@ -18,7 +18,7 @@ describe('code signing config', () => { signingProfiles: [signingProfile], }); - expect(stack).toHaveResource('AWS::Lambda::CodeSigningConfig', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::CodeSigningConfig', { AllowedPublishers: { SigningProfileVersionArns: [{ 'Fn::GetAtt': [ @@ -41,7 +41,7 @@ describe('code signing config', () => { signingProfiles: [signingProfile1, signingProfile2, signingProfile3], }); - expect(stack).toHaveResource('AWS::Lambda::CodeSigningConfig', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::CodeSigningConfig', { AllowedPublishers: { SigningProfileVersionArns: [ { @@ -76,7 +76,7 @@ describe('code signing config', () => { description: 'test description', }); - expect(stack).toHaveResource('AWS::Lambda::CodeSigningConfig', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::CodeSigningConfig', { CodeSigningPolicies: { UntrustedArtifactOnDeployment: 'Enforce', }, @@ -91,7 +91,7 @@ describe('code signing config', () => { expect(codeSigningConfig.codeSigningConfigArn).toBe(codeSigningConfigArn); expect(codeSigningConfig.codeSigningConfigId).toBe(codeSigningConfigId); - expect(stack).toCountResources('AWS::Lambda::CodeSigningConfig', 0); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::CodeSigningConfig', 0); }); test('fail import with malformed code signing config arn', () => { diff --git a/packages/@aws-cdk/aws-lambda/test/code.test.ts b/packages/@aws-cdk/aws-lambda/test/code.test.ts index 25e937b8e6415..76e771596d735 100644 --- a/packages/@aws-cdk/aws-lambda/test/code.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/code.test.ts @@ -1,6 +1,5 @@ -import '@aws-cdk/assert-internal/jest'; import * as path from 'path'; -import { ABSENT, ResourcePart } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ecr from '@aws-cdk/aws-ecr'; import { testFutureBehavior } from '@aws-cdk/cdk-build-tools'; import * as cdk from '@aws-cdk/core'; @@ -20,6 +19,7 @@ describe('code', () => { .toThrow(/Lambda source is too large, must be <= 4096 but is 4097/); }); }); + describe('lambda.Code.fromAsset', () => { test('fails if a non-zip asset is used', () => { // GIVEN @@ -71,13 +71,13 @@ describe('code', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { Metadata: { [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.9678c34eca93259d11f2d714177347afd66c50116e1e08996eff893d3ca81232', [cxapi.ASSET_RESOURCE_METADATA_IS_BUNDLED_KEY]: false, [cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'Code', }, - }, ResourcePart.CompleteDefinition); + }); }); test('fails if asset is bound with a second stack', () => { @@ -111,7 +111,7 @@ describe('code', () => { handler: 'index.handler', }); - expect(stack).toHaveResourceLike('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Code: { S3Bucket: { Ref: 'FunctionLambdaSourceBucketNameParameter9E9E108F', @@ -157,7 +157,7 @@ describe('code', () => { handler: 'index.handler', }); - expect(stack).toHaveResourceLike('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Code: { S3Bucket: { Ref: 'BucketNameParam', @@ -207,11 +207,11 @@ describe('code', () => { }); // then - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Code: { ImageUri: stack.resolve(repo.repositoryUriForTag('latest')), }, - ImageConfig: ABSENT, + ImageConfig: Match.absent(), }); }); @@ -233,7 +233,7 @@ describe('code', () => { }); // then - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Code: { ImageUri: stack.resolve(repo.repositoryUriForTag('mytag')), }, @@ -258,7 +258,7 @@ describe('code', () => { }); // then - expect(stack).toHaveResourceLike('AWS::ECR::Repository', { + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', { RepositoryPolicyText: { Statement: [ { @@ -292,7 +292,7 @@ describe('code', () => { }); // then - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Code: { ImageUri: { 'Fn::Join': ['', [ @@ -305,7 +305,7 @@ describe('code', () => { ]], }, }, - ImageConfig: ABSENT, + ImageConfig: Match.absent(), }); }); @@ -325,7 +325,7 @@ describe('code', () => { }); // then - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { ImageConfig: { Command: ['cmd', 'param1'], EntryPoint: ['entrypoint', 'param2'], @@ -382,7 +382,7 @@ describe('code', () => { }); // then - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { Metadata: { [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.650a009a909c30e767a843a84ff7812616447251d245e0ab65d9bfb37f413e32', [cxapi.ASSET_RESOURCE_METADATA_DOCKERFILE_PATH_KEY]: dockerfilePath, @@ -390,7 +390,7 @@ describe('code', () => { [cxapi.ASSET_RESOURCE_METADATA_DOCKER_BUILD_TARGET_KEY]: dockerBuildTarget, [cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'Code.ImageUri', }, - }, ResourcePart.CompleteDefinition); + }); }); test('adds code asset metadata with default dockerfile path', () => { @@ -406,13 +406,13 @@ describe('code', () => { }); // then - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { Metadata: { [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.a3cc4528c34874616814d9b3436ff0e5d01514c1d563ed8899657ca00982f308', [cxapi.ASSET_RESOURCE_METADATA_DOCKERFILE_PATH_KEY]: 'Dockerfile', [cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'Code.ImageUri', }, - }, ResourcePart.CompleteDefinition); + }); }); test('fails if asset is bound with a second stack', () => { @@ -470,13 +470,13 @@ describe('code', () => { }); // then - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { Metadata: { [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.fbafdbb9ae8d1bae0def415b791a93c486d18ebc63270c748abecc3ac0ab9533', [cxapi.ASSET_RESOURCE_METADATA_IS_BUNDLED_KEY]: false, [cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'Code', }, - }, ResourcePart.CompleteDefinition); + }); expect(fromBuildMock).toHaveBeenCalledWith(path.join(__dirname, 'docker-build-lambda'), {}); expect(cpMock).toHaveBeenCalledWith('/asset/.', undefined); diff --git a/packages/@aws-cdk/aws-lambda/test/event-source-mapping.test.ts b/packages/@aws-cdk/aws-lambda/test/event-source-mapping.test.ts index c974a7c82b4c4..793e9abb4c42d 100644 --- a/packages/@aws-cdk/aws-lambda/test/event-source-mapping.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/event-source-mapping.test.ts @@ -1,5 +1,4 @@ -import { ABSENT } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import { Code, EventSourceMapping, Function, Runtime } from '../lib'; @@ -165,7 +164,7 @@ describe('event source mapping', () => { kafkaTopic: topicNameParam.valueAsString, }); - expect(stack).toHaveResourceLike('AWS::Lambda::EventSourceMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { Topics: [{ Ref: 'TopicNameParam', }], @@ -234,7 +233,7 @@ describe('event source mapping', () => { kafkaTopic: topicNameParam.valueAsString, }); - expect(stack).toHaveResourceLike('AWS::Lambda::EventSourceMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { EventSourceArn: eventSourceArn, }); }); @@ -258,7 +257,7 @@ describe('event source mapping', () => { kafkaTopic: topicNameParam.valueAsString, }); - expect(stack).toHaveResourceLike('AWS::Lambda::EventSourceMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { SelfManagedEventSource: { Endpoints: { KafkaBootstrapServers: kafkaBootstrapServers } }, }); }); @@ -309,7 +308,7 @@ describe('event source mapping', () => { reportBatchItemFailures: true, }); - expect(stack).toHaveResourceLike('AWS::Lambda::EventSourceMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { FunctionResponseTypes: ['ReportBatchItemFailures'], }); }); @@ -328,8 +327,8 @@ describe('event source mapping', () => { eventSourceArn: '', }); - expect(stack).toHaveResourceLike('AWS::Lambda::EventSourceMapping', { - FunctionResponseTypes: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { + FunctionResponseTypes: Match.absent(), }); }); @@ -348,8 +347,8 @@ describe('event source mapping', () => { reportBatchItemFailures: false, }); - expect(stack).toHaveResourceLike('AWS::Lambda::EventSourceMapping', { - FunctionResponseTypes: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { + FunctionResponseTypes: Match.absent(), }); }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/function-hash.test.ts b/packages/@aws-cdk/aws-lambda/test/function-hash.test.ts index dbadd01b0975b..de07180924ef5 100644 --- a/packages/@aws-cdk/aws-lambda/test/function-hash.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/function-hash.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import * as path from 'path'; import { resourceSpecification } from '@aws-cdk/cfnspec'; import { App, CfnOutput, CfnResource, Stack } from '@aws-cdk/core'; diff --git a/packages/@aws-cdk/aws-lambda/test/function.test.ts b/packages/@aws-cdk/aws-lambda/test/function.test.ts index 04a8ab862b5b7..9c01956493d8b 100644 --- a/packages/@aws-cdk/aws-lambda/test/function.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/function.test.ts @@ -1,6 +1,5 @@ -import '@aws-cdk/assert-internal/jest'; import * as path from 'path'; -import { ABSENT, ResourcePart, SynthUtils } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import { ProfilingGroup } from '@aws-cdk/aws-codeguruprofiler'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as efs from '@aws-cdk/aws-efs'; @@ -9,6 +8,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 signer from '@aws-cdk/aws-signer'; +import * as sns from '@aws-cdk/aws-sns'; import * as sqs from '@aws-cdk/aws-sqs'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import * as cdk from '@aws-cdk/core'; @@ -26,7 +26,7 @@ describe('function', () => { runtime: lambda.Runtime.NODEJS_10_X, }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: @@ -41,7 +41,7 @@ describe('function', () => { [{ 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole']] }], }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { Properties: { Code: { ZipFile: 'foo' }, @@ -50,7 +50,7 @@ describe('function', () => { Runtime: 'nodejs10.x', }, DependsOn: ['MyLambdaServiceRole4539ECB6'], - }, ResourcePart.CompleteDefinition); + }); }); test('adds policy permissions', () => { @@ -61,7 +61,7 @@ describe('function', () => { runtime: lambda.Runtime.NODEJS_10_X, initialPolicy: [new iam.PolicyStatement({ actions: ['*'], resources: ['*'] })], }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: @@ -76,7 +76,7 @@ describe('function', () => { // eslint-disable-next-line max-len [{ 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole']] }], }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -95,7 +95,7 @@ describe('function', () => { ], }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { Properties: { Code: { ZipFile: 'foo' }, Handler: 'index.handler', @@ -103,7 +103,7 @@ describe('function', () => { Runtime: 'nodejs10.x', }, DependsOn: ['MyLambdaServiceRoleDefaultPolicy5BBC6F68', 'MyLambdaServiceRole4539ECB6'], - }, ResourcePart.CompleteDefinition); + }); }); test('fails if inline code is used for an invalid runtime', () => { @@ -127,7 +127,7 @@ describe('function', () => { sourceArn: 'arn:aws:s3:::my_bucket', }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -145,7 +145,7 @@ describe('function', () => { [{ 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole']] }], }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { Properties: { Code: { ZipFile: 'foo', @@ -162,9 +162,9 @@ describe('function', () => { DependsOn: [ 'MyLambdaServiceRole4539ECB6', ], - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:*', FunctionName: { 'Fn::GetAtt': [ @@ -209,7 +209,7 @@ describe('function', () => { fn.addPermission('S1', { principal: principal }); - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', FunctionName: { 'Fn::GetAtt': [ @@ -272,7 +272,7 @@ describe('function', () => { fn.addToRolePolicy(new iam.PolicyStatement({ actions: ['explicit:explicit'], resources: ['*'] })); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -297,6 +297,42 @@ describe('function', () => { expect(imported.functionName).toEqual('ProcessKinesisRecords'); }); + test('Function.fromFunctionName', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const imported = lambda.Function.fromFunctionName(stack, 'Imported', 'my-function'); + + // THEN + expect(stack.resolve(imported.functionArn)).toStrictEqual({ + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':lambda:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':function:my-function', + ]], + }); + expect(stack.resolve(imported.functionName)).toStrictEqual({ + 'Fn::Select': [6, { + 'Fn::Split': [':', { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':lambda:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':function:my-function', + ]], + }], + }], + }); + }); + describe('Function.fromFunctionAttributes()', () => { let stack: cdk.Stack; @@ -343,7 +379,7 @@ describe('function', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Permission'); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 1); }); test('imported Function w/ unresolved account', () => { @@ -360,7 +396,7 @@ describe('function', () => { }); // THEN - expect(stack).not.toHaveResource('AWS::Lambda::Permission'); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 0); }); test('imported Function w/ unresolved account & allowPermissions set', () => { @@ -378,7 +414,7 @@ describe('function', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Permission'); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 1); }); test('imported Function w/different account', () => { @@ -397,7 +433,7 @@ describe('function', () => { }); // THEN - expect(stack).not.toHaveResource('AWS::Lambda::Permission'); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 0); }); }); @@ -411,7 +447,7 @@ describe('function', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Code: { S3Bucket: { Ref: 'AssetParameters9678c34eca93259d11f2d714177347afd66c50116e1e08996eff893d3ca81232S3Bucket1354C645', @@ -445,7 +481,7 @@ describe('function', () => { deadLetterQueueEnabled: true, }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -473,7 +509,7 @@ describe('function', () => { }, ], }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -496,7 +532,7 @@ describe('function', () => { }, ], }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { Properties: { Code: { ZipFile: 'foo', @@ -523,7 +559,7 @@ describe('function', () => { 'MyLambdaServiceRoleDefaultPolicy5BBC6F68', 'MyLambdaServiceRole4539ECB6', ], - }, ResourcePart.CompleteDefinition); + }); }); test('default function with SQS DLQ when client sets deadLetterQueueEnabled to true and functionName not defined by client', () => { @@ -536,11 +572,11 @@ describe('function', () => { deadLetterQueueEnabled: true, }); - expect(stack).toHaveResource('AWS::SQS::Queue', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::Queue', { MessageRetentionPeriod: 1209600, }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { DeadLetterConfig: { TargetArn: { 'Fn::GetAtt': [ @@ -562,7 +598,7 @@ describe('function', () => { deadLetterQueueEnabled: false, }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Code: { ZipFile: 'foo', }, @@ -592,7 +628,7 @@ describe('function', () => { deadLetterQueue: dlQueue, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -610,7 +646,7 @@ describe('function', () => { }, }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { DeadLetterConfig: { TargetArn: { 'Fn::GetAtt': [ @@ -638,7 +674,7 @@ describe('function', () => { deadLetterQueue: dlQueue, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -656,7 +692,7 @@ describe('function', () => { }, }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { DeadLetterConfig: { TargetArn: { 'Fn::GetAtt': [ @@ -685,6 +721,84 @@ describe('function', () => { })).toThrow(/deadLetterQueue defined but deadLetterQueueEnabled explicitly set to false/); }); + test('default function with SNS DLQ when client provides Topic to be used as DLQ', () => { + const stack = new cdk.Stack(); + + const dlTopic = new sns.Topic(stack, 'DeadLetterTopic'); + + new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, + deadLetterTopic: dlTopic, + }); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: Match.arrayWith([ + { + Action: 'sns:Publish', + Effect: 'Allow', + Resource: { + Ref: 'DeadLetterTopicC237650B', + }, + }, + ]), + }, + }); + template.hasResourceProperties('AWS::Lambda::Function', { + DeadLetterConfig: { + TargetArn: { + Ref: 'DeadLetterTopicC237650B', + }, + }, + }); + }); + + test('error when default function with SNS DLQ when client provides Topic to be used as DLQ and deadLetterQueueEnabled set to false', () => { + const stack = new cdk.Stack(); + + const dlTopic = new sns.Topic(stack, 'DeadLetterTopic'); + + expect(() => new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, + deadLetterQueueEnabled: false, + deadLetterTopic: dlTopic, + })).toThrow(/deadLetterQueue and deadLetterTopic cannot be specified together at the same time/); + }); + + test('error when default function with SNS DLQ when client provides Topic to be used as DLQ and deadLetterQueueEnabled set to true', () => { + const stack = new cdk.Stack(); + + const dlTopic = new sns.Topic(stack, 'DeadLetterTopic'); + + expect(() => new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, + deadLetterQueueEnabled: true, + deadLetterTopic: dlTopic, + })).toThrow(/deadLetterQueue and deadLetterTopic cannot be specified together at the same time/); + }); + + test('error when both topic and queue are presented as DLQ', () => { + const stack = new cdk.Stack(); + + const dlQueue = new sqs.Queue(stack, 'DLQ'); + const dlTopic = new sns.Topic(stack, 'DeadLetterTopic'); + + expect(() => new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, + deadLetterQueue: dlQueue, + deadLetterTopic: dlTopic, + })).toThrow(/deadLetterQueue and deadLetterTopic cannot be specified together at the same time/); + }); + test('default function with Active tracing', () => { const stack = new cdk.Stack(); @@ -695,7 +809,7 @@ describe('function', () => { tracing: lambda.Tracing.ACTIVE, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -717,7 +831,7 @@ describe('function', () => { ], }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { Properties: { Code: { ZipFile: 'foo', @@ -738,7 +852,7 @@ describe('function', () => { 'MyLambdaServiceRoleDefaultPolicy5BBC6F68', 'MyLambdaServiceRole4539ECB6', ], - }, ResourcePart.CompleteDefinition); + }); }); test('default function with PassThrough tracing', () => { @@ -751,7 +865,7 @@ describe('function', () => { tracing: lambda.Tracing.PASS_THROUGH, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -773,7 +887,7 @@ describe('function', () => { ], }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { Properties: { Code: { ZipFile: 'foo', @@ -794,7 +908,7 @@ describe('function', () => { 'MyLambdaServiceRoleDefaultPolicy5BBC6F68', 'MyLambdaServiceRole4539ECB6', ], - }, ResourcePart.CompleteDefinition); + }); }); test('default function with Disabled tracing', () => { @@ -807,29 +921,9 @@ describe('function', () => { tracing: lambda.Tracing.DISABLED, }); - expect(stack).not.toHaveResource('AWS::IAM::Policy', { - PolicyDocument: { - Statement: [ - { - Action: [ - 'xray:PutTraceSegments', - 'xray:PutTelemetryRecords', - ], - Effect: 'Allow', - Resource: '*', - }, - ], - Version: '2012-10-17', - }, - PolicyName: 'MyLambdaServiceRoleDefaultPolicy5BBC6F68', - Roles: [ - { - Ref: 'MyLambdaServiceRole4539ECB6', - }, - ], - }); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 0); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { Properties: { Code: { ZipFile: 'foo', @@ -846,7 +940,7 @@ describe('function', () => { DependsOn: [ 'MyLambdaServiceRole4539ECB6', ], - }, ResourcePart.CompleteDefinition); + }); }); test('runtime and handler set to FROM_IMAGE are set to undefined in CloudFormation', () => { @@ -858,15 +952,14 @@ describe('function', () => { runtime: lambda.Runtime.FROM_IMAGE, }); - expect(stack).toHaveResource('AWS::Lambda::Function', { - Runtime: ABSENT, - Handler: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { + Runtime: Match.absent(), + Handler: Match.absent(), PackageType: 'Image', }); }); describe('grantInvoke', () => { - test('adds iam:InvokeFunction', () => { // GIVEN const stack = new cdk.Stack(); @@ -883,7 +976,7 @@ describe('function', () => { fn.grantInvoke(role); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -911,7 +1004,7 @@ describe('function', () => { fn.grantInvoke(service); // THEN - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', FunctionName: { 'Fn::GetAtt': [ @@ -937,7 +1030,7 @@ describe('function', () => { fn.grantInvoke(account); // THEN - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', FunctionName: { 'Fn::GetAtt': [ @@ -963,7 +1056,7 @@ describe('function', () => { fn.grantInvoke(account); // THEN - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', FunctionName: { 'Fn::GetAtt': [ @@ -990,7 +1083,7 @@ describe('function', () => { fn.grantInvoke(service); // THEN - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', FunctionName: { 'Fn::GetAtt': [ @@ -1017,7 +1110,7 @@ describe('function', () => { fn.grantInvoke(iam.Role.fromRoleArn(stack, 'ForeignRole', 'arn:aws:iam::123456789012:role/someRole')); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -1046,7 +1139,7 @@ describe('function', () => { fn.grantInvoke(iam.Role.fromRoleArn(stack, 'ForeignRole', 'arn:aws:iam::123456789012:role/someRole')); // THEN - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', FunctionName: { 'Fn::GetAtt': [ @@ -1069,7 +1162,7 @@ describe('function', () => { fn.grantInvoke(new iam.ServicePrincipal('elasticloadbalancing.amazonaws.com')); // THEN - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', FunctionName: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn', Principal: 'elasticloadbalancing.amazonaws.com', @@ -1097,7 +1190,7 @@ describe('function', () => { fn.grantInvoke(new iam.ServicePrincipal('elasticloadbalancing.amazonaws.com')); // THEN - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', FunctionName: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn', Principal: 'elasticloadbalancing.amazonaws.com', @@ -1112,8 +1205,25 @@ describe('function', () => { const fn = lambda.Function.fromFunctionArn(stack, 'Function', 'arn:aws:lambda:us-east-1:123456789012:function:MyFn'); // THEN - expect(() => { fn.grantInvoke(new iam.ServicePrincipal('elasticloadbalancing.amazonaws.com')); }) - .toThrow(/Cannot modify permission to lambda function/); + expect(() => { + fn.grantInvoke(new iam.ServicePrincipal('elasticloadbalancing.amazonaws.com')); + }).toThrow(/Cannot modify permission to lambda function/); + }); + + test('on an imported function (different account & w/ skipPermissions', () => { + // GIVEN + const stack = new cdk.Stack(undefined, undefined, { + env: { account: '111111111111' }, // Different account + }); + const fn = lambda.Function.fromFunctionAttributes(stack, 'Function', { + functionArn: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn', + skipPermissions: true, + }); + + // THEN + expect(() => { + fn.grantInvoke(new iam.ServicePrincipal('elasticloadbalancing.amazonaws.com')); + }).not.toThrow(); }); }); @@ -1209,7 +1319,7 @@ describe('function', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Environment: { Variables: { SOME: 'Variable', @@ -1233,7 +1343,7 @@ describe('function', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Environment: { Variables: { SOME: 'Variable', @@ -1252,7 +1362,7 @@ describe('function', () => { reservedConcurrentExecutions: 10, }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { ReservedConcurrentExecutions: 10, }); }); @@ -1304,7 +1414,7 @@ describe('function', () => { }); // THEN - expect(stack).toHaveResource('Custom::LogRetention', { + Template.fromStack(stack).hasResourceProperties('Custom::LogRetention', { LogGroupName: { 'Fn::Join': [ '', @@ -1335,7 +1445,7 @@ describe('function', () => { fn.connections.allowToAnyIpv4(ec2.Port.tcp(443)); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: 'sg-123456789', }); }); @@ -1360,7 +1470,7 @@ describe('function', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::EventInvokeConfig', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventInvokeConfig', { FunctionName: { Ref: 'fn5FF616E3', }, @@ -1405,7 +1515,7 @@ describe('function', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::EventInvokeConfig', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventInvokeConfig', { FunctionName: 'my-function', Qualifier: '$LATEST', MaximumRetryAttempts: 1, @@ -1427,7 +1537,7 @@ describe('function', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::EventInvokeConfig', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventInvokeConfig', { FunctionName: { Ref: 'fn5FF616E3', }, @@ -1455,8 +1565,8 @@ describe('function', () => { fn._checkEdgeCompatibility(); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { - Environment: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { + Environment: Match.absent(), }); }); @@ -1566,7 +1676,7 @@ describe('function', () => { expect(logGroup.logGroupArn).toBeDefined(); }); - test('dlq is returned when provided by user', () => { + test('dlq is returned when provided by user and is Queue', () => { const stack = new cdk.Stack(); const dlQueue = new sqs.Queue(stack, 'DeadLetterQueue', { @@ -1581,12 +1691,37 @@ describe('function', () => { deadLetterQueue: dlQueue, }); const deadLetterQueue = fn.deadLetterQueue; - expect(deadLetterQueue?.queueArn).toBeDefined(); - expect(deadLetterQueue?.queueName).toBeDefined(); - expect(deadLetterQueue?.queueUrl).toBeDefined(); + const deadLetterTopic = fn.deadLetterTopic; + + expect(deadLetterTopic).toBeUndefined(); + + expect(deadLetterQueue).toBeDefined(); + expect(deadLetterQueue).toBeInstanceOf(sqs.Queue); }); - test('dlq is returned when setup by cdk', () => { + test('dlq is returned when provided by user and is Topic', () => { + const stack = new cdk.Stack(); + + const dlTopic = new sns.Topic(stack, 'DeadLetterQueue', { + topicName: 'MyLambda_DLQ', + }); + + const fn = new lambda.Function(stack, 'fn', { + handler: 'foo', + runtime: lambda.Runtime.NODEJS_10_X, + code: lambda.Code.fromInline('foo'), + deadLetterTopic: dlTopic, + }); + const deadLetterQueue = fn.deadLetterQueue; + const deadLetterTopic = fn.deadLetterTopic; + + expect(deadLetterQueue).toBeUndefined(); + + expect(deadLetterTopic).toBeDefined(); + expect(deadLetterTopic).toBeInstanceOf(sns.Topic); + }); + + test('dlq is returned when setup by cdk and is Queue', () => { const stack = new cdk.Stack(); const fn = new lambda.Function(stack, 'fn', { handler: 'foo', @@ -1595,9 +1730,12 @@ describe('function', () => { deadLetterQueueEnabled: true, }); const deadLetterQueue = fn.deadLetterQueue; - expect(deadLetterQueue?.queueArn).toBeDefined(); - expect(deadLetterQueue?.queueName).toBeDefined(); - expect(deadLetterQueue?.queueUrl).toBeDefined(); + const deadLetterTopic = fn.deadLetterTopic; + + expect(deadLetterTopic).toBeUndefined(); + + expect(deadLetterQueue).toBeDefined(); + expect(deadLetterQueue).toBeInstanceOf(sqs.Queue); }); test('dlq is undefined when not setup', () => { @@ -1608,7 +1746,10 @@ describe('function', () => { code: lambda.Code.fromInline('foo'), }); const deadLetterQueue = fn.deadLetterQueue; + const deadLetterTopic = fn.deadLetterTopic; + expect(deadLetterQueue).toBeUndefined(); + expect(deadLetterTopic).toBeUndefined(); }); test('one and only one child LogRetention construct will be created', () => { @@ -1678,7 +1819,7 @@ describe('function', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Environment: { Variables: { SOME: 'Variable', @@ -1704,12 +1845,12 @@ describe('function', () => { profiling: true, }); - expect(stack).toHaveResource('AWS::CodeGuruProfiler::ProfilingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeGuruProfiler::ProfilingGroup', { ProfilingGroupName: 'MyLambdaProfilingGroupC5B6CCD8', ComputePlatform: 'AWSLambda', }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -1733,7 +1874,7 @@ describe('function', () => { ], }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Environment: { Variables: { AWS_CODEGURU_PROFILER_GROUP_ARN: { 'Fn::GetAtt': ['MyLambdaProfilingGroupEC6DE32F', 'Arn'] }, @@ -1753,7 +1894,7 @@ describe('function', () => { profilingGroup: new ProfilingGroup(stack, 'ProfilingGroup'), }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -1777,7 +1918,7 @@ describe('function', () => { ], }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Environment: { Variables: { AWS_CODEGURU_PROFILER_GROUP_ARN: { @@ -1806,9 +1947,9 @@ describe('function', () => { profilingGroup: new ProfilingGroup(stack, 'ProfilingGroup'), }); - expect(stack).not.toHaveResource('AWS::IAM::Policy'); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 0); - expect(stack).not.toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', Match.not({ Environment: { Variables: { AWS_CODEGURU_PROFILER_GROUP_ARN: { @@ -1823,7 +1964,7 @@ describe('function', () => { AWS_CODEGURU_PROFILER_ENABLED: 'TRUE', }, }, - }); + })); }); test('default function with profiling enabled and client provided env vars', () => { @@ -1936,8 +2077,8 @@ describe('function', () => { }); // THEN - const template1 = SynthUtils.synthesize(stack1).template; - const template2 = SynthUtils.synthesize(stack2).template; + const template1 = Template.fromStack(stack1).toJSON(); + const template2 = Template.fromStack(stack2).toJSON(); // these functions are different in their configuration but the original // logical ID of the version would be the same unless the logical ID @@ -1970,7 +2111,7 @@ describe('function', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { FileSystemConfigs: [ { Arn: { @@ -2053,7 +2194,7 @@ describe('function', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { DependsOn: [ 'EfsEfsMountTarget195B2DD2E', 'EfsEfsMountTarget2315C927F', @@ -2062,7 +2203,7 @@ describe('function', () => { 'MyFunctionServiceRoleDefaultPolicyB705ABD4', 'MyFunctionServiceRole3C357FF2', ], - }, ResourcePart.CompleteDefinition); + }); }); }); @@ -2158,11 +2299,11 @@ describe('function', () => { runtime: lambda.Runtime.FROM_IMAGE, }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Code: { ImageUri: 'ecr image uri', }, - ImageConfig: ABSENT, + ImageConfig: Match.absent(), }); }); @@ -2182,7 +2323,7 @@ describe('function', () => { runtime: lambda.Runtime.FROM_IMAGE, }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { ImageConfig: { Command: ['cmd', 'param1'], EntryPoint: ['entrypoint', 'param2'], @@ -2211,7 +2352,7 @@ describe('function', () => { codeSigningConfig, }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { CodeSigningConfigArn: { 'Fn::GetAtt': [ 'CodeSigningConfigD8D41C10', @@ -2247,7 +2388,7 @@ describe('function', () => { architectures: [lambda.Architecture.ARM_64], }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Architectures: ['arm64'], }); }); @@ -2262,7 +2403,7 @@ describe('function', () => { architecture: lambda.Architecture.ARM_64, }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Architectures: ['arm64'], }); }); @@ -2299,6 +2440,45 @@ describe('function', () => { }); expect(fn.architecture?.name).toEqual('arm64'); }); + + test('Error when function name is longer than 64 chars', () => { + const stack = new cdk.Stack(); + expect(() => new lambda.Function(stack, 'MyFunction', { + code: lambda.Code.fromInline('foo'), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + functionName: 'a'.repeat(65), + })).toThrow(/Function name can not be longer than 64 characters/); + }); + + test('Error when function name contains invalid characters', () => { + const stack = new cdk.Stack(); + [' ', '\n', '\r', '[', ']', '<', '>', '$'].forEach(invalidChar => { + expect(() => { + new lambda.Function(stack, `foo${invalidChar}`, { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_14_X, + functionName: `foo${invalidChar}`, + }); + }).toThrow(/can contain only letters, numbers, hyphens, or underscores with no spaces./); + }); + }); + + test('No error when function name is Tokenized and Unresolved', () => { + const stack = new cdk.Stack(); + expect(() => { + const realFunctionName = 'a'.repeat(141); + const tokenizedFunctionName = cdk.Token.asString(new cdk.Intrinsic(realFunctionName)); + + new lambda.Function(stack, 'foo', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_14_X, + functionName: tokenizedFunctionName, + }); + }).not.toThrow(); + }); }); function newTestLambda(scope: constructs.Construct) { diff --git a/packages/@aws-cdk/aws-lambda/test/integ.lambda-insights-mapping.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.lambda-insights-mapping.expected.json index a7a94e5d13ba7..824a0b3e6b8f9 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.lambda-insights-mapping.expected.json +++ b/packages/@aws-cdk/aws-lambda/test/integ.lambda-insights-mapping.expected.json @@ -291,7 +291,7 @@ { "Ref": "AWS::Region" }, - "1_0_98_0_x86_64" + "1x0x98x0xx86x64" ] } ], @@ -364,7 +364,7 @@ { "Ref": "AWS::Region" }, - "1_0_119_0_x86_64" + "1x0x119x0xx86x64" ] } ], @@ -457,92 +457,92 @@ "Mappings": { "CloudwatchlambdainsightsversionMap": { "af-south-1": { - "1_0_98_0_x86_64": "arn:aws:lambda:af-south-1:012438385374:layer:LambdaInsightsExtension:8", - "1_0_119_0_x86_64": "arn:aws:lambda:af-south-1:012438385374:layer:LambdaInsightsExtension:9" + "1x0x98x0xx86x64": "arn:aws:lambda:af-south-1:012438385374:layer:LambdaInsightsExtension:8", + "1x0x119x0xx86x64": "arn:aws:lambda:af-south-1:012438385374:layer:LambdaInsightsExtension:9" }, "ap-east-1": { - "1_0_98_0_x86_64": "arn:aws:lambda:ap-east-1:519774774795:layer:LambdaInsightsExtension:8", - "1_0_119_0_x86_64": "arn:aws:lambda:ap-east-1:519774774795:layer:LambdaInsightsExtension:9" + "1x0x98x0xx86x64": "arn:aws:lambda:ap-east-1:519774774795:layer:LambdaInsightsExtension:8", + "1x0x119x0xx86x64": "arn:aws:lambda:ap-east-1:519774774795:layer:LambdaInsightsExtension:9" }, "ap-northeast-1": { - "1_0_98_0_x86_64": "arn:aws:lambda:ap-northeast-1:580247275435:layer:LambdaInsightsExtension:14", - "1_0_119_0_x86_64": "arn:aws:lambda:ap-northeast-1:580247275435:layer:LambdaInsightsExtension:23" + "1x0x98x0xx86x64": "arn:aws:lambda:ap-northeast-1:580247275435:layer:LambdaInsightsExtension:14", + "1x0x119x0xx86x64": "arn:aws:lambda:ap-northeast-1:580247275435:layer:LambdaInsightsExtension:23" }, "ap-northeast-2": { - "1_0_98_0_x86_64": "arn:aws:lambda:ap-northeast-2:580247275435:layer:LambdaInsightsExtension:14", - "1_0_119_0_x86_64": "arn:aws:lambda:ap-northeast-2:580247275435:layer:LambdaInsightsExtension:16" + "1x0x98x0xx86x64": "arn:aws:lambda:ap-northeast-2:580247275435:layer:LambdaInsightsExtension:14", + "1x0x119x0xx86x64": "arn:aws:lambda:ap-northeast-2:580247275435:layer:LambdaInsightsExtension:16" }, "ap-south-1": { - "1_0_98_0_x86_64": "arn:aws:lambda:ap-south-1:580247275435:layer:LambdaInsightsExtension:14", - "1_0_119_0_x86_64": "arn:aws:lambda:ap-south-1:580247275435:layer:LambdaInsightsExtension:16" + "1x0x98x0xx86x64": "arn:aws:lambda:ap-south-1:580247275435:layer:LambdaInsightsExtension:14", + "1x0x119x0xx86x64": "arn:aws:lambda:ap-south-1:580247275435:layer:LambdaInsightsExtension:16" }, "ap-southeast-1": { - "1_0_98_0_x86_64": "arn:aws:lambda:ap-southeast-1:580247275435:layer:LambdaInsightsExtension:14", - "1_0_119_0_x86_64": "arn:aws:lambda:ap-southeast-1:580247275435:layer:LambdaInsightsExtension:16" + "1x0x98x0xx86x64": "arn:aws:lambda:ap-southeast-1:580247275435:layer:LambdaInsightsExtension:14", + "1x0x119x0xx86x64": "arn:aws:lambda:ap-southeast-1:580247275435:layer:LambdaInsightsExtension:16" }, "ap-southeast-2": { - "1_0_98_0_x86_64": "arn:aws:lambda:ap-southeast-2:580247275435:layer:LambdaInsightsExtension:14", - "1_0_119_0_x86_64": "arn:aws:lambda:ap-southeast-2:580247275435:layer:LambdaInsightsExtension:16" + "1x0x98x0xx86x64": "arn:aws:lambda:ap-southeast-2:580247275435:layer:LambdaInsightsExtension:14", + "1x0x119x0xx86x64": "arn:aws:lambda:ap-southeast-2:580247275435:layer:LambdaInsightsExtension:16" }, "ca-central-1": { - "1_0_98_0_x86_64": "arn:aws:lambda:ca-central-1:580247275435:layer:LambdaInsightsExtension:14", - "1_0_119_0_x86_64": "arn:aws:lambda:ca-central-1:580247275435:layer:LambdaInsightsExtension:16" + "1x0x98x0xx86x64": "arn:aws:lambda:ca-central-1:580247275435:layer:LambdaInsightsExtension:14", + "1x0x119x0xx86x64": "arn:aws:lambda:ca-central-1:580247275435:layer:LambdaInsightsExtension:16" }, "cn-north-1": { - "1_0_98_0_x86_64": "arn:aws-cn:lambda:cn-north-1:488211338238:layer:LambdaInsightsExtension:8", - "1_0_119_0_x86_64": "arn:aws-cn:lambda:cn-north-1:488211338238:layer:LambdaInsightsExtension:9" + "1x0x98x0xx86x64": "arn:aws-cn:lambda:cn-north-1:488211338238:layer:LambdaInsightsExtension:8", + "1x0x119x0xx86x64": "arn:aws-cn:lambda:cn-north-1:488211338238:layer:LambdaInsightsExtension:9" }, "cn-northwest-1": { - "1_0_98_0_x86_64": "arn:aws-cn:lambda:cn-northwest-1:488211338238:layer:LambdaInsightsExtension:8", - "1_0_119_0_x86_64": "arn:aws-cn:lambda:cn-northwest-1:488211338238:layer:LambdaInsightsExtension:9" + "1x0x98x0xx86x64": "arn:aws-cn:lambda:cn-northwest-1:488211338238:layer:LambdaInsightsExtension:8", + "1x0x119x0xx86x64": "arn:aws-cn:lambda:cn-northwest-1:488211338238:layer:LambdaInsightsExtension:9" }, "eu-central-1": { - "1_0_98_0_x86_64": "arn:aws:lambda:eu-central-1:580247275435:layer:LambdaInsightsExtension:14", - "1_0_119_0_x86_64": "arn:aws:lambda:eu-central-1:580247275435:layer:LambdaInsightsExtension:16" + "1x0x98x0xx86x64": "arn:aws:lambda:eu-central-1:580247275435:layer:LambdaInsightsExtension:14", + "1x0x119x0xx86x64": "arn:aws:lambda:eu-central-1:580247275435:layer:LambdaInsightsExtension:16" }, "eu-north-1": { - "1_0_98_0_x86_64": "arn:aws:lambda:eu-north-1:580247275435:layer:LambdaInsightsExtension:14", - "1_0_119_0_x86_64": "arn:aws:lambda:eu-north-1:580247275435:layer:LambdaInsightsExtension:16" + "1x0x98x0xx86x64": "arn:aws:lambda:eu-north-1:580247275435:layer:LambdaInsightsExtension:14", + "1x0x119x0xx86x64": "arn:aws:lambda:eu-north-1:580247275435:layer:LambdaInsightsExtension:16" }, "eu-south-1": { - "1_0_98_0_x86_64": "arn:aws:lambda:eu-south-1:339249233099:layer:LambdaInsightsExtension:8", - "1_0_119_0_x86_64": "arn:aws:lambda:eu-south-1:339249233099:layer:LambdaInsightsExtension:9" + "1x0x98x0xx86x64": "arn:aws:lambda:eu-south-1:339249233099:layer:LambdaInsightsExtension:8", + "1x0x119x0xx86x64": "arn:aws:lambda:eu-south-1:339249233099:layer:LambdaInsightsExtension:9" }, "eu-west-1": { - "1_0_98_0_x86_64": "arn:aws:lambda:eu-west-1:580247275435:layer:LambdaInsightsExtension:14", - "1_0_119_0_x86_64": "arn:aws:lambda:eu-west-1:580247275435:layer:LambdaInsightsExtension:16" + "1x0x98x0xx86x64": "arn:aws:lambda:eu-west-1:580247275435:layer:LambdaInsightsExtension:14", + "1x0x119x0xx86x64": "arn:aws:lambda:eu-west-1:580247275435:layer:LambdaInsightsExtension:16" }, "eu-west-2": { - "1_0_98_0_x86_64": "arn:aws:lambda:eu-west-2:580247275435:layer:LambdaInsightsExtension:14", - "1_0_119_0_x86_64": "arn:aws:lambda:eu-west-2:580247275435:layer:LambdaInsightsExtension:16" + "1x0x98x0xx86x64": "arn:aws:lambda:eu-west-2:580247275435:layer:LambdaInsightsExtension:14", + "1x0x119x0xx86x64": "arn:aws:lambda:eu-west-2:580247275435:layer:LambdaInsightsExtension:16" }, "eu-west-3": { - "1_0_98_0_x86_64": "arn:aws:lambda:eu-west-3:580247275435:layer:LambdaInsightsExtension:14", - "1_0_119_0_x86_64": "arn:aws:lambda:eu-west-3:580247275435:layer:LambdaInsightsExtension:16" + "1x0x98x0xx86x64": "arn:aws:lambda:eu-west-3:580247275435:layer:LambdaInsightsExtension:14", + "1x0x119x0xx86x64": "arn:aws:lambda:eu-west-3:580247275435:layer:LambdaInsightsExtension:16" }, "me-south-1": { - "1_0_98_0_x86_64": "arn:aws:lambda:me-south-1:285320876703:layer:LambdaInsightsExtension:8", - "1_0_119_0_x86_64": "arn:aws:lambda:me-south-1:285320876703:layer:LambdaInsightsExtension:9" + "1x0x98x0xx86x64": "arn:aws:lambda:me-south-1:285320876703:layer:LambdaInsightsExtension:8", + "1x0x119x0xx86x64": "arn:aws:lambda:me-south-1:285320876703:layer:LambdaInsightsExtension:9" }, "sa-east-1": { - "1_0_98_0_x86_64": "arn:aws:lambda:sa-east-1:580247275435:layer:LambdaInsightsExtension:14", - "1_0_119_0_x86_64": "arn:aws:lambda:sa-east-1:580247275435:layer:LambdaInsightsExtension:16" + "1x0x98x0xx86x64": "arn:aws:lambda:sa-east-1:580247275435:layer:LambdaInsightsExtension:14", + "1x0x119x0xx86x64": "arn:aws:lambda:sa-east-1:580247275435:layer:LambdaInsightsExtension:16" }, "us-east-1": { - "1_0_98_0_x86_64": "arn:aws:lambda:us-east-1:580247275435:layer:LambdaInsightsExtension:14", - "1_0_119_0_x86_64": "arn:aws:lambda:us-east-1:580247275435:layer:LambdaInsightsExtension:16" + "1x0x98x0xx86x64": "arn:aws:lambda:us-east-1:580247275435:layer:LambdaInsightsExtension:14", + "1x0x119x0xx86x64": "arn:aws:lambda:us-east-1:580247275435:layer:LambdaInsightsExtension:16" }, "us-east-2": { - "1_0_98_0_x86_64": "arn:aws:lambda:us-east-2:580247275435:layer:LambdaInsightsExtension:14", - "1_0_119_0_x86_64": "arn:aws:lambda:us-east-2:580247275435:layer:LambdaInsightsExtension:16" + "1x0x98x0xx86x64": "arn:aws:lambda:us-east-2:580247275435:layer:LambdaInsightsExtension:14", + "1x0x119x0xx86x64": "arn:aws:lambda:us-east-2:580247275435:layer:LambdaInsightsExtension:16" }, "us-west-1": { - "1_0_98_0_x86_64": "arn:aws:lambda:us-west-1:580247275435:layer:LambdaInsightsExtension:14", - "1_0_119_0_x86_64": "arn:aws:lambda:us-west-1:580247275435:layer:LambdaInsightsExtension:16" + "1x0x98x0xx86x64": "arn:aws:lambda:us-west-1:580247275435:layer:LambdaInsightsExtension:14", + "1x0x119x0xx86x64": "arn:aws:lambda:us-west-1:580247275435:layer:LambdaInsightsExtension:16" }, "us-west-2": { - "1_0_98_0_x86_64": "arn:aws:lambda:us-west-2:580247275435:layer:LambdaInsightsExtension:14", - "1_0_119_0_x86_64": "arn:aws:lambda:us-west-2:580247275435:layer:LambdaInsightsExtension:16" + "1x0x98x0xx86x64": "arn:aws:lambda:us-west-2:580247275435:layer:LambdaInsightsExtension:14", + "1x0x119x0xx86x64": "arn:aws:lambda:us-west-2:580247275435:layer:LambdaInsightsExtension:16" } } } diff --git a/packages/@aws-cdk/aws-lambda/test/lambda-insights.test.ts b/packages/@aws-cdk/aws-lambda/test/lambda-insights.test.ts index 581106e7a1f92..ac758ab4be5ad 100644 --- a/packages/@aws-cdk/aws-lambda/test/lambda-insights.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/lambda-insights.test.ts @@ -1,9 +1,8 @@ -import '@aws-cdk/assert-internal/jest'; -import { arrayWith, SynthUtils } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ecr from '@aws-cdk/aws-ecr'; import * as cdk from '@aws-cdk/core'; -import * as lambda from '../lib'; import { Fact, FactName } from '@aws-cdk/region-info'; +import * as lambda from '../lib'; /** * Boilerplate code to create a Function with a given insights version @@ -28,7 +27,7 @@ function functionWithInsightsVersion( * Check if the function's Role has the Lambda Insights IAM policy */ function verifyRoleHasCorrectPolicies(stack: cdk.Stack) { - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { ManagedPolicyArns: [ { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole']] }, @@ -47,7 +46,7 @@ describe('lambda-insights', () => { verifyRoleHasCorrectPolicies(stack); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Layers: [layerArn], }); @@ -64,7 +63,7 @@ describe('lambda-insights', () => { verifyRoleHasCorrectPolicies(stack); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Layers: ['arn:aws:lambda:us-east-1:580247275435:layer:LambdaInsightsExtension:2'], }); @@ -91,14 +90,14 @@ describe('lambda-insights', () => { functionWithInsightsVersion(stack, 'MyLambda', lambda.LambdaInsightsVersion.VERSION_1_0_98_0); // Still resolves because all elements of the mapping map to the same value - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Layers: [{ 'Fn::FindInMap': [ 'CloudwatchlambdainsightsversionMap', { Ref: 'AWS::Region', }, - '1_0_98_0_x86_64', + '1x0x98x0xx86x64', ], }], }); @@ -115,23 +114,24 @@ describe('lambda-insights', () => { functionWithInsightsVersion(stack, 'MyLambda1', lambda.LambdaInsightsVersion.VERSION_1_0_98_0); functionWithInsightsVersion(stack, 'MyLambda2', lambda.LambdaInsightsVersion.VERSION_1_0_98_0); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { FunctionName: 'MyLambda1', Layers: [{ - 'Fn::FindInMap': ['CloudwatchlambdainsightsversionMap', { Ref: 'AWS::Region' }, '1_0_98_0_x86_64'], + 'Fn::FindInMap': ['CloudwatchlambdainsightsversionMap', { Ref: 'AWS::Region' }, '1x0x98x0xx86x64'], }], }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { FunctionName: 'MyLambda2', Layers: [{ - 'Fn::FindInMap': ['CloudwatchlambdainsightsversionMap', { Ref: 'AWS::Region' }, '1_0_98_0_x86_64'], + 'Fn::FindInMap': ['CloudwatchlambdainsightsversionMap', { Ref: 'AWS::Region' }, '1x0x98x0xx86x64'], }], }); - const template = SynthUtils.toCloudFormation(stack); - expect(template.Mappings.CloudwatchlambdainsightsversionMap?.['af-south-1']).toEqual({ - '1_0_98_0_x86_64': 'arn:aws:lambda:af-south-1:012438385374:layer:LambdaInsightsExtension:8', + Template.fromStack(stack).hasMapping('CloudwatchlambdainsightsversionMap', { + 'af-south-1': { + '1x0x98x0xx86x64': 'arn:aws:lambda:af-south-1:012438385374:layer:LambdaInsightsExtension:8', + }, }); // On synthesis it should not throw an error @@ -146,18 +146,19 @@ describe('lambda-insights', () => { insightsVersion: lambda.LambdaInsightsVersion.VERSION_1_0_98_0, }); - expect(stack).toCountResources('AWS::Lambda::LayerVersion', 0); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::LayerVersion', 0); - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { Action: 'sts:AssumeRole', + Effect: 'Allow', Principal: { Service: 'lambda.amazonaws.com' }, }, ], }, - ManagedPolicyArns: arrayWith( + ManagedPolicyArns: Match.arrayWith([ { 'Fn::Join': ['', [ 'arn:', @@ -165,7 +166,7 @@ describe('lambda-insights', () => { ':iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy', ]], }, - ), + ]), }); }); @@ -176,7 +177,7 @@ describe('lambda-insights', () => { }); functionWithInsightsVersion(stack, 'MyLambda', lambda.LambdaInsightsVersion.VERSION_1_0_119_0, lambda.Architecture.ARM_64); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Layers: ['arn:aws:lambda:us-east-1:580247275435:layer:LambdaInsightsExtension-Arm64:1'], }); @@ -214,24 +215,25 @@ describe('lambda-insights', () => { functionWithInsightsVersion(stack, 'MyLambda1', lambda.LambdaInsightsVersion.VERSION_1_0_119_0); functionWithInsightsVersion(stack, 'MyLambda2', lambda.LambdaInsightsVersion.VERSION_1_0_119_0, lambda.Architecture.ARM_64); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { FunctionName: 'MyLambda1', Layers: [{ - 'Fn::FindInMap': ['CloudwatchlambdainsightsversionMap', { Ref: 'AWS::Region' }, '1_0_119_0_x86_64'], + 'Fn::FindInMap': ['CloudwatchlambdainsightsversionMap', { Ref: 'AWS::Region' }, '1x0x119x0xx86x64'], }], }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { FunctionName: 'MyLambda2', Layers: [{ - 'Fn::FindInMap': ['CloudwatchlambdainsightsversionMap', { Ref: 'AWS::Region' }, '1_0_119_0_arm64'], + 'Fn::FindInMap': ['CloudwatchlambdainsightsversionMap', { Ref: 'AWS::Region' }, '1x0x119x0xarm64'], }], }); - const template = SynthUtils.toCloudFormation(stack); - expect(template.Mappings.CloudwatchlambdainsightsversionMap?.['ap-south-1']).toEqual({ - '1_0_119_0_x86_64': 'arn:aws:lambda:ap-south-1:580247275435:layer:LambdaInsightsExtension:16', - '1_0_119_0_arm64': 'arn:aws:lambda:ap-south-1:580247275435:layer:LambdaInsightsExtension-Arm64:1', + Template.fromStack(stack).hasMapping('CloudwatchlambdainsightsversionMap', { + 'ap-south-1': { + '1x0x119x0xx86x64': 'arn:aws:lambda:ap-south-1:580247275435:layer:LambdaInsightsExtension:16', + '1x0x119x0xarm64': 'arn:aws:lambda:ap-south-1:580247275435:layer:LambdaInsightsExtension-Arm64:1', + }, }); // On synthesis it should not throw an error diff --git a/packages/@aws-cdk/aws-lambda/test/lambda-version.test.ts b/packages/@aws-cdk/aws-lambda/test/lambda-version.test.ts index c5daabab66b5d..ed98e802998dd 100644 --- a/packages/@aws-cdk/aws-lambda/test/lambda-version.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/lambda-version.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as lambda from '../lib'; @@ -14,7 +14,7 @@ describe('lambda version', () => { new cdk.CfnOutput(stack, 'Name', { value: version.functionName }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Outputs: { ARN: { Value: 'arn:aws:lambda:region:account-id:function:function-name:version', @@ -43,7 +43,7 @@ describe('lambda version', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::EventInvokeConfig', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventInvokeConfig', { FunctionName: { Ref: 'Fn9270CBC0', }, @@ -91,12 +91,12 @@ describe('lambda version', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::EventInvokeConfig', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventInvokeConfig', { FunctionName: 'function-name', Qualifier: 'version1', MaximumRetryAttempts: 1, }); - expect(stack).toHaveResource('AWS::Lambda::EventInvokeConfig', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventInvokeConfig', { FunctionName: 'function-name', Qualifier: 'version2', MaximumRetryAttempts: 0, @@ -117,7 +117,7 @@ describe('lambda version', () => { version.addAlias('foo'); // THEN - expect(stack).toHaveResource('AWS::Lambda::Alias', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Alias', { FunctionName: { Ref: 'Fn9270CBC0', }, diff --git a/packages/@aws-cdk/aws-lambda/test/layers.test.ts b/packages/@aws-cdk/aws-lambda/test/layers.test.ts index c8c700585c686..5f818c480e2da 100644 --- a/packages/@aws-cdk/aws-lambda/test/layers.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/layers.test.ts @@ -1,6 +1,5 @@ -import '@aws-cdk/assert-internal/jest'; import * as path from 'path'; -import { canonicalizeTemplate, ResourcePart, SynthUtils } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; @@ -20,7 +19,7 @@ describe('layers', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::LayerVersion', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::LayerVersion', { Content: { S3Bucket: stack.resolve(bucket.bucketName), S3Key: 'ObjectKey', @@ -44,12 +43,12 @@ describe('layers', () => { layer.addPermission('GrantUsage-o-123456', { accountId: '*', organizationId: 'o-123456' }); // THEN - expect(stack).toHaveResource('AWS::Lambda::LayerVersionPermission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::LayerVersionPermission', { Action: 'lambda:GetLayerVersion', LayerVersionArn: stack.resolve(layer.layerVersionArn), Principal: '123456789012', }); - expect(stack).toHaveResource('AWS::Lambda::LayerVersionPermission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::LayerVersionPermission', { Action: 'lambda:GetLayerVersion', LayerVersionArn: stack.resolve(layer.layerVersionArn), Principal: '*', @@ -79,13 +78,13 @@ describe('layers', () => { }); // THEN - expect(canonicalizeTemplate(SynthUtils.toCloudFormation(stack))).toHaveResource('AWS::Lambda::LayerVersion', { + Template.fromStack(stack).hasResource('AWS::Lambda::LayerVersion', { Metadata: { - 'aws:asset:path': 'asset.Asset1Hash', + 'aws:asset:path': 'asset.8811a2632ac5564a08fd269e159298f7e497f259578b0dc5e927a1f48ab24d34', 'aws:asset:is-bundled': false, 'aws:asset:property': 'Content', }, - }, ResourcePart.CompleteDefinition); + }); }); test('creating a layer with a removal policy', () => { @@ -99,10 +98,10 @@ describe('layers', () => { }); // THEN - expect(canonicalizeTemplate(SynthUtils.toCloudFormation(stack))).toHaveResource('AWS::Lambda::LayerVersion', { + Template.fromStack(stack).hasResource('AWS::Lambda::LayerVersion', { UpdateReplacePolicy: 'Retain', DeletionPolicy: 'Retain', - }, ResourcePart.CompleteDefinition); + }); }); test('specified compatible architectures is recognized', () => { @@ -114,7 +113,7 @@ describe('layers', () => { compatibleArchitectures: [lambda.Architecture.ARM_64], }); - expect(stack).toHaveResource('AWS::Lambda::LayerVersion', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::LayerVersion', { CompatibleArchitectures: ['arm64'], }); }); diff --git a/packages/@aws-cdk/aws-lambda/test/runtime.test.ts b/packages/@aws-cdk/aws-lambda/test/runtime.test.ts index 17203a11f9d7e..f3976e70c4327 100644 --- a/packages/@aws-cdk/aws-lambda/test/runtime.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/runtime.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import * as lambda from '../lib'; describe('runtime', () => { diff --git a/packages/@aws-cdk/aws-lambda/test/singleton-lambda.test.ts b/packages/@aws-cdk/aws-lambda/test/singleton-lambda.test.ts index 1e9f984b4aee5..3e6db8d6ea422 100644 --- a/packages/@aws-cdk/aws-lambda/test/singleton-lambda.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/singleton-lambda.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; @@ -23,7 +22,7 @@ describe('singleton lambda', () => { } // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { SingletonLambda84c0de93353f42179b0b45b6c993251aServiceRole26D59235: { Type: 'AWS::IAM::Role', @@ -78,12 +77,12 @@ describe('singleton lambda', () => { singleton.addDependency(dependency); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { DependsOn: [ 'dependencyUser1B9CB07E', 'SingletonLambda84c0de93353f42179b0b45b6c993251aServiceRole26D59235', ], - }, ResourcePart.CompleteDefinition); + }); }); test('dependsOn are correctly added', () => { @@ -102,12 +101,12 @@ describe('singleton lambda', () => { singleton.dependOn(user); // THEN - expect(stack).toHaveResource('AWS::IAM::User', { + Template.fromStack(stack).hasResource('AWS::IAM::User', { DependsOn: [ 'SingletonLambda84c0de93353f42179b0b45b6c993251a840BCC38', 'SingletonLambda84c0de93353f42179b0b45b6c993251aServiceRole26D59235', ], - }, ResourcePart.CompleteDefinition); + }); }); test('Environment is added to Lambda, when .addEnvironment() is provided one key pair', () => { @@ -125,7 +124,7 @@ describe('singleton lambda', () => { singleton.addEnvironment('KEY', 'value'); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Environment: { Variables: { KEY: 'value', @@ -154,7 +153,7 @@ describe('singleton lambda', () => { singleton.addLayers(layer); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Layers: [{ Ref: 'myLayerBA1B098A', }], @@ -176,7 +175,7 @@ describe('singleton lambda', () => { const statement = stack.resolve(invokeResult.resourceStatement); // THEN - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', Principal: 'events.amazonaws.com', }); @@ -256,7 +255,7 @@ describe('singleton lambda', () => { version.addAlias('foo'); // THEN - expect(stack).toHaveResource('AWS::Lambda::Version', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Version', { FunctionName: { Ref: 'SingletonLambda84c0de93353f42179b0b45b6c993251a840BCC38', }, diff --git a/packages/@aws-cdk/aws-lambda/test/vpc-lambda.test.ts b/packages/@aws-cdk/aws-lambda/test/vpc-lambda.test.ts index 409ffc5fa3a45..aa7587411fa26 100644 --- a/packages/@aws-cdk/aws-lambda/test/vpc-lambda.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/vpc-lambda.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import * as cdk from '@aws-cdk/core'; @@ -29,7 +29,7 @@ describe('lambda + vpc', () => { test('has subnet and securitygroup', () => { // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { VpcConfig: { SecurityGroupIds: [ { 'Fn::GetAtt': ['LambdaSecurityGroupE74659A1', 'GroupId'] }, @@ -52,7 +52,7 @@ describe('lambda + vpc', () => { securityGroup: new ec2.SecurityGroup(stack, 'CustomSecurityGroupX', { vpc }), }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { VpcConfig: { SecurityGroupIds: [ { 'Fn::GetAtt': ['CustomSecurityGroupX6C7F3A78', 'GroupId'] }, @@ -78,7 +78,7 @@ describe('lambda + vpc', () => { ], }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { VpcConfig: { SecurityGroupIds: [ { 'Fn::GetAtt': ['CustomSecurityGroupA267F62DE', 'GroupId'] }, @@ -118,7 +118,7 @@ describe('lambda + vpc', () => { fn.connections.allowTo(somethingConnectable, ec2.Port.allTcp(), 'Lambda can call connectable'); // THEN: Lambda can connect to SomeSecurityGroup - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: { 'Fn::GetAtt': ['LambdaSecurityGroupE74659A1', 'GroupId'] }, IpProtocol: 'tcp', Description: 'Lambda can call connectable', @@ -128,7 +128,7 @@ describe('lambda + vpc', () => { }); // THEN: SomeSecurityGroup accepts connections from Lambda - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { IpProtocol: 'tcp', Description: 'Lambda can call connectable', FromPort: 0, @@ -148,7 +148,7 @@ describe('lambda + vpc', () => { somethingConnectable.connections.allowFrom(fn.connections, ec2.Port.allTcp(), 'Lambda can call connectable'); // THEN: SomeSecurityGroup accepts connections from Lambda - expect(stack2).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack2).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: { 'Fn::ImportValue': 'stack:ExportsOutputFnGetAttLambdaSecurityGroupE74659A1GroupId8F3EC6F1', }, @@ -165,7 +165,7 @@ describe('lambda + vpc', () => { }); // THEN: Lambda can connect to SomeSecurityGroup - expect(stack2).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack2).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { IpProtocol: 'tcp', Description: 'Lambda can call connectable', FromPort: 0, @@ -214,7 +214,7 @@ describe('lambda + vpc', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { VpcConfig: { SecurityGroupIds: [ { 'Fn::GetAtt': ['PublicLambdaSecurityGroup61D896FD', 'GroupId'] }, @@ -243,7 +243,7 @@ describe('lambda + vpc', () => { // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { VpcConfig: { SecurityGroupIds: [ { 'Fn::GetAtt': ['PrivateLambdaSecurityGroupF53C8342', 'GroupId'] }, @@ -279,7 +279,7 @@ describe('lambda + vpc', () => { // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { VpcConfig: { SecurityGroupIds: [ { 'Fn::GetAtt': ['IsolatedLambdaSecurityGroupCE25B6A9', 'GroupId'] }, diff --git a/packages/@aws-cdk/aws-licensemanager/package.json b/packages/@aws-cdk/aws-licensemanager/package.json index 6ff8f00cb60cb..7a41e125ce49d 100644 --- a/packages/@aws-cdk/aws-licensemanager/package.json +++ b/packages/@aws-cdk/aws-licensemanager/package.json @@ -28,6 +28,13 @@ "distName": "aws-cdk.aws-licensemanager", "module": "aws_cdk.aws_licensemanager" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-lightsail/package.json b/packages/@aws-cdk/aws-lightsail/package.json index d577b74448e78..8f71c3ebd1192 100644 --- a/packages/@aws-cdk/aws-lightsail/package.json +++ b/packages/@aws-cdk/aws-lightsail/package.json @@ -7,6 +7,13 @@ "jsii": { "outdir": "dist", "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + }, "targets": { "dotnet": { "namespace": "Amazon.CDK.AWS.Lightsail", @@ -81,7 +88,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-location/package.json b/packages/@aws-cdk/aws-location/package.json index 17d950bc136bb..e686e64f27c41 100644 --- a/packages/@aws-cdk/aws-location/package.json +++ b/packages/@aws-cdk/aws-location/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-location", "module": "aws_cdk.aws_location" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-logs-destinations/README.md b/packages/@aws-cdk/aws-logs-destinations/README.md index 28d124c669017..4e2b4d7bedf5b 100644 --- a/packages/@aws-cdk/aws-logs-destinations/README.md +++ b/packages/@aws-cdk/aws-logs-destinations/README.md @@ -9,4 +9,7 @@ -A short description here. +This library contains destinations for AWS CloudWatch Logs SubscriptionFilters. You +can send log data to Kinesis Streams or Lambda Functions. + +See the documentation of the `logs` module for more information. diff --git a/packages/@aws-cdk/aws-logs-destinations/lib/kinesis.ts b/packages/@aws-cdk/aws-logs-destinations/lib/kinesis.ts index e4f57f5d4b33f..41aca9dea4ca5 100644 --- a/packages/@aws-cdk/aws-logs-destinations/lib/kinesis.ts +++ b/packages/@aws-cdk/aws-logs-destinations/lib/kinesis.ts @@ -3,18 +3,35 @@ import * as kinesis from '@aws-cdk/aws-kinesis'; import * as logs from '@aws-cdk/aws-logs'; import { Construct } from '@aws-cdk/core'; +/** + * Customize the Kinesis Logs Destination + */ +export interface KinesisDestinationProps { + /** + * The role to assume to write log events to the destination + * + * @default - A new Role is created + */ + readonly role?: iam.IRole; +} + /** * Use a Kinesis stream as the destination for a log subscription */ export class KinesisDestination implements logs.ILogSubscriptionDestination { - constructor(private readonly stream: kinesis.IStream) { + /** + * @param stream The Kinesis stream to use as destination + * @param props The Kinesis Destination properties + * + */ + constructor(private readonly stream: kinesis.IStream, private readonly props: KinesisDestinationProps = {}) { } public bind(scope: Construct, _sourceLogGroup: logs.ILogGroup): logs.LogSubscriptionDestinationConfig { // Following example from https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/SubscriptionFilters.html#DestinationKinesisExample // Create a role to be assumed by CWL that can write to this stream and pass itself. const id = 'CloudWatchLogsCanPutRecords'; - const role = scope.node.tryFindChild(id) as iam.IRole || new iam.Role(scope, id, { + const role = this.props.role ?? scope.node.tryFindChild(id) as iam.IRole ?? new iam.Role(scope, id, { assumedBy: new iam.ServicePrincipal('logs.amazonaws.com'), }); this.stream.grantWrite(role); diff --git a/packages/@aws-cdk/aws-logs-destinations/package.json b/packages/@aws-cdk/aws-logs-destinations/package.json index 192ba4f6a0248..07e783848cb15 100644 --- a/packages/@aws-cdk/aws-logs-destinations/package.json +++ b/packages/@aws-cdk/aws-logs-destinations/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -64,13 +71,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-iam": "0.0.0", diff --git a/packages/@aws-cdk/aws-logs-destinations/test/kinesis.test.ts b/packages/@aws-cdk/aws-logs-destinations/test/kinesis.test.ts index 42c87261d60b2..e09a58ba7dd3a 100644 --- a/packages/@aws-cdk/aws-logs-destinations/test/kinesis.test.ts +++ b/packages/@aws-cdk/aws-logs-destinations/test/kinesis.test.ts @@ -1,4 +1,5 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; +import * as iam from '@aws-cdk/aws-iam'; import * as kinesis from '@aws-cdk/aws-kinesis'; import * as logs from '@aws-cdk/aws-logs'; import * as cdk from '@aws-cdk/core'; @@ -18,13 +19,13 @@ test('stream can be subscription destination', () => { }); // THEN: subscription target is Stream - expect(stack).toHaveResource('AWS::Logs::SubscriptionFilter', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::SubscriptionFilter', { DestinationArn: { 'Fn::GetAtt': ['MyStream5C050E93', 'Arn'] }, RoleArn: { 'Fn::GetAtt': ['SubscriptionCloudWatchLogsCanPutRecords9C1223EC', 'Arn'] }, }); // THEN: we have a role to write to the Stream - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Version: '2012-10-17', Statement: [{ @@ -44,7 +45,7 @@ test('stream can be subscription destination', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -88,13 +89,13 @@ test('stream can be subscription destination twice, without duplicating permissi }); // THEN: subscription target is Stream - expect(stack).toHaveResource('AWS::Logs::SubscriptionFilter', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::SubscriptionFilter', { DestinationArn: { 'Fn::GetAtt': ['MyStream5C050E93', 'Arn'] }, RoleArn: { 'Fn::GetAtt': ['SubscriptionCloudWatchLogsCanPutRecords9C1223EC', 'Arn'] }, }); // THEN: we have a role to write to the Stream - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Version: '2012-10-17', Statement: [{ @@ -114,7 +115,7 @@ test('stream can be subscription destination twice, without duplicating permissi }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -136,3 +137,54 @@ test('stream can be subscription destination twice, without duplicating permissi }, }); }); + +test('an existing IAM role can be passed to new destination instance instead of auto-created ', ()=> { + // GIVEN + const stack = new cdk.Stack(); + const stream = new kinesis.Stream(stack, 'MyStream'); + const logGroup = new logs.LogGroup(stack, 'LogGroup'); + + const importedRole = iam.Role.fromRoleArn(stack, 'ImportedRole', 'arn:aws:iam::123456789012:role/ImportedRoleKinesisDestinationTest'); + + const kinesisDestination = new dests.KinesisDestination(stream, { role: importedRole }); + + new logs.SubscriptionFilter(logGroup, 'MySubscriptionFilter', { + logGroup: logGroup, + destination: kinesisDestination, + filterPattern: logs.FilterPattern.allEvents(), + }); + + // THEN + const template = Template.fromStack(stack); + template.resourceCountIs('AWS::IAM::Role', 0); + template.hasResourceProperties('AWS::Logs::SubscriptionFilter', { + RoleArn: importedRole.roleArn, + }); +}); + +test('creates a new IAM Role if not passed on new destination instance', ()=> { + // GIVEN + const stack = new cdk.Stack(); + const stream = new kinesis.Stream(stack, 'MyStream'); + const logGroup = new logs.LogGroup(stack, 'LogGroup'); + + const kinesisDestination = new dests.KinesisDestination(stream); + + new logs.SubscriptionFilter(logGroup, 'MySubscriptionFilter', { + logGroup: logGroup, + destination: kinesisDestination, + filterPattern: logs.FilterPattern.allEvents(), + }); + + // THEN + const template = Template.fromStack(stack); + template.resourceCountIs('AWS::IAM::Role', 1); + template.hasResourceProperties('AWS::Logs::SubscriptionFilter', { + RoleArn: { + 'Fn::GetAtt': [ + 'LogGroupMySubscriptionFilterCloudWatchLogsCanPutRecords9112BD02', + 'Arn', + ], + }, + }); +}); diff --git a/packages/@aws-cdk/aws-logs-destinations/test/lambda.test.ts b/packages/@aws-cdk/aws-logs-destinations/test/lambda.test.ts index b1f0048716a74..f5a80285c8146 100644 --- a/packages/@aws-cdk/aws-logs-destinations/test/lambda.test.ts +++ b/packages/@aws-cdk/aws-logs-destinations/test/lambda.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as lambda from '@aws-cdk/aws-lambda'; import * as logs from '@aws-cdk/aws-logs'; import * as cdk from '@aws-cdk/core'; @@ -28,12 +28,12 @@ test('lambda can be used as metric subscription destination', () => { }); // THEN: subscription target is Lambda - expect(stack).toHaveResource('AWS::Logs::SubscriptionFilter', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::SubscriptionFilter', { DestinationArn: { 'Fn::GetAtt': ['MyLambdaCCE802FB', 'Arn'] }, }); // THEN: Lambda has permissions to be invoked by CWL - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', FunctionName: { 'Fn::GetAtt': ['MyLambdaCCE802FB', 'Arn'] }, Principal: 'logs.amazonaws.com', @@ -55,14 +55,14 @@ test('can have multiple subscriptions use the same Lambda', () => { }); // THEN: Lambda has permissions to be invoked by CWL from both Source Arns - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', FunctionName: { 'Fn::GetAtt': ['MyLambdaCCE802FB', 'Arn'] }, SourceArn: { 'Fn::GetAtt': ['LogGroupF5B46931', 'Arn'] }, Principal: 'logs.amazonaws.com', }); - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', FunctionName: { 'Fn::GetAtt': ['MyLambdaCCE802FB', 'Arn'] }, SourceArn: { 'Fn::GetAtt': ['LG224A94C8F', 'Arn'] }, @@ -79,14 +79,14 @@ test('lambda permissions are not added when addPermissions is false', () => { }); // THEN: subscription target is Lambda - expect(stack).toHaveResource('AWS::Logs::SubscriptionFilter', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::SubscriptionFilter', { DestinationArn: { 'Fn::GetAtt': ['MyLambdaCCE802FB', 'Arn'] }, }); // THEN: Lambda does not have permissions to be invoked by CWL - expect(stack).not.toHaveResource('AWS::Lambda::Permission', { + expect(Template.fromStack(stack).findResources('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', FunctionName: { 'Fn::GetAtt': ['MyLambdaCCE802FB', 'Arn'] }, Principal: 'logs.amazonaws.com', - }); + })).toEqual({}); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-logs/package.json b/packages/@aws-cdk/aws-logs/package.json index a30de23a679a7..770c2ff30e78c 100644 --- a/packages/@aws-cdk/aws-logs/package.json +++ b/packages/@aws-cdk/aws-logs/package.json @@ -79,18 +79,18 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/aws-lambda": "^8.10.89", - "@types/jest": "^27.0.3", + "@types/aws-lambda": "^8.10.92", + "@types/jest": "^27.4.1", "@types/sinon": "^9.0.11", "aws-sdk": "^2.848.0", - "aws-sdk-mock": "^5.5.0", - "jest": "^27.4.5", - "nock": "^13.2.1", + "aws-sdk-mock": "5.6.0", + "jest": "^27.5.1", + "nock": "^13.2.4", "sinon": "^9.2.4" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-logs/test/destination.test.ts b/packages/@aws-cdk/aws-logs/test/destination.test.ts index 05f482441d265..096addb02a885 100644 --- a/packages/@aws-cdk/aws-logs/test/destination.test.ts +++ b/packages/@aws-cdk/aws-logs/test/destination.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template, Match } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import { CrossAccountDestination } from '../lib'; @@ -19,13 +19,11 @@ describe('destination', () => { }); // THEN - expect(stack).toHaveResource('AWS::Logs::Destination', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::Destination', { DestinationName: 'MyDestination', RoleArn: { 'Fn::GetAtt': ['Role1ABCC5F0', 'Arn'] }, TargetArn: 'arn:bogus', }); - - }); test('add policy to destination', () => { @@ -47,12 +45,24 @@ describe('destination', () => { })); // THEN - expect(stack).toHaveResource('AWS::Logs::Destination', (props: any) => { - const pol = JSON.parse(props.DestinationPolicy); - - return pol.Statement[0].Action === 'logs:TalkToMe'; + Template.fromStack(stack).hasResourceProperties('AWS::Logs::Destination', { + DestinationName: 'MyDestination', + DestinationPolicy: Match.serializedJson({ + Statement: [ + { + Action: 'logs:TalkToMe', + Effect: 'Allow', + }, + ], + Version: '2012-10-17', + }), + RoleArn: { + 'Fn::GetAtt': [ + 'Role1ABCC5F0', + 'Arn', + ], + }, + TargetArn: 'arn:bogus', }); - - }); }); diff --git a/packages/@aws-cdk/aws-logs/test/log-retention.test.ts b/packages/@aws-cdk/aws-logs/test/log-retention.test.ts index 7fd7d6b8532bf..5902f7de8dd86 100644 --- a/packages/@aws-cdk/aws-logs/test/log-retention.test.ts +++ b/packages/@aws-cdk/aws-logs/test/log-retention.test.ts @@ -1,6 +1,5 @@ import * as path from 'path'; -import '@aws-cdk/assert-internal/jest'; -import { ABSENT, ResourcePart } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; @@ -20,7 +19,7 @@ describe('log retention', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -42,12 +41,12 @@ describe('log retention', () => { ], }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Handler: 'index.handler', Runtime: 'nodejs14.x', }); - expect(stack).toHaveResource('Custom::LogRetention', { + Template.fromStack(stack).hasResourceProperties('Custom::LogRetention', { 'ServiceToken': { 'Fn::GetAtt': [ 'LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A', @@ -57,8 +56,6 @@ describe('log retention', () => { 'LogGroupName': 'group', 'RetentionInDays': 30, }); - - }); test('with imported role', () => { @@ -74,7 +71,7 @@ describe('log retention', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -94,9 +91,7 @@ describe('log retention', () => { ], }); - expect(stack).toCountResources('AWS::IAM::Role', 0); - - + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 0); }); test('with RetentionPeriod set to Infinity', () => { @@ -107,11 +102,9 @@ describe('log retention', () => { retention: RetentionDays.INFINITE, }); - expect(stack).toHaveResource('Custom::LogRetention', { - RetentionInDays: ABSENT, + Template.fromStack(stack).hasResourceProperties('Custom::LogRetention', { + RetentionInDays: Match.absent(), }); - - }); test('with LogGroupRegion specified', () => { @@ -122,11 +115,9 @@ describe('log retention', () => { retention: RetentionDays.INFINITE, }); - expect(stack).toHaveResource('Custom::LogRetention', { + Template.fromStack(stack).hasResourceProperties('Custom::LogRetention', { LogGroupRegion: 'us-east-1', }); - - }); test('log group ARN is well formed and conforms', () => { @@ -140,7 +131,6 @@ describe('log retention', () => { expect(logGroupArn.indexOf('logs')).toBeGreaterThan(-1); expect(logGroupArn.indexOf('log-group')).toBeGreaterThan(-1); expect(logGroupArn.endsWith(':*')).toEqual(true); - }); test('log group ARN is well formed and conforms when region is specified', () => { @@ -156,7 +146,6 @@ describe('log retention', () => { expect(logGroupArn.indexOf('logs')).toBeGreaterThan(-1); expect(logGroupArn.indexOf('log-group')).toBeGreaterThan(-1); expect(logGroupArn.endsWith(':*')).toEqual(true); - }); test('retention Lambda CfnResource receives propagated tags', () => { @@ -167,7 +156,7 @@ describe('log retention', () => { retention: RetentionDays.ONE_MONTH, }); - expect(stack).toHaveResourceLike('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Tags: [ { Key: 'test-key', @@ -175,7 +164,6 @@ describe('log retention', () => { }, ], }); - }); test('asset metadata added to log retention construct lambda function', () => { @@ -193,13 +181,12 @@ describe('log retention', () => { }); // Then - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { Metadata: { 'aws:asset:path': assetLocation, 'aws:asset:is-bundled': false, 'aws:asset:property': 'Code', }, - }, ResourcePart.CompleteDefinition); - + }); }); }); diff --git a/packages/@aws-cdk/aws-logs/test/loggroup.test.ts b/packages/@aws-cdk/aws-logs/test/loggroup.test.ts index 7928de182d7ff..4fefc67272d5f 100644 --- a/packages/@aws-cdk/aws-logs/test/loggroup.test.ts +++ b/packages/@aws-cdk/aws-logs/test/loggroup.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import { CfnParameter, RemovalPolicy, Stack } from '@aws-cdk/core'; @@ -16,12 +16,9 @@ describe('log group', () => { }); // THEN - expect(stack).toHaveResource('AWS::Logs::LogGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogGroup', { KmsKeyId: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, - }); - - }); test('fixed retention', () => { @@ -34,11 +31,9 @@ describe('log group', () => { }); // THEN - expect(stack).toHaveResource('AWS::Logs::LogGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogGroup', { RetentionInDays: 7, }); - - }); test('default retention', () => { @@ -49,11 +44,9 @@ describe('log group', () => { new LogGroup(stack, 'LogGroup'); // THEN - expect(stack).toHaveResource('AWS::Logs::LogGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogGroup', { RetentionInDays: 731, }); - - }); test('infinite retention/dont delete log group by default', () => { @@ -66,7 +59,7 @@ describe('log group', () => { }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { LogGroupF5B46931: { Type: 'AWS::Logs::LogGroup', @@ -75,8 +68,6 @@ describe('log group', () => { }, }, }); - - }); test('infinite retention via legacy method', () => { @@ -92,7 +83,7 @@ describe('log group', () => { }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { LogGroupF5B46931: { Type: 'AWS::Logs::LogGroup', @@ -101,8 +92,6 @@ describe('log group', () => { }, }, }); - - }); test('unresolved retention', () => { @@ -116,13 +105,11 @@ describe('log group', () => { }); // THEN - expect(stack).toHaveResource('AWS::Logs::LogGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogGroup', { RetentionInDays: { Ref: 'RetentionInDays', }, }); - - }); test('will delete log group if asked to', () => { @@ -136,7 +123,7 @@ describe('log group', () => { }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { LogGroupF5B46931: { Type: 'AWS::Logs::LogGroup', @@ -145,8 +132,6 @@ describe('log group', () => { }, }, }); - - }); test('import from ARN, same region', () => { @@ -160,10 +145,9 @@ describe('log group', () => { // THEN expect(imported.logGroupName).toEqual('my-log-group'); expect(imported.logGroupArn).toEqual('arn:aws:logs:us-east-1:123456789012:log-group:my-log-group:*'); - expect(stack2).toHaveResource('AWS::Logs::LogStream', { + Template.fromStack(stack2).hasResourceProperties('AWS::Logs::LogStream', { LogGroupName: 'my-log-group', }); - }); test('import from ARN, different region', () => { @@ -182,10 +166,10 @@ describe('log group', () => { expect(imported.env.region).not.toEqual(stack.region); expect(imported.env.region).toEqual(importRegion); - expect(stack).toHaveResource('AWS::Logs::LogStream', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogStream', { LogGroupName: 'my-log-group', }); - expect(stack).toCountResources('AWS::Logs::LogGroup', 0); + Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 0); }); test('import from name', () => { @@ -200,10 +184,9 @@ describe('log group', () => { expect(imported.logGroupName).toEqual('my-log-group'); expect(imported.logGroupArn).toMatch(/^arn:.+:logs:.+:.+:log-group:my-log-group:\*$/); - expect(stack).toHaveResource('AWS::Logs::LogStream', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogStream', { LogGroupName: 'my-log-group', }); - }); describe('loggroups imported by name have stream wildcard appended to grant ARN', () => void dataDrivenTests([ @@ -221,7 +204,7 @@ describe('log group', () => { imported.grantWrite(user); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -243,9 +226,8 @@ describe('log group', () => { ], }, }); - expect(imported.logGroupName).toEqual('my-log-group'); - + expect(imported.logGroupName).toEqual('my-log-group'); })); describe('loggroups imported by ARN have stream wildcard appended to grant ARN', () => void dataDrivenTests([ @@ -263,7 +245,7 @@ describe('log group', () => { imported.grantWrite(user); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -275,9 +257,8 @@ describe('log group', () => { ], }, }); - expect(imported.logGroupName).toEqual('my-log-group'); - + expect(imported.logGroupName).toEqual('my-log-group'); })); test('extractMetric', () => { @@ -289,7 +270,7 @@ describe('log group', () => { const metric = lg.extractMetric('$.myField', 'MyService', 'Field'); // THEN - expect(stack).toHaveResource('AWS::Logs::MetricFilter', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::MetricFilter', { FilterPattern: '{ $.myField = "*" }', LogGroupName: { Ref: 'LogGroupF5B46931' }, MetricTransformations: [ @@ -300,10 +281,9 @@ describe('log group', () => { }, ], }); + expect(metric.namespace).toEqual('MyService'); expect(metric.metricName).toEqual('Field'); - - }); test('extractMetric allows passing in namespaces with "/"', () => { @@ -315,7 +295,7 @@ describe('log group', () => { const metric = lg.extractMetric('$.myField', 'MyNamespace/MyService', 'Field'); // THEN - expect(stack).toHaveResource('AWS::Logs::MetricFilter', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::MetricFilter', { FilterPattern: '{ $.myField = "*" }', MetricTransformations: [ { @@ -325,10 +305,9 @@ describe('log group', () => { }, ], }); + expect(metric.namespace).toEqual('MyNamespace/MyService'); expect(metric.metricName).toEqual('Field'); - - }); test('grant', () => { @@ -341,7 +320,7 @@ describe('log group', () => { lg.grantWrite(user); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -353,8 +332,6 @@ describe('log group', () => { Version: '2012-10-17', }, }); - - }); test('grant to service principal', () => { @@ -367,7 +344,7 @@ describe('log group', () => { lg.grantWrite(sp); // THEN - expect(stack).toHaveResource('AWS::Logs::ResourcePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::ResourcePolicy', { PolicyDocument: { 'Fn::Join': [ '', @@ -385,10 +362,8 @@ describe('log group', () => { }, PolicyName: 'LogGroupPolicy643B329C', }); - }); - test('can add a policy to the log group', () => { // GIVEN const stack = new Stack(); @@ -402,7 +377,7 @@ describe('log group', () => { })); // THEN - expect(stack).toHaveResource('AWS::Logs::ResourcePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::ResourcePolicy', { PolicyDocument: '{"Statement":[{"Action":"logs:PutLogEvents","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::123456789012:user/user-name"},"Resource":"*"}],"Version":"2012-10-17"}', PolicyName: 'LogGroupPolicy643B329C', }); @@ -419,11 +394,9 @@ describe('log group', () => { // THEN expect(logGroup.logGroupPhysicalName()).toEqual('my-log-group'); - expect(stack).toHaveResource('AWS::Logs::LogGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogGroup', { LogGroupName: 'my-log-group', }); - - }); }); @@ -434,5 +407,4 @@ function dataDrivenTests(cases: string[], body: (suffix: string) => void): void body(args); }); } - } diff --git a/packages/@aws-cdk/aws-logs/test/logstream.test.ts b/packages/@aws-cdk/aws-logs/test/logstream.test.ts index c55501adc6b1b..d599ca8bdbe0a 100644 --- a/packages/@aws-cdk/aws-logs/test/logstream.test.ts +++ b/packages/@aws-cdk/aws-logs/test/logstream.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import { LogGroup, LogStream } from '../lib'; @@ -15,9 +15,6 @@ describe('log stream', () => { }); // THEN - expect(stack).toHaveResource('AWS::Logs::LogStream', { - }); - - + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogStream', { }); }); }); diff --git a/packages/@aws-cdk/aws-logs/test/metricfilter.test.ts b/packages/@aws-cdk/aws-logs/test/metricfilter.test.ts index 15814140a9488..6ab67899db2ea 100644 --- a/packages/@aws-cdk/aws-logs/test/metricfilter.test.ts +++ b/packages/@aws-cdk/aws-logs/test/metricfilter.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Metric } from '@aws-cdk/aws-cloudwatch'; import { Stack } from '@aws-cdk/core'; import { FilterPattern, LogGroup, MetricFilter } from '../lib'; @@ -19,7 +19,7 @@ describe('metric filter', () => { }); // THEN - expect(stack).toHaveResource('AWS::Logs::MetricFilter', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::MetricFilter', { MetricTransformations: [{ MetricNamespace: 'AWS/Test', MetricName: 'Latency', @@ -28,8 +28,6 @@ describe('metric filter', () => { FilterPattern: '{ $.latency = "*" }', LogGroupName: { Ref: 'LogGroupF5B46931' }, }); - - }); test('metric filter exposes metric', () => { @@ -54,8 +52,6 @@ describe('metric filter', () => { namespace: 'AWS/Test', statistic: 'avg', })); - - }); test('metric filter exposes metric with custom statistic', () => { @@ -80,7 +76,5 @@ describe('metric filter', () => { namespace: 'AWS/Test', statistic: 'maximum', })); - - }); }); diff --git a/packages/@aws-cdk/aws-logs/test/pattern.test.ts b/packages/@aws-cdk/aws-logs/test/pattern.test.ts index 848c3ef824aa7..b49c9cd220588 100644 --- a/packages/@aws-cdk/aws-logs/test/pattern.test.ts +++ b/packages/@aws-cdk/aws-logs/test/pattern.test.ts @@ -6,16 +6,12 @@ describe('pattern', () => { const pattern = FilterPattern.allTerms('foo', 'bar', 'baz'); expect('"foo" "bar" "baz"').toEqual(pattern.logPatternString); - - }); test('quoted terms', () => { const pattern = FilterPattern.allTerms('"foo" he said'); expect('"\\"foo\\" he said"').toEqual(pattern.logPatternString); - - }); test('disjunction of conjunctions', () => { @@ -25,8 +21,6 @@ describe('pattern', () => { ); expect('?"foo" "bar" ?"baz"').toEqual(pattern.logPatternString); - - }); test('dont prefix with ? if only one disjunction', () => { @@ -35,16 +29,12 @@ describe('pattern', () => { ); expect('"foo" "bar"').toEqual(pattern.logPatternString); - - }); test('empty log pattern is empty string', () => { const pattern = FilterPattern.anyTermGroup(); expect('').toEqual(pattern.logPatternString); - - }); }); @@ -53,24 +43,18 @@ describe('pattern', () => { const pattern = FilterPattern.stringValue('$.field', '=', 'value'); expect('{ $.field = "value" }').toEqual(pattern.logPatternString); - - }); test('also recognize ==', () => { const pattern = FilterPattern.stringValue('$.field', '==', 'value'); expect('{ $.field = "value" }').toEqual(pattern.logPatternString); - - }); test('number patterns', () => { const pattern = FilterPattern.numberValue('$.field', '<=', 300); expect('{ $.field <= 300 }').toEqual(pattern.logPatternString); - - }); test('combining with AND or OR', () => { @@ -82,8 +66,6 @@ describe('pattern', () => { const orPattern = FilterPattern.any(p1, p2); expect('{ ($.field <= 300) || ($.field = "value") }').toEqual(orPattern.logPatternString); - - }); test('single AND is not wrapped with parens', () => { @@ -92,32 +74,24 @@ describe('pattern', () => { const pattern = FilterPattern.all(p1); expect('{ $.field = "value" }').toEqual(pattern.logPatternString); - - }); test('empty AND is rejected', () => { expect(() => { FilterPattern.all(); }).toThrow(); - - }); test('invalid string operators are rejected', () => { expect(() => { FilterPattern.stringValue('$.field', '<=', 'hello'); }).toThrow(); - - }); test('can test boolean value', () => { const pattern = FilterPattern.booleanValue('$.field', false); expect('{ $.field IS FALSE }').toEqual(pattern.logPatternString); - - }); }); @@ -126,8 +100,6 @@ describe('pattern', () => { const pattern = FilterPattern.spaceDelimited('...', 'status_code', 'bytes'); expect(pattern.logPatternString).toEqual('[..., status_code, bytes]'); - - }); test('add restrictions', () => { @@ -136,16 +108,12 @@ describe('pattern', () => { .whereNumber('status_code', '!=', 403); expect(pattern.logPatternString).toEqual('[..., status_code = "4*" && status_code != 403, bytes]'); - - }); test('cant use more than one ellipsis', () => { expect(() => { FilterPattern.spaceDelimited('...', 'status_code', '...'); }).toThrow(); - - }); }); }); diff --git a/packages/@aws-cdk/aws-logs/test/policy.test.ts b/packages/@aws-cdk/aws-logs/test/policy.test.ts index 4b2684a9957b1..8ffb66ab8eb29 100644 --- a/packages/@aws-cdk/aws-logs/test/policy.test.ts +++ b/packages/@aws-cdk/aws-logs/test/policy.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { PolicyStatement, ServicePrincipal } from '@aws-cdk/aws-iam'; import { Stack } from '@aws-cdk/core'; import { LogGroup, ResourcePolicy } from '../lib'; @@ -16,7 +16,7 @@ describe('resource policy', () => { })); // THEN - expect(stack).toHaveResource('AWS::Logs::ResourcePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::ResourcePolicy', { PolicyName: 'LogGroupPolicy643B329C', PolicyDocument: JSON.stringify({ Statement: [ @@ -45,7 +45,7 @@ describe('resource policy', () => { })); // THEN - expect(stack).toHaveResource('AWS::Logs::ResourcePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::ResourcePolicy', { PolicyName: 'ResourcePolicy', }); }); diff --git a/packages/@aws-cdk/aws-logs/test/subscriptionfilter.test.ts b/packages/@aws-cdk/aws-logs/test/subscriptionfilter.test.ts index 595e4933745ca..bc691e26aaa96 100644 --- a/packages/@aws-cdk/aws-logs/test/subscriptionfilter.test.ts +++ b/packages/@aws-cdk/aws-logs/test/subscriptionfilter.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { FilterPattern, ILogGroup, ILogSubscriptionDestination, LogGroup, SubscriptionFilter } from '../lib'; @@ -17,13 +17,11 @@ describe('subscription filter', () => { }); // THEN - expect(stack).toHaveResource('AWS::Logs::SubscriptionFilter', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::SubscriptionFilter', { DestinationArn: 'arn:bogus', FilterPattern: 'some pattern', LogGroupName: { Ref: 'LogGroupF5B46931' }, }); - - }); }); diff --git a/packages/@aws-cdk/aws-lookoutequipment/package.json b/packages/@aws-cdk/aws-lookoutequipment/package.json index d59dc1960c96c..7218c3f7fd95f 100644 --- a/packages/@aws-cdk/aws-lookoutequipment/package.json +++ b/packages/@aws-cdk/aws-lookoutequipment/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-lookoutequipment", "module": "aws_cdk.aws_lookoutequipment" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-lookoutmetrics/package.json b/packages/@aws-cdk/aws-lookoutmetrics/package.json index fcafae216f42b..bce07b4f9fed7 100644 --- a/packages/@aws-cdk/aws-lookoutmetrics/package.json +++ b/packages/@aws-cdk/aws-lookoutmetrics/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-lookoutmetrics", "module": "aws_cdk.aws_lookoutmetrics" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-lookoutvision/package.json b/packages/@aws-cdk/aws-lookoutvision/package.json index fd64ea05542dd..f24b9b3a26db1 100644 --- a/packages/@aws-cdk/aws-lookoutvision/package.json +++ b/packages/@aws-cdk/aws-lookoutvision/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-lookoutvision", "module": "aws_cdk.aws_lookoutvision" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-macie/package.json b/packages/@aws-cdk/aws-macie/package.json index c435dfbe95e6a..c24dd63ebd1ba 100644 --- a/packages/@aws-cdk/aws-macie/package.json +++ b/packages/@aws-cdk/aws-macie/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-managedblockchain/package.json b/packages/@aws-cdk/aws-managedblockchain/package.json index cbea43ff7e7ed..9190b3c3b9955 100644 --- a/packages/@aws-cdk/aws-managedblockchain/package.json +++ b/packages/@aws-cdk/aws-managedblockchain/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-mediaconnect/package.json b/packages/@aws-cdk/aws-mediaconnect/package.json index 29d305f1c45b4..5a67e0a1c0942 100644 --- a/packages/@aws-cdk/aws-mediaconnect/package.json +++ b/packages/@aws-cdk/aws-mediaconnect/package.json @@ -28,6 +28,13 @@ "distName": "aws-cdk.aws-mediaconnect", "module": "aws_cdk.aws_mediaconnect" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-mediaconvert/package.json b/packages/@aws-cdk/aws-mediaconvert/package.json index be49a311a7951..637617e65843e 100644 --- a/packages/@aws-cdk/aws-mediaconvert/package.json +++ b/packages/@aws-cdk/aws-mediaconvert/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-medialive/package.json b/packages/@aws-cdk/aws-medialive/package.json index b1e3ca8e9260a..dbe092b4b0dfb 100644 --- a/packages/@aws-cdk/aws-medialive/package.json +++ b/packages/@aws-cdk/aws-medialive/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-mediapackage/package.json b/packages/@aws-cdk/aws-mediapackage/package.json index 7630c6c3f9338..23f112ba5ae80 100644 --- a/packages/@aws-cdk/aws-mediapackage/package.json +++ b/packages/@aws-cdk/aws-mediapackage/package.json @@ -7,6 +7,13 @@ "jsii": { "outdir": "dist", "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + }, "targets": { "dotnet": { "namespace": "Amazon.CDK.AWS.MediaPackage", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-mediastore/package.json b/packages/@aws-cdk/aws-mediastore/package.json index 03e130c867a4c..73dc4e62f8961 100644 --- a/packages/@aws-cdk/aws-mediastore/package.json +++ b/packages/@aws-cdk/aws-mediastore/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-memorydb/package.json b/packages/@aws-cdk/aws-memorydb/package.json index 2d4c661126fb2..5db59ada0d550 100644 --- a/packages/@aws-cdk/aws-memorydb/package.json +++ b/packages/@aws-cdk/aws-memorydb/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-memorydb", "module": "aws_cdk.aws_memorydb" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -81,7 +88,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-msk/README.md b/packages/@aws-cdk/aws-msk/README.md index 664ec4f66c973..36c85154c8d04 100644 --- a/packages/@aws-cdk/aws-msk/README.md +++ b/packages/@aws-cdk/aws-msk/README.md @@ -26,9 +26,9 @@ The following example creates an MSK Cluster. ```ts -import * as msk from '@aws-cdk/aws-msk'; - -const cluster = new Cluster(this, 'Cluster', { +declare const vpc: ec2.Vpc; +const cluster = new msk.Cluster(this, 'Cluster', { + clusterName: 'myCluster', kafkaVersion: msk.KafkaVersion.V2_8_1, vpc, }); @@ -38,40 +38,45 @@ const cluster = new Cluster(this, 'Cluster', { To control who can access the Cluster, use the `.connections` attribute. For a list of ports used by MSK, refer to the [MSK documentation](https://docs.aws.amazon.com/msk/latest/developerguide/client-access.html#port-info). -```typescript -import * as msk from "@aws-cdk/aws-msk" -import * as ec2 from "@aws-cdk/aws-ec2" - -const cluster = new msk.Cluster(this, "Cluster", {...}) +```ts +declare const vpc: ec2.Vpc; +const cluster = new msk.Cluster(this, 'Cluster', { + clusterName: 'myCluster', + kafkaVersion: msk.KafkaVersion.V2_8_1, + vpc, +}); cluster.connections.allowFrom( - ec2.Peer.ipv4("1.2.3.4/8"), - ec2.Port.tcp(2181) -) + ec2.Peer.ipv4('1.2.3.4/8'), + ec2.Port.tcp(2181), +); cluster.connections.allowFrom( - ec2.Peer.ipv4("1.2.3.4/8"), - ec2.Port.tcp(9094) -) + ec2.Peer.ipv4('1.2.3.4/8'), + ec2.Port.tcp(9094), +); ``` ## Cluster Endpoints You can use the following attributes to get a list of the Kafka broker or ZooKeeper node endpoints -```typescript -new cdk.CfnOutput(this, 'BootstrapBrokers', { value: cluster.bootstrapBrokers }); -new cdk.CfnOutput(this, 'BootstrapBrokersTls', { value: cluster.bootstrapBrokersTls }); -new cdk.CfnOutput(this, 'BootstrapBrokersSaslScram', { value: cluster.bootstrapBrokersSaslScram }); -new cdk.CfnOutput(this, 'ZookeeperConnection', { value: cluster.zookeeperConnectionString }); -new cdk.CfnOutput(this, 'ZookeeperConnectionTls', { value: cluster.zookeeperConnectionStringTls }); +```ts +declare const cluster: msk.Cluster; +new CfnOutput(this, 'BootstrapBrokers', { value: cluster.bootstrapBrokers }); +new CfnOutput(this, 'BootstrapBrokersTls', { value: cluster.bootstrapBrokersTls }); +new CfnOutput(this, 'BootstrapBrokersSaslScram', { value: cluster.bootstrapBrokersSaslScram }); +new CfnOutput(this, 'ZookeeperConnection', { value: cluster.zookeeperConnectionString }); +new CfnOutput(this, 'ZookeeperConnectionTls', { value: cluster.zookeeperConnectionStringTls }); ``` ## Importing an existing Cluster To import an existing MSK cluster into your CDK app use the `.fromClusterArn()` method. -```typescript -const cluster = msk.Cluster.fromClusterArn(this, 'Cluster', 'arn:aws:kafka:us-west-2:1234567890:cluster/a-cluster/11111111-1111-1111-1111-111111111111-1') +```ts +const cluster = msk.Cluster.fromClusterArn(this, 'Cluster', + 'arn:aws:kafka:us-west-2:1234567890:cluster/a-cluster/11111111-1111-1111-1111-111111111111-1', +); ``` ## Client Authentication @@ -84,25 +89,26 @@ const cluster = msk.Cluster.fromClusterArn(this, 'Cluster', 'arn:aws:kafka:us-we To enable client authentication with TLS set the `certificateAuthorityArns` property to reference your ACM Private CA. [More info on Private CAs.](https://docs.aws.amazon.com/msk/latest/developerguide/msk-authentication.html) -```typescript -import * as msk from "@aws-cdk/aws-msk" -import * as acmpca from "@aws-cdk/aws-acmpca" +```ts +import * as acmpca from '@aws-cdk/aws-acmpca'; +declare const vpc: ec2.Vpc; const cluster = new msk.Cluster(this, 'Cluster', { - ... - encryptionInTransit: { - clientBroker: msk.ClientBrokerEncryption.TLS, - }, - clientAuthentication: msk.ClientAuthentication.tls({ - certificateAuthorities: [ - acmpca.CertificateAuthority.fromCertificateAuthorityArn( - stack, - "CertificateAuthority", - "arn:aws:acm-pca:us-west-2:1234567890:certificate-authority/11111111-1111-1111-1111-111111111111" - ), - ], - }), - }); + clusterName: 'myCluster', + kafkaVersion: msk.KafkaVersion.V2_8_1, + vpc, + encryptionInTransit: { + clientBroker: msk.ClientBrokerEncryption.TLS, + }, + clientAuthentication: msk.ClientAuthentication.tls({ + certificateAuthorities: [ + acmpca.CertificateAuthority.fromCertificateAuthorityArn( + this, + 'CertificateAuthority', + 'arn:aws:acm-pca:us-west-2:1234567890:certificate-authority/11111111-1111-1111-1111-111111111111', + ), + ], + }), }); ``` @@ -110,34 +116,36 @@ const cluster = new msk.Cluster(this, 'Cluster', { Enable client authentication with [SASL/SCRAM](https://docs.aws.amazon.com/msk/latest/developerguide/msk-password.html): -```typescript -import * as msk from "@aws-cdk/aws-msk" - -const cluster = new msk.cluster(this, "cluster", { - ... +```ts +declare const vpc: ec2.Vpc; +const cluster = new msk.Cluster(this, 'cluster', { + clusterName: 'myCluster', + kafkaVersion: msk.KafkaVersion.V2_8_1, + vpc, encryptionInTransit: { clientBroker: msk.ClientBrokerEncryption.TLS, }, clientAuthentication: msk.ClientAuthentication.sasl({ scram: true, }), -}) +}); ``` ### SASL/IAM Enable client authentication with [IAM](https://docs.aws.amazon.com/msk/latest/developerguide/iam-access-control.html): -```typescript -import * as msk from "@aws-cdk/aws-msk" - -const cluster = new msk.cluster(this, "cluster", { - ... +```ts +declare const vpc: ec2.Vpc; +const cluster = new msk.Cluster(this, 'cluster', { + clusterName: 'myCluster', + kafkaVersion: msk.KafkaVersion.V2_8_1, + vpc, encryptionInTransit: { clientBroker: msk.ClientBrokerEncryption.TLS, }, clientAuthentication: msk.ClientAuthentication.sasl({ iam: true, }), -}) +}); ``` diff --git a/packages/@aws-cdk/aws-msk/package.json b/packages/@aws-cdk/aws-msk/package.json index 70978b758f5e0..e175174ae9da6 100644 --- a/packages/@aws-cdk/aws-msk/package.json +++ b/packages/@aws-cdk/aws-msk/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -79,8 +86,8 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-acmpca": "0.0.0", diff --git a/packages/@aws-cdk/aws-msk/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-msk/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..e6009423c7553 --- /dev/null +++ b/packages/@aws-cdk/aws-msk/rosetta/default.ts-fixture @@ -0,0 +1,12 @@ +// Fixture with packages imported, but nothing else +import { CfnOutput, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as msk from '@aws-cdk/aws-msk'; +import * as ec2 from '@aws-cdk/aws-ec2'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-mwaa/package.json b/packages/@aws-cdk/aws-mwaa/package.json index 9fdf1cc3689db..ab2d6061c871b 100644 --- a/packages/@aws-cdk/aws-mwaa/package.json +++ b/packages/@aws-cdk/aws-mwaa/package.json @@ -28,6 +28,13 @@ "distName": "aws-cdk.aws-mwaa", "module": "aws_cdk.aws_mwaa" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-neptune/README.md b/packages/@aws-cdk/aws-neptune/README.md index 57ecabab058be..77f50a7f6192d 100644 --- a/packages/@aws-cdk/aws-neptune/README.md +++ b/packages/@aws-cdk/aws-neptune/README.md @@ -36,7 +36,7 @@ To set up a Neptune database, define a `DatabaseCluster`. You must always launch ```ts const cluster = new neptune.DatabaseCluster(this, 'Database', { vpc, - instanceType: neptune.InstanceType.R5_LARGE + instanceType: neptune.InstanceType.R5_LARGE, }); ``` @@ -92,7 +92,7 @@ const clusterParams = new neptune.ClusterParameterGroup(this, 'ClusterParams', { const dbParams = new neptune.ParameterGroup(this, 'DbParams', { description: 'Db parameter group', parameters: { - neptune_query_timeout: '120000' + neptune_query_timeout: '120000', }, }); @@ -113,7 +113,7 @@ attribute. const cluster = new neptune.DatabaseCluster(this, 'Database', { vpc, instanceType: neptune.InstanceType.R5_LARGE, - instances: 2 + instances: 2, }); ``` @@ -122,6 +122,20 @@ Additionally it is also possible to add replicas using `DatabaseInstance` for an ```ts fixture=with-cluster const replica1 = new neptune.DatabaseInstance(this, 'Instance', { cluster, - instanceType: neptune.InstanceType.R5_LARGE + instanceType: neptune.InstanceType.R5_LARGE, +}); +``` + +## Automatic minor version upgrades + +By setting `autoMinorVersionUpgrade` to true, Neptune will automatically update +the engine of the entire cluster to the latest minor version after a stabilization +window of 2 to 3 weeks. + +```ts +new neptune.DatabaseCluster(this, 'Cluster', { + vpc, + instanceType: neptune.InstanceType.R5_LARGE, + autoMinorVersionUpgrade: true, }); ``` diff --git a/packages/@aws-cdk/aws-neptune/lib/cluster.ts b/packages/@aws-cdk/aws-neptune/lib/cluster.ts index cf5245d33c476..2a33f4e4629fa 100644 --- a/packages/@aws-cdk/aws-neptune/lib/cluster.ts +++ b/packages/@aws-cdk/aws-neptune/lib/cluster.ts @@ -226,6 +226,14 @@ export interface DatabaseClusterProps { * @default - Retain cluster. */ readonly removalPolicy?: RemovalPolicy + + /** + * If set to true, Neptune will automatically update the engine of the entire + * cluster to the latest minor version after a stabilization window of 2 to 3 weeks. + * + * @default - false + */ + readonly autoMinorVersionUpgrade?: boolean; } /** @@ -513,6 +521,7 @@ export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseClu // Instance properties dbInstanceClass: props.instanceType._instanceType, dbParameterGroupName: props.parameterGroup?.parameterGroupName, + autoMinorVersionUpgrade: props.autoMinorVersionUpgrade === true, }); // We must have a dependency on the NAT gateway provider here to create diff --git a/packages/@aws-cdk/aws-neptune/package.json b/packages/@aws-cdk/aws-neptune/package.json index 9ea7c1dc27705..73b0ac6867a50 100644 --- a/packages/@aws-cdk/aws-neptune/package.json +++ b/packages/@aws-cdk/aws-neptune/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-ec2": "0.0.0", diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts index f53af7911dc6c..e4318d5521028 100644 --- a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts @@ -523,6 +523,46 @@ describe('DatabaseCluster', () => { // THEN expect(() => { cluster.grantConnect(role); }).toThrow(/Cannot grant connect when IAM authentication is disabled/); }); + + test('autoMinorVersionUpgrade is enabled when configured', () => { + + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Cluster', { + vpc, + instanceType: InstanceType.R5_LARGE, + autoMinorVersionUpgrade: true, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBInstance', { + AutoMinorVersionUpgrade: true, + }); + + }); + + test('autoMinorVersionUpgrade is not enabled when not configured', () => { + + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Cluster', { + vpc, + instanceType: InstanceType.R5_LARGE, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBInstance', { + AutoMinorVersionUpgrade: false, + }); + + }); + }); function testStack() { diff --git a/packages/@aws-cdk/aws-neptune/test/integ.cluster.expected.json b/packages/@aws-cdk/aws-neptune/test/integ.cluster.expected.json index 823f7af2a5b45..36a69ceec62c9 100644 --- a/packages/@aws-cdk/aws-neptune/test/integ.cluster.expected.json +++ b/packages/@aws-cdk/aws-neptune/test/integ.cluster.expected.json @@ -491,7 +491,8 @@ "DBInstanceClass": "db.r5.large", "DBClusterIdentifier": { "Ref": "DatabaseB269D8BB" - } + }, + "AutoMinorVersionUpgrade": true }, "DependsOn": [ "VPCPrivateSubnet1DefaultRouteAE1D6490", diff --git a/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts b/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts index b62c0d054a624..2a29ebe2cc1c2 100644 --- a/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts +++ b/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts @@ -35,6 +35,7 @@ class TestStack extends cdk.Stack { clusterParameterGroup: params, kmsKey, removalPolicy: cdk.RemovalPolicy.DESTROY, + autoMinorVersionUpgrade: true, }); cluster.connections.allowDefaultPortFromAnyIpv4('Open to the world'); diff --git a/packages/@aws-cdk/aws-networkfirewall/package.json b/packages/@aws-cdk/aws-networkfirewall/package.json index 795561d0abf28..937d63aa53730 100644 --- a/packages/@aws-cdk/aws-networkfirewall/package.json +++ b/packages/@aws-cdk/aws-networkfirewall/package.json @@ -28,6 +28,13 @@ "distName": "aws-cdk.aws-networkfirewall", "module": "aws_cdk.aws_networkfirewall" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-networkmanager/package.json b/packages/@aws-cdk/aws-networkmanager/package.json index 9b0252be4cdd5..c34d778bac829 100644 --- a/packages/@aws-cdk/aws-networkmanager/package.json +++ b/packages/@aws-cdk/aws-networkmanager/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-nimblestudio/package.json b/packages/@aws-cdk/aws-nimblestudio/package.json index 4b489613e5b9a..52ab33b43c1ca 100644 --- a/packages/@aws-cdk/aws-nimblestudio/package.json +++ b/packages/@aws-cdk/aws-nimblestudio/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-nimblestudio", "module": "aws_cdk.aws_nimblestudio" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-opensearchservice/lib/version.ts b/packages/@aws-cdk/aws-opensearchservice/lib/version.ts index f170504c17338..119b7844502dc 100644 --- a/packages/@aws-cdk/aws-opensearchservice/lib/version.ts +++ b/packages/@aws-cdk/aws-opensearchservice/lib/version.ts @@ -62,6 +62,9 @@ export class EngineVersion { /** AWS OpenSearch 1.0 */ public static readonly OPENSEARCH_1_0 = EngineVersion.openSearch('1.0'); + /** AWS OpenSearch 1.1 */ + public static readonly OPENSEARCH_1_1 = EngineVersion.openSearch('1.1'); + /** * Custom ElasticSearch version * @param version custom version number diff --git a/packages/@aws-cdk/aws-opensearchservice/package.json b/packages/@aws-cdk/aws-opensearchservice/package.json index cb7bea7e5e0b4..1bece17b44f7a 100644 --- a/packages/@aws-cdk/aws-opensearchservice/package.json +++ b/packages/@aws-cdk/aws-opensearchservice/package.json @@ -84,12 +84,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-certificatemanager": "0.0.0", diff --git a/packages/@aws-cdk/aws-opensearchservice/test/domain.test.ts b/packages/@aws-cdk/aws-opensearchservice/test/domain.test.ts index 2389c30ab7c29..3739031d6f3b2 100644 --- a/packages/@aws-cdk/aws-opensearchservice/test/domain.test.ts +++ b/packages/@aws-cdk/aws-opensearchservice/test/domain.test.ts @@ -1,6 +1,5 @@ /* eslint-disable jest/expect-expect */ -import '@aws-cdk/assert-internal/jest'; -import * as assert from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as acm from '@aws-cdk/aws-certificatemanager'; import { Metric, Statistic } from '@aws-cdk/aws-cloudwatch'; import { Vpc, EbsDeviceVolumeType, SecurityGroup } from '@aws-cdk/aws-ec2'; @@ -55,7 +54,7 @@ test('subnets and security groups can be provided when vpc is used', () => { }); expect(domain.connections.securityGroups[0].securityGroupId).toEqual(securityGroup.securityGroupId); - expect(stack).toHaveResource('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { VPCOptions: { SecurityGroupIds: [ { @@ -84,7 +83,7 @@ test('default subnets and security group when vpc is used', () => { }); expect(stack.resolve(domain.connections.securityGroups[0].securityGroupId)).toEqual({ 'Fn::GetAtt': ['DomainSecurityGroup48AA5FD6', 'GroupId'] }); - expect(stack).toHaveResource('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { VPCOptions: { SecurityGroupIds: [ { @@ -115,9 +114,9 @@ test('default removalpolicy is retain', () => { version: defaultVersion, }); - expect(stack).toHaveResource('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResource('AWS::OpenSearchService::Domain', { DeletionPolicy: 'Retain', - }, assert.ResourcePart.CompleteDefinition); + }); }); test('grants kms permissions if needed', () => { @@ -153,7 +152,7 @@ test('grants kms permissions if needed', () => { Version: '2012-10-17', }; - const resources = assert.expect(stack).value.Resources; + const resources = Template.fromStack(stack).toJSON().Resources; expect(resources.AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E.Properties.PolicyDocument).toStrictEqual(expectedPolicy); }); @@ -161,7 +160,7 @@ test('grants kms permissions if needed', () => { test('minimal example renders correctly', () => { new Domain(stack, 'Domain', { version: defaultVersion }); - expect(stack).toHaveResource('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { CognitoOptions: { Enabled: false, }, @@ -181,10 +180,10 @@ test('minimal example renders correctly', () => { Enabled: false, }, LogPublishingOptions: { - AUDIT_LOGS: assert.ABSENT, - ES_APPLICATION_LOGS: assert.ABSENT, - SEARCH_SLOW_LOGS: assert.ABSENT, - INDEX_SLOW_LOGS: assert.ABSENT, + AUDIT_LOGS: Match.absent(), + ES_APPLICATION_LOGS: Match.absent(), + SEARCH_SLOW_LOGS: Match.absent(), + INDEX_SLOW_LOGS: Match.absent(), }, NodeToNodeEncryptionOptions: { Enabled: false, @@ -198,11 +197,11 @@ test('can enable version upgrade update policy', () => { enableVersionUpgrade: true, }); - expect(stack).toHaveResource('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResource('AWS::OpenSearchService::Domain', { UpdatePolicy: { EnableVersionUpgrade: true, }, - }, assert.ResourcePart.CompleteDefinition); + }); }); describe('UltraWarm instances', () => { @@ -216,7 +215,7 @@ describe('UltraWarm instances', () => { }, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { ClusterConfig: { DedicatedMasterEnabled: true, WarmEnabled: true, @@ -236,7 +235,7 @@ describe('UltraWarm instances', () => { }, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { ClusterConfig: { DedicatedMasterEnabled: true, WarmEnabled: true, @@ -261,7 +260,7 @@ test('can use tokens in capacity configuration', () => { }, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { ClusterConfig: { InstanceCount: { Ref: 'dataNodes', @@ -297,7 +296,7 @@ describe('log groups', () => { }, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { LogPublishingOptions: { SEARCH_SLOW_LOGS: { CloudWatchLogsLogGroupArn: { @@ -308,9 +307,9 @@ describe('log groups', () => { }, Enabled: true, }, - AUDIT_LOGS: assert.ABSENT, - ES_APPLICATION_LOGS: assert.ABSENT, - INDEX_SLOW_LOGS: assert.ABSENT, + AUDIT_LOGS: Match.absent(), + ES_APPLICATION_LOGS: Match.absent(), + INDEX_SLOW_LOGS: Match.absent(), }, }); }); @@ -323,7 +322,7 @@ describe('log groups', () => { }, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { LogPublishingOptions: { INDEX_SLOW_LOGS: { CloudWatchLogsLogGroupArn: { @@ -334,9 +333,9 @@ describe('log groups', () => { }, Enabled: true, }, - AUDIT_LOGS: assert.ABSENT, - ES_APPLICATION_LOGS: assert.ABSENT, - SEARCH_SLOW_LOGS: assert.ABSENT, + AUDIT_LOGS: Match.absent(), + ES_APPLICATION_LOGS: Match.absent(), + SEARCH_SLOW_LOGS: Match.absent(), }, }); }); @@ -349,7 +348,7 @@ describe('log groups', () => { }, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { LogPublishingOptions: { ES_APPLICATION_LOGS: { CloudWatchLogsLogGroupArn: { @@ -360,9 +359,9 @@ describe('log groups', () => { }, Enabled: true, }, - AUDIT_LOGS: assert.ABSENT, - SEARCH_SLOW_LOGS: assert.ABSENT, - INDEX_SLOW_LOGS: assert.ABSENT, + AUDIT_LOGS: Match.absent(), + SEARCH_SLOW_LOGS: Match.absent(), + INDEX_SLOW_LOGS: Match.absent(), }, }); }); @@ -383,7 +382,7 @@ describe('log groups', () => { enforceHttps: true, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { LogPublishingOptions: { AUDIT_LOGS: { CloudWatchLogsLogGroupArn: { @@ -394,9 +393,9 @@ describe('log groups', () => { }, Enabled: true, }, - ES_APPLICATION_LOGS: assert.ABSENT, - SEARCH_SLOW_LOGS: assert.ABSENT, - INDEX_SLOW_LOGS: assert.ABSENT, + ES_APPLICATION_LOGS: Match.absent(), + SEARCH_SLOW_LOGS: Match.absent(), + INDEX_SLOW_LOGS: Match.absent(), }, }); }); @@ -418,7 +417,7 @@ describe('log groups', () => { slowIndexLogEnabled: true, }, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { LogPublishingOptions: { ES_APPLICATION_LOGS: { CloudWatchLogsLogGroupArn: { @@ -447,10 +446,10 @@ describe('log groups', () => { }, Enabled: true, }, - AUDIT_LOGS: assert.ABSENT, + AUDIT_LOGS: Match.absent(), }, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { LogPublishingOptions: { ES_APPLICATION_LOGS: { CloudWatchLogsLogGroupArn: { @@ -479,7 +478,7 @@ describe('log groups', () => { }, Enabled: true, }, - AUDIT_LOGS: assert.ABSENT, + AUDIT_LOGS: Match.absent(), }, }); }); @@ -499,7 +498,7 @@ describe('log groups', () => { }); // Domain1 - expect(stack).toHaveResourceLike('Custom::CloudwatchLogResourcePolicy', { + Template.fromStack(stack).hasResourceProperties('Custom::CloudwatchLogResourcePolicy', { Create: { 'Fn::Join': [ '', @@ -517,7 +516,7 @@ describe('log groups', () => { }, }); // Domain2 - expect(stack).toHaveResourceLike('Custom::CloudwatchLogResourcePolicy', { + Template.fromStack(stack).hasResourceProperties('Custom::CloudwatchLogResourcePolicy', { Create: { 'Fn::Join': [ '', @@ -556,7 +555,7 @@ describe('log groups', () => { }, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { LogPublishingOptions: { SEARCH_SLOW_LOGS: { CloudWatchLogsLogGroupArn: { @@ -567,9 +566,9 @@ describe('log groups', () => { }, Enabled: true, }, - AUDIT_LOGS: assert.ABSENT, - ES_APPLICATION_LOGS: assert.ABSENT, - INDEX_SLOW_LOGS: assert.ABSENT, + AUDIT_LOGS: Match.absent(), + ES_APPLICATION_LOGS: Match.absent(), + INDEX_SLOW_LOGS: Match.absent(), }, }); }); @@ -585,7 +584,7 @@ describe('log groups', () => { }, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { LogPublishingOptions: { INDEX_SLOW_LOGS: { CloudWatchLogsLogGroupArn: { @@ -596,9 +595,9 @@ describe('log groups', () => { }, Enabled: true, }, - AUDIT_LOGS: assert.ABSENT, - ES_APPLICATION_LOGS: assert.ABSENT, - SEARCH_SLOW_LOGS: assert.ABSENT, + AUDIT_LOGS: Match.absent(), + ES_APPLICATION_LOGS: Match.absent(), + SEARCH_SLOW_LOGS: Match.absent(), }, }); }); @@ -614,7 +613,7 @@ describe('log groups', () => { }, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { LogPublishingOptions: { ES_APPLICATION_LOGS: { CloudWatchLogsLogGroupArn: { @@ -625,9 +624,9 @@ describe('log groups', () => { }, Enabled: true, }, - AUDIT_LOGS: assert.ABSENT, - SEARCH_SLOW_LOGS: assert.ABSENT, - INDEX_SLOW_LOGS: assert.ABSENT, + AUDIT_LOGS: Match.absent(), + SEARCH_SLOW_LOGS: Match.absent(), + INDEX_SLOW_LOGS: Match.absent(), }, }); }); @@ -651,7 +650,7 @@ describe('log groups', () => { }, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { LogPublishingOptions: { AUDIT_LOGS: { CloudWatchLogsLogGroupArn: { @@ -662,9 +661,9 @@ describe('log groups', () => { }, Enabled: true, }, - ES_APPLICATION_LOGS: assert.ABSENT, - SEARCH_SLOW_LOGS: assert.ABSENT, - INDEX_SLOW_LOGS: assert.ABSENT, + ES_APPLICATION_LOGS: Match.absent(), + SEARCH_SLOW_LOGS: Match.absent(), + INDEX_SLOW_LOGS: Match.absent(), }, }); }); @@ -746,7 +745,7 @@ describe('grants', () => { domain.grantReadWrite(user); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -938,7 +937,7 @@ describe('import', () => { expect(imported.domainArn).toMatch(RegExp(`es:testregion:1234:domain/${domainName}$`)); expect(imported.domainEndpoint).toEqual(domainEndpointWithoutHttps); - expect(stack).not.toHaveResource('AWS::OpenSearchService::Domain'); + Template.fromStack(stack).resourceCountIs('AWS::OpenSearchService::Domain', 0); }); test('static fromDomainAttributes(attributes) allows importing an external/existing domain', () => { @@ -955,7 +954,7 @@ describe('import', () => { expect(imported.domainArn).toEqual(domainArn); expect(imported.domainEndpoint).toEqual(domainEndpointWithoutHttps); - expect(stack).not.toHaveResource('AWS::OpenSearchService::Domain'); + Template.fromStack(stack).resourceCountIs('AWS::OpenSearchService::Domain', 0); }); test('static fromDomainAttributes(attributes) allows importing with token arn and endpoint', () => { @@ -993,7 +992,7 @@ describe('import', () => { expect(imported.domainArn).toEqual(domainArn); expect(imported.domainEndpoint).toEqual(domainEndpoint); - expect(stack).not.toHaveResource('AWS::OpenSearchService::Domain'); + Template.fromStack(stack).resourceCountIs('AWS::OpenSearchService::Domain', 0); }); }); @@ -1016,7 +1015,7 @@ describe('advanced security options', () => { enforceHttps: true, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { AdvancedSecurityOptions: { Enabled: true, InternalUserDatabaseEnabled: false, @@ -1050,7 +1049,7 @@ describe('advanced security options', () => { enforceHttps: true, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { AdvancedSecurityOptions: { Enabled: true, InternalUserDatabaseEnabled: true, @@ -1084,7 +1083,7 @@ describe('advanced security options', () => { enforceHttps: true, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { AdvancedSecurityOptions: { Enabled: true, InternalUserDatabaseEnabled: true, @@ -1115,7 +1114,7 @@ describe('advanced security options', () => { }, }); - expect(stack).toHaveResourceLike('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { GenerateSecretString: { GenerateStringKey: 'password', }, @@ -1192,7 +1191,7 @@ describe('custom endpoints', () => { }, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { DomainEndpointOptions: { EnforceHTTPS: true, CustomEndpointEnabled: true, @@ -1202,7 +1201,7 @@ describe('custom endpoints', () => { }, }, }); - expect(stack).toHaveResourceLike('AWS::CertificateManager::Certificate', { + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { DomainName: customDomainName, ValidationMethod: 'EMAIL', }); @@ -1220,7 +1219,7 @@ describe('custom endpoints', () => { }, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { DomainEndpointOptions: { EnforceHTTPS: true, CustomEndpointEnabled: true, @@ -1230,7 +1229,7 @@ describe('custom endpoints', () => { }, }, }); - expect(stack).toHaveResourceLike('AWS::CertificateManager::Certificate', { + Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { DomainName: customDomainName, DomainValidationOptions: [ { @@ -1242,7 +1241,7 @@ describe('custom endpoints', () => { ], ValidationMethod: 'DNS', }); - expect(stack).toHaveResourceLike('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'search.example.com.', Type: 'CNAME', HostedZoneId: { @@ -1278,7 +1277,7 @@ describe('custom endpoints', () => { }, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { DomainEndpointOptions: { EnforceHTTPS: true, CustomEndpointEnabled: true, @@ -1288,7 +1287,7 @@ describe('custom endpoints', () => { }, }, }); - expect(stack).toHaveResourceLike('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'search.example.com.', Type: 'CNAME', HostedZoneId: { @@ -1550,7 +1549,7 @@ describe('custom error responses', () => { }, }); // both configurations pass synth-time validation - expect(stack).toCountResources('AWS::OpenSearchService::Domain', 2); + Template.fromStack(stack).resourceCountIs('AWS::OpenSearchService::Domain', 2); }); test('error when availabilityZoneCount is not 2 or 3', () => { @@ -1608,7 +1607,7 @@ describe('custom error responses', () => { test('can specify future version', () => { new Domain(stack, 'Domain', { version: EngineVersion.elasticsearch('8.2') }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { EngineVersion: 'Elasticsearch_8.2', }); }); @@ -1620,7 +1619,7 @@ describe('unsigned basic auth', () => { useUnsignedBasicAuth: true, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { AdvancedSecurityOptions: { Enabled: true, InternalUserDatabaseEnabled: true, @@ -1651,7 +1650,7 @@ describe('unsigned basic auth', () => { useUnsignedBasicAuth: true, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { AdvancedSecurityOptions: { Enabled: true, InternalUserDatabaseEnabled: false, @@ -1685,7 +1684,7 @@ describe('unsigned basic auth', () => { useUnsignedBasicAuth: true, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { AdvancedSecurityOptions: { Enabled: true, InternalUserDatabaseEnabled: true, @@ -1748,7 +1747,7 @@ describe('advanced options', () => { }, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { AdvancedOptions: { 'rest.action.multi.allow_explicit_index': 'true', 'indices.fielddata.cache.size': '50', @@ -1761,8 +1760,8 @@ describe('advanced options', () => { version: defaultVersion, }); - expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { - AdvancedOptions: assert.ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { + AdvancedOptions: Match.absent(), }); }); }); @@ -1802,7 +1801,7 @@ function testGrant( ? resolvedPaths : resolvedPaths[0]; - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { diff --git a/packages/@aws-cdk/aws-opensearchservice/test/log-group-resource-policy.test.ts b/packages/@aws-cdk/aws-opensearchservice/test/log-group-resource-policy.test.ts index 68518297588c9..815c0086d4a99 100644 --- a/packages/@aws-cdk/aws-opensearchservice/test/log-group-resource-policy.test.ts +++ b/packages/@aws-cdk/aws-opensearchservice/test/log-group-resource-policy.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import { App, Stack } from '@aws-cdk/core'; import { LogGroupResourcePolicy } from '../lib/log-group-resource-policy'; @@ -24,7 +24,7 @@ test('minimal example renders correctly', () => { })], }); - expect(stack).toHaveResource('Custom::CloudwatchLogResourcePolicy', { + Template.fromStack(stack).hasResourceProperties('Custom::CloudwatchLogResourcePolicy', { ServiceToken: { 'Fn::GetAtt': [ 'AWS679f53fac002430cb0da5b7982bd22872D164C4C', diff --git a/packages/@aws-cdk/aws-opensearchservice/test/opensearch-access-policy.test.ts b/packages/@aws-cdk/aws-opensearchservice/test/opensearch-access-policy.test.ts index 62e4361f76e46..6e449d3521adb 100644 --- a/packages/@aws-cdk/aws-opensearchservice/test/opensearch-access-policy.test.ts +++ b/packages/@aws-cdk/aws-opensearchservice/test/opensearch-access-policy.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import { App, Stack } from '@aws-cdk/core'; import { OpenSearchAccessPolicy } from '../lib/opensearch-access-policy'; @@ -28,7 +28,7 @@ test('minimal example renders correctly', () => { })], }); - expect(stack).toHaveResource('Custom::OpenSearchAccessPolicy', { + Template.fromStack(stack).hasResourceProperties('Custom::OpenSearchAccessPolicy', { ServiceToken: { 'Fn::GetAtt': [ 'AWS679f53fac002430cb0da5b7982bd22872D164C4C', @@ -56,7 +56,7 @@ test('minimal example renders correctly', () => { physicalResourceId: { id: 'TestDomainAccessPolicy' }, }), }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [{ Action: 'es:UpdateDomainConfig', diff --git a/packages/@aws-cdk/aws-opsworks/package.json b/packages/@aws-cdk/aws-opsworks/package.json index 54f88b61a607d..011fa1d4d0790 100644 --- a/packages/@aws-cdk/aws-opsworks/package.json +++ b/packages/@aws-cdk/aws-opsworks/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -76,7 +83,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-opsworkscm/package.json b/packages/@aws-cdk/aws-opsworkscm/package.json index 583647ba83fc3..f429f37979df0 100644 --- a/packages/@aws-cdk/aws-opsworkscm/package.json +++ b/packages/@aws-cdk/aws-opsworkscm/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-panorama/package.json b/packages/@aws-cdk/aws-panorama/package.json index 6ed613253198d..f766e427bbffd 100644 --- a/packages/@aws-cdk/aws-panorama/package.json +++ b/packages/@aws-cdk/aws-panorama/package.json @@ -7,6 +7,13 @@ "jsii": { "outdir": "dist", "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + }, "targets": { "dotnet": { "namespace": "Amazon.CDK.AWS.Panorama", @@ -30,6 +37,13 @@ "distName": "aws-cdk.aws-panorama", "module": "aws_cdk.aws_panorama" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -81,7 +95,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-pinpoint/package.json b/packages/@aws-cdk/aws-pinpoint/package.json index f819b0945b053..b4f9c8565e526 100644 --- a/packages/@aws-cdk/aws-pinpoint/package.json +++ b/packages/@aws-cdk/aws-pinpoint/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-pinpointemail/package.json b/packages/@aws-cdk/aws-pinpointemail/package.json index d8588bf884fca..3c51a3c74abd1 100644 --- a/packages/@aws-cdk/aws-pinpointemail/package.json +++ b/packages/@aws-cdk/aws-pinpointemail/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-qldb/package.json b/packages/@aws-cdk/aws-qldb/package.json index d20ee60754447..9e4e6e0c90ac0 100644 --- a/packages/@aws-cdk/aws-qldb/package.json +++ b/packages/@aws-cdk/aws-qldb/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-quicksight/package.json b/packages/@aws-cdk/aws-quicksight/package.json index 6a591c143a85a..79d41343ab08b 100644 --- a/packages/@aws-cdk/aws-quicksight/package.json +++ b/packages/@aws-cdk/aws-quicksight/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-quicksight", "module": "aws_cdk.aws_quicksight" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-ram/package.json b/packages/@aws-cdk/aws-ram/package.json index 2f4c7a792c293..ddf8049356491 100644 --- a/packages/@aws-cdk/aws-ram/package.json +++ b/packages/@aws-cdk/aws-ram/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index d97acee7ec616..78d58f64041ec 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -527,6 +527,51 @@ new rds.OptionGroup(this, 'Options', { }); ``` +## Parameter Groups + +Database parameters specify how the database is configured. +For example, database parameters can specify the amount of resources, such as memory, to allocate to a database. +You manage your database configuration by associating your DB instances with parameter groups. +Amazon RDS defines parameter groups with default settings. + +You can create your own parameter group for your cluster or instance and associate it with your database: + +```ts +declare const vpc: ec2.Vpc; + +const parameterGroup = new rds.ParameterGroup(this, 'ParameterGroup', { + engine: rds.DatabaseInstanceEngine.sqlServerEe({ + version: rds.SqlServerEngineVersion.VER_11, + }), + parameters: { + locks: '100', + }, +}); + +new rds.DatabaseInstance(this, 'Database', { + engine: rds.DatabaseInstanceEngine.SQL_SERVER_EE, + vpc, + parameterGroup, +}); +``` + +Another way to specify parameters is to use the inline field `parameters` that creates an RDS parameter group for you. +You can use this if you do not want to reuse the parameter group instance for different instances: + +```ts +declare const vpc: ec2.Vpc; + +new rds.DatabaseInstance(this, 'Database', { + engine: rds.DatabaseInstanceEngine.sqlServerEe({ version: rds.SqlServerEngineVersion.VER_11 }), + vpc, + parameters: { + locks: '100', + }, +}); +``` + +You cannot specify a parameter map and a parameter group at the same time. + ## Serverless [Amazon Aurora Serverless](https://aws.amazon.com/rds/aurora/serverless/) is an on-demand, auto-scaling configuration for Amazon @@ -594,7 +639,7 @@ declare const vpc: ec2.Vpc; const cluster = new rds.ServerlessCluster(this, 'AnotherCluster', { engine: rds.DatabaseClusterEngine.AURORA_MYSQL, - vpc, + vpc, // this parameter is optional for serverless Clusters enableDataApi: true, // Optional - will be automatically set if you call grantDataApiAccess() }); @@ -614,3 +659,11 @@ cluster.grantDataApiAccess(fn); **Note**: To invoke the Data API, the resource will need to read the secret associated with the cluster. To learn more about using the Data API, see the [documentation](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html). + +### Default VPC + +The `vpc` parameter is optional. + +If not provided, the cluster will be created in the default VPC of the account and region. +As this VPC is not deployed with AWS CDK, you can't configure the `vpcSubnets`, `subnetGroup` or `securityGroups` of the Aurora Serverless Cluster. +If you want to provide one of `vpcSubnets`, `subnetGroup` or `securityGroups` parameter, please provide a `vpc`. diff --git a/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts b/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts index fa1068025c657..6d7b0d361b4a5 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts @@ -334,10 +334,14 @@ export class AuroraMysqlEngineVersion { public static readonly VER_2_09_1 = AuroraMysqlEngineVersion.builtIn_5_7('2.09.1'); /** Version "5.7.mysql_aurora.2.09.2". */ public static readonly VER_2_09_2 = AuroraMysqlEngineVersion.builtIn_5_7('2.09.2'); + /** Version "5.7.mysql_aurora.2.09.3". */ + public static readonly VER_2_09_3 = AuroraMysqlEngineVersion.builtIn_5_7('2.09.3'); /** Version "5.7.mysql_aurora.2.10.0". */ public static readonly VER_2_10_0 = AuroraMysqlEngineVersion.builtIn_5_7('2.10.0'); /** Version "5.7.mysql_aurora.2.10.1". */ public static readonly VER_2_10_1 = AuroraMysqlEngineVersion.builtIn_5_7('2.10.1'); + /** Version "5.7.mysql_aurora.2.10.2". */ + public static readonly VER_2_10_2 = AuroraMysqlEngineVersion.builtIn_5_7('2.10.2'); /** Version "8.0.mysql_aurora.3.01.0". */ public static readonly VER_3_01_0 = AuroraMysqlEngineVersion.builtIn_8_0('3.01.0'); diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index 7324cacb333c2..72d922d618a36 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -10,7 +10,7 @@ import { Construct } from 'constructs'; import { IClusterEngine } from './cluster-engine'; import { DatabaseClusterAttributes, IDatabaseCluster } from './cluster-ref'; import { Endpoint } from './endpoint'; -import { IParameterGroup } from './parameter-group'; +import { IParameterGroup, ParameterGroup } from './parameter-group'; import { DEFAULT_PASSWORD_EXCLUDE_CHARS, defaultDeletionProtection, renderCredentials, setupS3ImportExport, helperRemovalPolicy, renderUnless } from './private/util'; import { BackupProps, Credentials, InstanceProps, PerformanceInsightRetention, RotationSingleUserOptions, RotationMultiUserOptions } from './props'; import { DatabaseProxy, DatabaseProxyOptions, ProxyTarget } from './proxy'; @@ -116,6 +116,16 @@ interface DatabaseClusterBaseProps { */ readonly parameterGroup?: IParameterGroup; + /** + * The parameters in the DBClusterParameterGroup to create automatically + * + * You can only specify parameterGroup or parameters but not both. + * You need to use a versioned engine to auto-generate a DBClusterParameterGroup. + * + * @default - None + */ + readonly parameters?: { [key: string]: string }; + /** * The removal policy to apply when the cluster and its instances are removed * from the stack or replaced during an update. @@ -338,11 +348,23 @@ abstract class DatabaseClusterNew extends DatabaseClusterBase { ]; let { s3ImportRole, s3ExportRole } = setupS3ImportExport(this, props, /* combineRoles */ false); + + if (props.parameterGroup && props.parameters) { + throw new Error('You cannot specify both parameterGroup and parameters'); + } + const parameterGroup = props.parameterGroup ?? ( + props.parameters + ? new ParameterGroup(this, 'ParameterGroup', { + engine: props.engine, + parameters: props.parameters, + }) + : undefined + ); // bind the engine to the Cluster const clusterEngineBindConfig = props.engine.bindToCluster(this, { s3ImportRole, s3ExportRole, - parameterGroup: props.parameterGroup, + parameterGroup, }); const clusterAssociatedRoles: CfnDBCluster.DBClusterRoleProperty[] = []; @@ -722,7 +744,21 @@ function createInstances(cluster: DatabaseClusterNew, props: DatabaseClusterBase } const instanceType = instanceProps.instanceType ?? ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM); - const instanceParameterGroupConfig = instanceProps.parameterGroup?.bindToInstance({}); + + if (instanceProps.parameterGroup && instanceProps.parameters) { + throw new Error('You cannot specify both parameterGroup and parameters'); + } + + const instanceParameterGroup = instanceProps.parameterGroup ?? ( + instanceProps.parameters + ? new ParameterGroup(cluster, 'InstanceParameterGroup', { + engine: props.engine, + parameters: instanceProps.parameters, + }) + : undefined + ); + const instanceParameterGroupConfig = instanceParameterGroup?.bindToInstance({}); + for (let i = 0; i < instanceCount; i++) { const instanceIndex = i + 1; const instanceIdentifier = props.instanceIdentifierBase != null ? `${props.instanceIdentifierBase}${instanceIndex}` : diff --git a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts index 5b2388eb35b45..01f2f62d7a4da 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts @@ -799,6 +799,11 @@ export class PostgresEngineVersion { * @deprecated PostgreSQL 9.6 will reach end of life in November 2021 */ public static readonly VER_9_6_23 = PostgresEngineVersion.of('9.6.23', '9.6'); + /** + * Version "9.6.24". + * @deprecated PostgreSQL 9.6 will reach end of life in November 2021 + */ + public static readonly VER_9_6_24 = PostgresEngineVersion.of('9.6.24', '9.6'); /** Version "10" (only a major version, without a specific minor version). */ public static readonly VER_10 = PostgresEngineVersion.of('10', '10'); @@ -834,6 +839,8 @@ export class PostgresEngineVersion { public static readonly VER_10_17 = PostgresEngineVersion.of('10.17', '10', { s3Import: true, s3Export: true }); /** Version "10.18". */ public static readonly VER_10_18 = PostgresEngineVersion.of('10.18', '10', { s3Import: true, s3Export: true }); + /** Version "10.19". */ + public static readonly VER_10_19 = PostgresEngineVersion.of('10.19', '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 }); @@ -861,6 +868,8 @@ export class PostgresEngineVersion { public static readonly VER_11_12 = PostgresEngineVersion.of('11.12', '11', { s3Import: true, s3Export: true }); /** Version "11.13". */ public static readonly VER_11_13 = PostgresEngineVersion.of('11.13', '11', { s3Import: true, s3Export: true }); + /** Version "11.14". */ + public static readonly VER_11_14 = PostgresEngineVersion.of('11.14', '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 }); @@ -878,6 +887,8 @@ export class PostgresEngineVersion { public static readonly VER_12_7 = PostgresEngineVersion.of('12.7', '12', { s3Import: true, s3Export: true }); /** Version "12.8". */ public static readonly VER_12_8 = PostgresEngineVersion.of('12.8', '12', { s3Import: true, s3Export: true }); + /** Version "12.9". */ + public static readonly VER_12_9 = PostgresEngineVersion.of('12.9', '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, s3Export: true }); @@ -889,6 +900,13 @@ export class PostgresEngineVersion { public static readonly VER_13_3 = PostgresEngineVersion.of('13.3', '13', { s3Import: true, s3Export: true }); /** Version "13.4". */ public static readonly VER_13_4 = PostgresEngineVersion.of('13.4', '13', { s3Import: true, s3Export: true }); + /** Version "13.5". */ + public static readonly VER_13_5 = PostgresEngineVersion.of('13.5', '13', { s3Import: true, s3Export: true }); + + /** Version "14" (only a major version, without a specific minor version). */ + public static readonly VER_14 = PostgresEngineVersion.of('14', '14', { s3Import: true, s3Export: true }); + /** Version "14.1". */ + public static readonly VER_14_1 = PostgresEngineVersion.of('14.1', '14', { s3Import: true, s3Export: true }); /** * Create a new PostgresEngineVersion with an arbitrary version. @@ -1392,7 +1410,10 @@ export class SqlServerEngineVersion { public static readonly VER_14_00_3035_2_V1 = SqlServerEngineVersion.of('14.00.3035.2.v1', '14.00'); /** Version "14.00.3049.1.v1". */ public static readonly VER_14_00_3049_1_V1 = SqlServerEngineVersion.of('14.00.3049.1.v1', '14.00'); - /** Version "14.00.3192.2.v1". */ + /** + * Version "14.00.3192.2.v1". + * @deprecated SQL Server version 14.00.3192.2.v1 reached end of life + */ public static readonly VER_14_00_3192_2_V1 = SqlServerEngineVersion.of('14.00.3192.2.v1', '14.00'); /** Version "14.00.3223.3.v1". */ public static readonly VER_14_00_3223_3_V1 = SqlServerEngineVersion.of('14.00.3223.3.v1', '14.00'); diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index 10467e0b19390..6236fe0fbc156 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -12,7 +12,7 @@ import { DatabaseSecret } from './database-secret'; import { Endpoint } from './endpoint'; import { IInstanceEngine } from './instance-engine'; import { IOptionGroup } from './option-group'; -import { IParameterGroup } from './parameter-group'; +import { IParameterGroup, ParameterGroup } from './parameter-group'; import { DEFAULT_PASSWORD_EXCLUDE_CHARS, defaultDeletionProtection, engineDescription, renderCredentials, setupS3ImportExport, helperRemovalPolicy, renderUnless } from './private/util'; import { Credentials, PerformanceInsightRetention, RotationMultiUserOptions, RotationSingleUserOptions, SnapshotCredentials } from './props'; import { DatabaseProxy, DatabaseProxyOptions, ProxyTarget } from './proxy'; @@ -822,6 +822,16 @@ export interface DatabaseInstanceSourceProps extends DatabaseInstanceNewProps { * @default - no name */ readonly databaseName?: string; + + /** + * The parameters in the DBParameterGroup to create automatically + * + * You can only specify parameterGroup or parameters but not both. + * You need to use a versioned engine to auto-generate a DBParameterGroup. + * + * @default - None + */ + readonly parameters?: { [key: string]: string }; } /** @@ -877,6 +887,17 @@ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDa this.instanceType = props.instanceType ?? ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.LARGE); + if (props.parameterGroup && props.parameters) { + throw new Error('You cannot specify both parameterGroup and parameters'); + } + + const dbParameterGroupName = props.parameters + ? new ParameterGroup(this, 'ParameterGroup', { + engine: props.engine, + parameters: props.parameters, + }).bindToInstance({}).parameterGroupName + : this.newCfnProps.dbParameterGroupName; + this.sourceCfnProps = { ...this.newCfnProps, associatedRoles: instanceAssociatedRoles.length > 0 ? instanceAssociatedRoles : undefined, @@ -888,6 +909,7 @@ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDa engineVersion: props.engine.engineVersion?.fullVersion, licenseModel: props.licenseModel, timezone: props.timezone, + dbParameterGroupName, }; } diff --git a/packages/@aws-cdk/aws-rds/lib/props.ts b/packages/@aws-cdk/aws-rds/lib/props.ts index 4a8883df504e3..663719431d640 100644 --- a/packages/@aws-cdk/aws-rds/lib/props.ts +++ b/packages/@aws-cdk/aws-rds/lib/props.ts @@ -43,6 +43,16 @@ export interface InstanceProps { */ readonly parameterGroup?: IParameterGroup; + /** + * The parameters in the DBParameterGroup to create automatically + * + * You can only specify parameterGroup or parameters but not both. + * You need to use a versioned engine to auto-generate a DBParameterGroup. + * + * @default - None + */ + readonly parameters?: { [key: string]: string }; + /** * Whether to enable Performance Insights for the DB instance. * diff --git a/packages/@aws-cdk/aws-rds/lib/serverless-cluster.ts b/packages/@aws-cdk/aws-rds/lib/serverless-cluster.ts index 0365bfd98c9e9..955b92ac58e5f 100644 --- a/packages/@aws-cdk/aws-rds/lib/serverless-cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/serverless-cluster.ts @@ -99,11 +99,14 @@ interface ServerlessClusterNewProps { /** * The VPC that this Aurora Serverless cluster has been created in. + * + * @default - the default VPC in the account and region will be used */ - readonly vpc: ec2.IVpc; + readonly vpc?: ec2.IVpc; /** - * Where to place the instances within the VPC + * Where to place the instances within the VPC. + * If provided, the `vpc` property must also be specified. * * @default - the VPC default strategy if not specified. */ @@ -129,7 +132,8 @@ interface ServerlessClusterNewProps { /** * Security group. * - * @default - a new security group is created. + * @default - a new security group is created if `vpc` was provided. + * If the `vpc` property was not provided, no VPC security groups will be associated with the DB cluster. */ readonly securityGroups?: ec2.ISecurityGroup[]; @@ -143,7 +147,8 @@ interface ServerlessClusterNewProps { /** * Existing subnet group for the cluster. * - * @default - a new subnet group will be created. + * @default - a new subnet group is created if `vpc` was provided. + * If the `vpc` property was not provided, no subnet group will be associated with the DB cluster */ readonly subnetGroup?: ISubnetGroup; } @@ -351,19 +356,42 @@ abstract class ServerlessClusterNew extends ServerlessClusterBase { constructor(scope: Construct, id: string, props: ServerlessClusterNewProps) { super(scope, id); - const { subnetIds } = props.vpc.selectSubnets(props.vpcSubnets); - - // Cannot test whether the subnets are in different AZs, but at least we can test the amount. - if (subnetIds.length < 2) { - Annotations.of(this).addError(`Cluster requires at least 2 subnets, got ${subnetIds.length}`); + if (props.vpc === undefined) { + if (props.vpcSubnets !== undefined) { + throw new Error('A VPC is required to use vpcSubnets in ServerlessCluster. Please add a VPC or remove vpcSubnets'); + } + if (props.subnetGroup !== undefined) { + throw new Error('A VPC is required to use subnetGroup in ServerlessCluster. Please add a VPC or remove subnetGroup'); + } + if (props.securityGroups !== undefined) { + throw new Error('A VPC is required to use securityGroups in ServerlessCluster. Please add a VPC or remove securityGroups'); + } } - const subnetGroup = props.subnetGroup ?? new SubnetGroup(this, 'Subnets', { - description: `Subnets for ${id} database`, - vpc: props.vpc, - vpcSubnets: props.vpcSubnets, - removalPolicy: props.removalPolicy === RemovalPolicy.RETAIN ? props.removalPolicy : undefined, - }); + let subnetGroup: ISubnetGroup | undefined = props.subnetGroup; + this.securityGroups = props.securityGroups ?? []; + if (props.vpc !== undefined) { + const { subnetIds } = props.vpc.selectSubnets(props.vpcSubnets); + + // Cannot test whether the subnets are in different AZs, but at least we can test the amount. + if (subnetIds.length < 2) { + Annotations.of(this).addError(`Cluster requires at least 2 subnets, got ${subnetIds.length}`); + } + + subnetGroup = props.subnetGroup ?? new SubnetGroup(this, 'Subnets', { + description: `Subnets for ${id} database`, + vpc: props.vpc, + vpcSubnets: props.vpcSubnets, + removalPolicy: props.removalPolicy === RemovalPolicy.RETAIN ? props.removalPolicy : undefined, + }); + + this.securityGroups = props.securityGroups ?? [ + new ec2.SecurityGroup(this, 'SecurityGroup', { + description: 'RDS security group', + vpc: props.vpc, + }), + ]; + } if (props.backupRetention) { const backupRetentionDays = props.backupRetention.toDays(); @@ -379,12 +407,6 @@ abstract class ServerlessClusterNew extends ServerlessClusterBase { const clusterParameterGroup = props.parameterGroup ?? clusterEngineBindConfig.parameterGroup; const clusterParameterGroupConfig = clusterParameterGroup?.bindToCluster({}); - this.securityGroups = props.securityGroups ?? [ - new ec2.SecurityGroup(this, 'SecurityGroup', { - description: 'RDS security group', - vpc: props.vpc, - }), - ]; const clusterIdentifier = FeatureFlags.of(this).isEnabled(cxapi.RDS_LOWERCASE_DB_IDENTIFIER) ? props.clusterIdentifier?.toLowerCase() @@ -395,7 +417,7 @@ abstract class ServerlessClusterNew extends ServerlessClusterBase { databaseName: props.defaultDatabaseName, dbClusterIdentifier: clusterIdentifier, dbClusterParameterGroupName: clusterParameterGroupConfig?.parameterGroupName, - dbSubnetGroupName: subnetGroup.subnetGroupName, + dbSubnetGroupName: subnetGroup?.subnetGroupName, deletionProtection: defaultDeletionProtection(props.deletionProtection, props.removalPolicy), engine: props.engine.engineType, engineVersion: props.engine.engineVersion?.fullVersion, @@ -476,7 +498,7 @@ export class ServerlessCluster extends ServerlessClusterNew { public readonly secret?: secretsmanager.ISecret; - private readonly vpc: ec2.IVpc; + private readonly vpc?: ec2.IVpc; private readonly vpcSubnets?: ec2.SubnetSelection; private readonly singleUserRotationApplication: secretsmanager.SecretRotationApplication; @@ -525,6 +547,10 @@ export class ServerlessCluster extends ServerlessClusterNew { throw new Error('Cannot add single user rotation for a cluster without secret.'); } + if (this.vpc === undefined) { + throw new Error('Cannot add single user rotation for a cluster without VPC.'); + } + const id = 'RotationSingleUser'; const existing = this.node.tryFindChild(id); if (existing) { @@ -549,6 +575,11 @@ export class ServerlessCluster extends ServerlessClusterNew { if (!this.secret) { throw new Error('Cannot add multi user rotation for a cluster without secret.'); } + + if (this.vpc === undefined) { + throw new Error('Cannot add multi user rotation for a cluster without VPC.'); + } + return new secretsmanager.SecretRotation(this, id, { ...options, excludeCharacters: options.excludeCharacters ?? DEFAULT_PASSWORD_EXCLUDE_CHARS, @@ -680,4 +711,4 @@ export class ServerlessClusterFromSnapshot extends ServerlessClusterNew { this.secret = secret.attach(this); } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-rds/package.json b/packages/@aws-cdk/aws-rds/package.json index 5adbb21970889..3110738220ab1 100644 --- a/packages/@aws-cdk/aws-rds/package.json +++ b/packages/@aws-cdk/aws-rds/package.json @@ -79,7 +79,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-events-targets": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", @@ -87,8 +87,8 @@ "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-cloudwatch": "0.0.0", diff --git a/packages/@aws-cdk/aws-rds/test/cluster-engine.test.ts b/packages/@aws-cdk/aws-rds/test/cluster-engine.test.ts index 826c988688c24..f6e3824092442 100644 --- a/packages/@aws-cdk/aws-rds/test/cluster-engine.test.ts +++ b/packages/@aws-cdk/aws-rds/test/cluster-engine.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import { AuroraEngineVersion, AuroraMysqlEngineVersion, AuroraPostgresEngineVersion, DatabaseClusterEngine } from '../lib'; describe('cluster engine', () => { @@ -11,8 +10,6 @@ describe('cluster engine', () => { // THEN expect(family).toEqual('aurora5.6'); - - }); test("default parameterGroupFamily for versionless Aurora MySQL cluster engine is 'aurora-mysql5.7'", () => { @@ -24,8 +21,6 @@ describe('cluster engine', () => { // THEN expect(family).toEqual('aurora-mysql5.7'); - - }); test('default parameterGroupFamily for versionless Aurora PostgreSQL is not defined', () => { @@ -37,8 +32,6 @@ describe('cluster engine', () => { // THEN expect(family).toEqual(undefined); - - }); test('cluster parameter group correctly determined for AURORA and given version', () => { @@ -52,8 +45,6 @@ describe('cluster engine', () => { // THEN expect(family).toEqual('aurora5.6'); - - }); test('cluster parameter group correctly determined for AURORA_MYSQL and given version', () => { @@ -67,8 +58,6 @@ describe('cluster engine', () => { // THEN expect(family).toEqual('aurora-mysql5.7'); - - }); test('cluster parameter group correctly determined for AURORA_MYSQL and given version 3', () => { @@ -95,8 +84,6 @@ describe('cluster engine', () => { // THEN expect(family).toEqual('aurora-postgresql11'); - - }); test('parameter group family', () => { @@ -117,8 +104,6 @@ describe('cluster engine', () => { 'aurora-postgresql9.6'); expect(DatabaseClusterEngine.auroraPostgres({ version: AuroraPostgresEngineVersion.of('10.0', '10') }).parameterGroupFamily).toEqual( 'aurora-postgresql10'); - - }); test('supported log types', () => { @@ -126,6 +111,5 @@ describe('cluster engine', () => { expect(DatabaseClusterEngine.aurora({ version: AuroraEngineVersion.VER_1_22_2 }).supportedLogTypes).toEqual(mysqlLogTypes); expect(DatabaseClusterEngine.auroraMysql({ version: AuroraMysqlEngineVersion.VER_2_08_1 }).supportedLogTypes).toEqual(mysqlLogTypes); expect(DatabaseClusterEngine.auroraPostgres({ version: AuroraPostgresEngineVersion.VER_9_6_9 }).supportedLogTypes).toEqual(['postgresql']); - }); -}); \ No newline at end of file +}); diff --git a/packages/@aws-cdk/aws-rds/test/cluster.test.ts b/packages/@aws-cdk/aws-rds/test/cluster.test.ts index b3dbdfc81d007..4ee20a30daffd 100644 --- a/packages/@aws-cdk/aws-rds/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-rds/test/cluster.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT, ResourcePart, SynthUtils } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import { ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; @@ -11,6 +10,7 @@ import * as cxapi from '@aws-cdk/cx-api'; import { AuroraEngineVersion, AuroraMysqlEngineVersion, AuroraPostgresEngineVersion, CfnDBCluster, Credentials, DatabaseCluster, DatabaseClusterEngine, DatabaseClusterFromSnapshot, ParameterGroup, PerformanceInsightRetention, SubnetGroup, DatabaseSecret, + DatabaseInstanceEngine, SqlServerEngineVersion, } from '../lib'; describe('cluster', () => { @@ -34,7 +34,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResource('AWS::RDS::DBCluster', { Properties: { Engine: 'aurora', DBSubnetGroupName: { Ref: 'DatabaseSubnets56F17B9A' }, @@ -46,13 +46,13 @@ describe('cluster', () => { }, DeletionPolicy: 'Snapshot', UpdateReplacePolicy: 'Snapshot', - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toCountResources('AWS::RDS::DBInstance', 2); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).resourceCountIs('AWS::RDS::DBInstance', 2); + Template.fromStack(stack).hasResource('AWS::RDS::DBInstance', { DeletionPolicy: 'Delete', UpdateReplacePolicy: 'Delete', - }, ResourcePart.CompleteDefinition); + }); }); test('validates that the number of instances is not a deploy-time value', () => { @@ -91,7 +91,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { Engine: 'aurora', DBSubnetGroupName: { Ref: 'DatabaseSubnets56F17B9A' }, MasterUsername: 'admin', @@ -146,7 +146,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { Engine: 'aurora', DBSubnetGroupName: { Ref: 'DatabaseSubnets56F17B9A' }, MasterUsername: 'admin', @@ -182,7 +182,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { DBClusterParameterGroupName: { Ref: 'ParamsA8366201' }, }); }); @@ -201,10 +201,10 @@ describe('cluster', () => { removalPolicy: cdk.RemovalPolicy.RETAIN, }); - expect(stack).toHaveResourceLike('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResource('AWS::RDS::DBSubnetGroup', { DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition); + }); }); test('creates a secret when master credentials are not specified', () => { @@ -226,7 +226,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { MasterUsername: { 'Fn::Join': [ '', @@ -253,7 +253,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { GenerateSecretString: { ExcludeCharacters: '\"@/\\', GenerateStringKey: 'password', @@ -282,7 +282,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { KmsKeyId: { 'Fn::GetAtt': [ 'Key961B73FD', @@ -316,13 +316,119 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { DBParameterGroupName: { Ref: 'ParameterGroup5E32DECB', }, }); }); + test('cluster with inline parameter group', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + engine: DatabaseClusterEngine.AURORA, + parameters: { + locks: '100', + }, + instanceProps: { + vpc, + parameters: { + locks: '200', + }, + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { + DBClusterParameterGroupName: { + Ref: 'DatabaseParameterGroup2A921026', + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBClusterParameterGroup', { + Family: 'aurora5.6', + Parameters: { + locks: '100', + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { + DBParameterGroupName: { + Ref: 'DatabaseInstanceParameterGroup6968C5BF', + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBParameterGroup', { + Family: 'aurora5.6', + Parameters: { + locks: '200', + }, + }); + }); + + test('cluster with inline parameter group and parameterGroup arg fails', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const parameterGroup = new ParameterGroup(stack, 'ParameterGroup', { + engine: DatabaseInstanceEngine.sqlServerEe({ + version: SqlServerEngineVersion.VER_11, + }), + parameters: { + locks: '50', + }, + }); + + expect(() => { + new DatabaseCluster(stack, 'Database', { + engine: DatabaseClusterEngine.AURORA, + parameters: { + locks: '100', + }, + parameterGroup, + instanceProps: { + vpc, + parameters: { + locks: '200', + }, + }, + }); + }).toThrow(/You cannot specify both parameterGroup and parameters/); + }); + + test('instance with inline parameter group and parameterGroup arg fails', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const parameterGroup = new ParameterGroup(stack, 'ParameterGroup', { + engine: DatabaseInstanceEngine.sqlServerEe({ + version: SqlServerEngineVersion.VER_11, + }), + parameters: { + locks: '50', + }, + }); + + expect(() => { + new DatabaseCluster(stack, 'Database', { + engine: DatabaseClusterEngine.AURORA, + parameters: { + locks: '100', + }, + instanceProps: { + vpc, + parameterGroup, + parameters: { + locks: '200', + }, + }, + }); + }).toThrow(/You cannot specify both parameterGroup and parameters/); + }); + describe('performance insights', () => { test('cluster with all performance insights properties', () => { // GIVEN @@ -343,7 +449,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { EnablePerformanceInsights: true, PerformanceInsightsRetentionPeriod: 731, PerformanceInsightsKMSKeyId: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, @@ -367,7 +473,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { EnablePerformanceInsights: true, PerformanceInsightsRetentionPeriod: 731, }); @@ -408,7 +514,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { AutoMinorVersionUpgrade: false, }); }); @@ -427,7 +533,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { AllowMajorVersionUpgrade: true, }); }); @@ -446,7 +552,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { DeleteAutomatedBackups: false, }); }); @@ -471,7 +577,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { Engine: 'aurora-mysql', EngineVersion: '5.7.mysql_aurora.2.04.4', }); @@ -497,12 +603,10 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { Engine: 'aurora-postgresql', EngineVersion: '10.7', }); - - }); test('cluster exposes different read and write endpoints', () => { @@ -546,7 +650,7 @@ describe('cluster', () => { cluster.connections.allowToAnyIpv4(ec2.Port.tcp(443)); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: 'sg-123456789', }); }); @@ -559,8 +663,6 @@ describe('cluster', () => { }); expect(cluster.clusterIdentifier).toEqual('identifier'); - - }); test('minimal imported cluster throws on accessing attributes for unprovided parameters', () => { @@ -621,8 +723,6 @@ describe('cluster', () => { account: '12345', region: 'us-test-1', }); - - }); test('cluster with enabled monitoring', () => { @@ -645,14 +745,14 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { MonitoringInterval: 60, MonitoringRoleArn: { 'Fn::GetAtt': ['DatabaseMonitoringRole576991DA', 'Arn'], }, - }, ResourcePart.Properties); + }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -680,8 +780,6 @@ describe('cluster', () => { }, ], }); - - }); test('create a cluster with imported monitoring role', () => { @@ -712,14 +810,12 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { MonitoringInterval: 60, MonitoringRoleArn: { 'Fn::GetAtt': ['MonitoringRole90457BF9', 'Arn'], }, - }, ResourcePart.Properties); - - + }); }); test('addRotationSingleUser()', () => { @@ -737,7 +833,7 @@ describe('cluster', () => { // WHEN cluster.addRotationSingleUser(); - expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { SecretId: { Ref: 'DatabaseSecretAttachmentE5D1B020', }, @@ -768,7 +864,7 @@ describe('cluster', () => { const userSecret = new DatabaseSecret(stack, 'UserSecret', { username: 'user' }); cluster.addRotationMultiUser('user', { secret: userSecret.attach(cluster) }); - expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { SecretId: { Ref: 'UserSecretAttachment16ACBE6D', }, @@ -783,7 +879,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Serverless::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::Serverless::Application', { Parameters: { masterSecretArn: { Ref: 'DatabaseSecretAttachmentE5D1B020', @@ -822,13 +918,13 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { RotationRules: { AutomaticallyAfterDays: 15, }, }); - expect(stack).toHaveResource('AWS::Serverless::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::Serverless::Application', { Parameters: { endpoint: { 'Fn::Join': ['', [ @@ -882,7 +978,7 @@ describe('cluster', () => { // Rotation in isolated subnet with access to Secrets Manager API via endpoint cluster.addRotationSingleUser({ endpoint }); - expect(stack).toHaveResource('AWS::Serverless::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::Serverless::Application', { Parameters: { endpoint: { 'Fn::Join': ['', [ @@ -933,8 +1029,6 @@ describe('cluster', () => { // THEN expect(() => cluster.addRotationSingleUser()).toThrow(/without secret/); - - }); test('throws when trying to add single user rotation multiple times', () => { @@ -955,8 +1049,6 @@ describe('cluster', () => { // THEN expect(() => cluster.addRotationSingleUser()).toThrow(/A single user rotation was already added to this cluster/); - - }); test('create a cluster with s3 import role', () => { @@ -983,7 +1075,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -994,7 +1086,7 @@ describe('cluster', () => { }], }); - expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBClusterParameterGroup', { Family: 'aurora5.6', Parameters: { aurora_load_from_s3_role: { @@ -1005,8 +1097,6 @@ describe('cluster', () => { }, }, }); - - }); test('create a cluster with s3 import buckets', () => { @@ -1031,7 +1121,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1042,7 +1132,7 @@ describe('cluster', () => { }], }); - expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBClusterParameterGroup', { Family: 'aurora5.6', Parameters: { aurora_load_from_s3_role: { @@ -1054,7 +1144,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -1091,8 +1181,6 @@ describe('cluster', () => { Version: '2012-10-17', }, }); - - }); test('cluster with s3 import bucket adds supported feature name to IAM role', () => { @@ -1119,7 +1207,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1130,8 +1218,6 @@ describe('cluster', () => { FeatureName: 's3Import', }], }); - - }); test('throws when s3 import bucket or s3 export bucket is supplied for a Postgres version that does not support it', () => { @@ -1175,8 +1261,6 @@ describe('cluster', () => { s3ExportBuckets: [bucket], }); }).toThrow(/s3Export is not supported for Postgres version: 10.4. Use a version that supports the s3Export feature./); - - }); test('cluster with s3 export bucket adds supported feature name to IAM role', () => { @@ -1203,7 +1287,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1214,8 +1298,6 @@ describe('cluster', () => { FeatureName: 's3Export', }], }); - - }); test('create a cluster with s3 export role', () => { @@ -1242,7 +1324,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1253,7 +1335,7 @@ describe('cluster', () => { }], }); - expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBClusterParameterGroup', { Family: 'aurora5.6', Parameters: { aurora_select_into_s3_role: { @@ -1264,8 +1346,6 @@ describe('cluster', () => { }, }, }); - - }); testFutureBehavior('create a cluster with s3 export buckets', { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }, cdk.App, (app) => { @@ -1290,7 +1370,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1301,7 +1381,7 @@ describe('cluster', () => { }], }); - expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBClusterParameterGroup', { Family: 'aurora5.6', Parameters: { aurora_select_into_s3_role: { @@ -1313,7 +1393,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -1323,6 +1403,10 @@ describe('cluster', () => { 's3:List*', 's3:DeleteObject*', 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', 's3:Abort*', ], Effect: 'Allow', @@ -1353,8 +1437,6 @@ describe('cluster', () => { Version: '2012-10-17', }, }); - - }); test('create a cluster with s3 import and export buckets', () => { @@ -1381,7 +1463,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1400,7 +1482,7 @@ describe('cluster', () => { }], }); - expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBClusterParameterGroup', { Family: 'aurora5.6', Parameters: { aurora_load_from_s3_role: { @@ -1417,8 +1499,6 @@ describe('cluster', () => { }, }, }); - - }); test('create a cluster with s3 import and export buckets and custom parameter group', () => { @@ -1453,7 +1533,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1472,7 +1552,7 @@ describe('cluster', () => { }], }); - expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBClusterParameterGroup', { Family: 'aurora5.6', Parameters: { key: 'value', @@ -1490,8 +1570,6 @@ describe('cluster', () => { }, }, }); - - }); test('PostgreSQL cluster with s3 export buckets does not generate custom parameter group and specifies the correct port', () => { @@ -1518,7 +1596,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1531,9 +1609,7 @@ describe('cluster', () => { Port: 5432, }); - expect(stack).not.toHaveResource('AWS::RDS::DBClusterParameterGroup'); - - + Template.fromStack(stack).resourceCountIs('AWS::RDS::DBClusterParameterGroup', 0); }); test('unversioned PostgreSQL cluster can be used with s3 import and s3 export buckets', () => { @@ -1560,7 +1636,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { AssociatedRoles: [ { FeatureName: 's3Import', @@ -1582,8 +1658,6 @@ describe('cluster', () => { }, ], }); - - }); test("Aurora PostgreSQL cluster uses a different default master username than 'admin', which is a reserved word", () => { @@ -1600,13 +1674,11 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { GenerateSecretString: { SecretStringTemplate: '{"username":"postgres"}', }, }); - - }); test('MySQL cluster without S3 exports or imports references the correct default ParameterGroup', () => { @@ -1628,13 +1700,11 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { DBClusterParameterGroupName: 'default.aurora-mysql5.7', }); - expect(stack).not.toHaveResource('AWS::RDS::DBClusterParameterGroup'); - - + Template.fromStack(stack).resourceCountIs('AWS::RDS::DBClusterParameterGroup', 0); }); test('throws when s3ExportRole and s3ExportBuckets properties are both specified', () => { @@ -1661,8 +1731,6 @@ describe('cluster', () => { s3ExportRole: exportRole, s3ExportBuckets: [exportBucket], })).toThrow(); - - }); test('throws when s3ImportRole and s3ImportBuckets properties are both specified', () => { @@ -1689,8 +1757,6 @@ describe('cluster', () => { s3ImportRole: importRole, s3ImportBuckets: [importBucket], })).toThrow(); - - }); test('can set CloudWatch log exports', () => { @@ -1713,11 +1779,9 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { EnableCloudwatchLogsExports: ['error', 'general', 'slowquery', 'audit'], }); - - }); test('can set CloudWatch log retention', () => { @@ -1741,7 +1805,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('Custom::LogRetention', { + Template.fromStack(stack).hasResourceProperties('Custom::LogRetention', { ServiceToken: { 'Fn::GetAtt': [ 'LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A', @@ -1751,7 +1815,7 @@ describe('cluster', () => { LogGroupName: { 'Fn::Join': ['', ['/aws/rds/cluster/', { Ref: 'DatabaseB269D8BB' }, '/error']] }, RetentionInDays: 90, }); - expect(stack).toHaveResource('Custom::LogRetention', { + Template.fromStack(stack).hasResourceProperties('Custom::LogRetention', { ServiceToken: { 'Fn::GetAtt': [ 'LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A', @@ -1761,8 +1825,6 @@ describe('cluster', () => { LogGroupName: { 'Fn::Join': ['', ['/aws/rds/cluster/', { Ref: 'DatabaseB269D8BB' }, '/general']] }, RetentionInDays: 90, }); - - }); test('throws if given unsupported CloudWatch log exports', () => { @@ -1784,8 +1846,6 @@ describe('cluster', () => { cloudwatchLogsExports: ['error', 'general', 'slowquery', 'audit', 'thislogdoesnotexist', 'neitherdoesthisone'], }); }).toThrow(/Unsupported logs for the current engine type: thislogdoesnotexist,neitherdoesthisone/); - - }); test('can set deletion protection', () => { @@ -1808,16 +1868,15 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { DeletionProtection: true, }); - - }); test('does not throw (but adds a node error) if a (dummy) VPC does not have sufficient subnets', () => { // GIVEN - const stack = testStack(); + const app = new cdk.App(); + const stack = testStack(app, 'TestStack'); const vpc = ec2.Vpc.fromLookup(stack, 'VPC', { isDefault: true }); // WHEN @@ -1837,11 +1896,9 @@ describe('cluster', () => { }); // THEN - const art = SynthUtils.synthesize(stack); + const art = app.synth().getStackArtifact('TestStack'); const meta = art.findMetadataByType('aws:cdk:error'); expect(meta[0].data).toEqual('Cluster requires at least 2 subnets, got 0'); - - }); test('create a cluster from a snapshot', () => { @@ -1859,7 +1916,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResource('AWS::RDS::DBCluster', { Properties: { Engine: 'aurora', EngineVersion: '5.6.mysql_aurora.1.22.2', @@ -1870,9 +1927,9 @@ describe('cluster', () => { }, DeletionPolicy: 'Snapshot', UpdateReplacePolicy: 'Snapshot', - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toCountResources('AWS::RDS::DBInstance', 2); + Template.fromStack(stack).resourceCountIs('AWS::RDS::DBInstance', 2); expect(cluster.instanceIdentifiers).toHaveLength(2); expect(stack.resolve(cluster.instanceIdentifiers[0])).toEqual({ @@ -1915,12 +1972,10 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { DBSubnetGroupName: 'my-subnet-group', }); - expect(stack).toCountResources('AWS::RDS::DBSubnetGroup', 0); - - + Template.fromStack(stack).resourceCountIs('AWS::RDS::DBSubnetGroup', 0); }); test('defaultChild returns the DB Cluster', () => { @@ -1941,8 +1996,6 @@ describe('cluster', () => { // THEN expect(cluster.node.defaultChild instanceof CfnDBCluster).toBeTruthy(); - - }); test('fromGeneratedSecret', () => { @@ -1960,7 +2013,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { MasterUsername: 'admin', // username is a string MasterUserPassword: { 'Fn::Join': [ @@ -1994,7 +2047,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { ReplicaRegions: [ { Region: 'eu-west-1', @@ -2023,7 +2076,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { Name: secretName, }); }); @@ -2044,7 +2097,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { Name: secretName, }); }); @@ -2066,12 +2119,10 @@ describe('cluster', () => { }, }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { Engine: 'aurora', PubliclyAccessible: true, }); - - }); test('can set public accessibility for database cluster with instances in public subnet', () => { @@ -2091,12 +2142,10 @@ describe('cluster', () => { }, }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { Engine: 'aurora', PubliclyAccessible: false, }); - - }); test('database cluster instances in public subnet should by default have publiclyAccessible set to true', () => { @@ -2115,12 +2164,10 @@ describe('cluster', () => { }, }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { Engine: 'aurora', PubliclyAccessible: true, }); - - }); test('changes the case of the cluster identifier if the lowercaseDbIdentifier feature flag is enabled', () => { @@ -2140,7 +2187,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { DBClusterIdentifier: clusterIdentifier.toLowerCase(), }); }); @@ -2160,7 +2207,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { DBClusterIdentifier: clusterIdentifier, }); }); @@ -2179,7 +2226,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { CopyTagsToSnapshot: true, }); }); @@ -2199,7 +2246,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { CopyTagsToSnapshot: false, }); }); @@ -2219,7 +2266,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { CopyTagsToSnapshot: true, }); }); @@ -2239,7 +2286,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { BacktrackWindow: 24 * 60 * 60, }); }); @@ -2247,8 +2294,8 @@ describe('cluster', () => { test.each([ [cdk.RemovalPolicy.RETAIN, 'Retain', 'Retain', 'Retain'], - [cdk.RemovalPolicy.SNAPSHOT, 'Snapshot', 'Delete', ABSENT], - [cdk.RemovalPolicy.DESTROY, 'Delete', 'Delete', ABSENT], + [cdk.RemovalPolicy.SNAPSHOT, 'Snapshot', 'Delete', Match.absent()], + [cdk.RemovalPolicy.DESTROY, 'Delete', 'Delete', Match.absent()], ])('if Cluster RemovalPolicy is \'%s\', the DBCluster has DeletionPolicy \'%s\', the DBInstance has \'%s\' and the DBSubnetGroup has \'%s\'', (clusterRemovalPolicy, clusterValue, instanceValue, subnetValue) => { const stack = new cdk.Stack(); @@ -2264,25 +2311,25 @@ test.each([ }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResource('AWS::RDS::DBCluster', { DeletionPolicy: clusterValue, UpdateReplacePolicy: clusterValue, - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResource('AWS::RDS::DBInstance', { DeletionPolicy: instanceValue, UpdateReplacePolicy: instanceValue, - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResourceLike('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResource('AWS::RDS::DBSubnetGroup', { DeletionPolicy: subnetValue, - }, ResourcePart.CompleteDefinition); + }); }); test.each([ [cdk.RemovalPolicy.RETAIN, 'Retain', 'Retain', 'Retain'], - [cdk.RemovalPolicy.SNAPSHOT, 'Snapshot', 'Delete', ABSENT], - [cdk.RemovalPolicy.DESTROY, 'Delete', 'Delete', ABSENT], + [cdk.RemovalPolicy.SNAPSHOT, 'Snapshot', 'Delete', Match.absent()], + [cdk.RemovalPolicy.DESTROY, 'Delete', 'Delete', Match.absent()], ])('if Cluster RemovalPolicy is \'%s\', the DBCluster has DeletionPolicy \'%s\', the DBInstance has \'%s\' and the DBSubnetGroup has \'%s\'', (clusterRemovalPolicy, clusterValue, instanceValue, subnetValue) => { const stack = new cdk.Stack(); @@ -2298,25 +2345,24 @@ test.each([ }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResource('AWS::RDS::DBCluster', { DeletionPolicy: clusterValue, UpdateReplacePolicy: clusterValue, - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResource('AWS::RDS::DBInstance', { DeletionPolicy: instanceValue, UpdateReplacePolicy: instanceValue, - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResourceLike('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResource('AWS::RDS::DBSubnetGroup', { DeletionPolicy: subnetValue, UpdateReplacePolicy: subnetValue, - }, ResourcePart.CompleteDefinition); + }); }); - -function testStack(app?: cdk.App) { - const stack = new cdk.Stack(app, undefined, { env: { account: '12345', region: 'us-test-1' } }); +function testStack(app?: cdk.App, stackId?: string) { + const stack = new cdk.Stack(app, stackId, { env: { account: '12345', region: 'us-test-1' } }); stack.node.setContext('availability-zones:12345:us-test-1', ['us-test-1a', 'us-test-1b']); return stack; } diff --git a/packages/@aws-cdk/aws-rds/test/database-secret.test.ts b/packages/@aws-cdk/aws-rds/test/database-secret.test.ts index 9fe7793536a1c..424fe4164fd74 100644 --- a/packages/@aws-cdk/aws-rds/test/database-secret.test.ts +++ b/packages/@aws-cdk/aws-rds/test/database-secret.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { CfnResource, Stack } from '@aws-cdk/core'; import { DatabaseSecret } from '../lib'; import { DEFAULT_PASSWORD_EXCLUDE_CHARS } from '../lib/private/util'; @@ -14,7 +14,7 @@ describe('database secret', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { Description: { 'Fn::Join': [ '', @@ -35,8 +35,6 @@ describe('database secret', () => { }); expect(getSecretLogicalId(dbSecret, stack)).toEqual('SecretA720EF05'); - - }); test('with master secret', () => { @@ -54,7 +52,7 @@ describe('database secret', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { GenerateSecretString: { ExcludeCharacters: '"@/\\', GenerateStringKey: 'password', @@ -73,8 +71,6 @@ describe('database secret', () => { }, }, }); - - }); test('replace on password critera change', () => { @@ -106,8 +102,6 @@ describe('database secret', () => { replaceOnPasswordCriteriaChanges: true, }); expect(dbSecretlogicalId).not.toEqual(getSecretLogicalId(otherSecret2, stack)); - - }); }); diff --git a/packages/@aws-cdk/aws-rds/test/database-secretmanager.test.ts b/packages/@aws-cdk/aws-rds/test/database-secretmanager.test.ts index 135704a893f75..8d1da9e04b1b6 100644 --- a/packages/@aws-cdk/aws-rds/test/database-secretmanager.test.ts +++ b/packages/@aws-cdk/aws-rds/test/database-secretmanager.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as cdk from '@aws-cdk/core'; @@ -21,7 +20,7 @@ describe('database secret manager', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResource('AWS::RDS::DBCluster', { Properties: { Engine: 'aurora-postgresql', DBClusterParameterGroupName: 'default.aurora-postgresql10', @@ -65,9 +64,7 @@ describe('database secret manager', () => { }, DeletionPolicy: 'Snapshot', UpdateReplacePolicy: 'Snapshot', - }, ResourcePart.CompleteDefinition); - - + }); }); }); 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 99ed162c4f5eb..f9a93f3bca867 100644 --- a/packages/@aws-cdk/aws-rds/test/instance-engine.test.ts +++ b/packages/@aws-cdk/aws-rds/test/instance-engine.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as rds from '../lib'; @@ -10,8 +10,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual(undefined); - - }); test('default parameterGroupFamily for versionless MySQL instance engine is not defined', () => { @@ -20,8 +18,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual(undefined); - - }); test('default parameterGroupFamily for versionless PostgreSQL instance engine is not defined', () => { @@ -30,8 +26,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual(undefined); - - }); test("default parameterGroupFamily for versionless Oracle SE instance engine is 'oracle-se-11.2'", () => { @@ -40,8 +34,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual('oracle-se-11.2'); - - }); test("default parameterGroupFamily for versionless Oracle SE 1 instance engine is 'oracle-se1-11.2'", () => { @@ -50,8 +42,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual('oracle-se1-11.2'); - - }); test('default parameterGroupFamily for versionless Oracle SE 2 instance engine is not defined', () => { @@ -60,8 +50,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual(undefined); - - }); test('default parameterGroupFamily for versionless Oracle EE instance engine is not defined', () => { @@ -70,8 +58,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual(undefined); - - }); test('default parameterGroupFamily for versionless SQL Server SE instance engine is not defined', () => { @@ -80,8 +66,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual(undefined); - - }); test('default parameterGroupFamily for versionless SQL Server EX instance engine is not defined', () => { @@ -90,8 +74,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual(undefined); - - }); test('default parameterGroupFamily for versionless SQL Server Web instance engine is not defined', () => { @@ -100,8 +82,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual(undefined); - - }); test('default parameterGroupFamily for versionless SQL Server EE instance engine is not defined', () => { @@ -110,8 +90,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual(undefined); - - }); describe('Oracle engine bindToInstance', () => { @@ -122,8 +100,6 @@ describe('instance engine', () => { const engineConfig = engine.bindToInstance(new cdk.Stack(), {}); expect(engineConfig.features?.s3Import).toEqual('S3_INTEGRATION'); expect(engineConfig.features?.s3Export).toEqual('S3_INTEGRATION'); - - }); test('s3 import/export - creates an option group if needed', () => { @@ -136,15 +112,13 @@ describe('instance engine', () => { }); expect(engineConfig.optionGroup).toBeDefined(); - expect(stack).toHaveResourceLike('AWS::RDS::OptionGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::OptionGroup', { EngineName: 'oracle-se2', OptionConfigurations: [{ OptionName: 'S3_INTEGRATION', OptionVersion: '1.0', }], }); - - }); test('s3 import/export - appends to an existing option group if it exists', () => { @@ -163,7 +137,7 @@ describe('instance engine', () => { }); expect(engineConfig.optionGroup).toEqual(optionGroup); - expect(stack).toHaveResourceLike('AWS::RDS::OptionGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::OptionGroup', { EngineName: 'oracle-se2', OptionConfigurations: [{ OptionName: 'MY_OPTION_CONFIG', @@ -173,8 +147,6 @@ describe('instance engine', () => { OptionVersion: '1.0', }], }); - - }); }); @@ -185,8 +157,6 @@ describe('instance engine', () => { const engineConfig = engine.bindToInstance(new cdk.Stack(), {}); expect(engineConfig.features?.s3Import).toEqual('S3_INTEGRATION'); expect(engineConfig.features?.s3Export).toEqual('S3_INTEGRATION'); - - }); test('s3 import/export - throws if roles are not equal', () => { @@ -200,8 +170,6 @@ describe('instance engine', () => { expect(() => engine.bindToInstance(new cdk.Stack(), { s3ImportRole })).not.toThrow(); expect(() => engine.bindToInstance(new cdk.Stack(), { s3ExportRole })).not.toThrow(); expect(() => engine.bindToInstance(new cdk.Stack(), { s3ImportRole, s3ExportRole: s3ImportRole })).not.toThrow(); - - }); test('s3 import/export - creates an option group if needed', () => { @@ -214,7 +182,7 @@ describe('instance engine', () => { }); expect(engineConfig.optionGroup).toBeDefined(); - expect(stack).toHaveResourceLike('AWS::RDS::OptionGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::OptionGroup', { EngineName: 'sqlserver-se', OptionConfigurations: [{ OptionName: 'SQLSERVER_BACKUP_RESTORE', @@ -224,8 +192,6 @@ describe('instance engine', () => { }], }], }); - - }); test('s3 import/export - appends to an existing option group if it exists', () => { @@ -244,7 +210,7 @@ describe('instance engine', () => { }); expect(engineConfig.optionGroup).toEqual(optionGroup); - expect(stack).toHaveResourceLike('AWS::RDS::OptionGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::OptionGroup', { EngineName: 'sqlserver-se', OptionConfigurations: [{ OptionName: 'MY_OPTION_CONFIG', @@ -257,8 +223,6 @@ describe('instance engine', () => { }], }], }); - - }); }); @@ -269,8 +233,6 @@ describe('instance engine', () => { const engineConfig = engineNewerVersion.bindToInstance(new cdk.Stack(), {}); expect(engineConfig.features?.s3Import).toEqual(undefined); expect(engineConfig.features?.s3Export).toEqual(undefined); - - }); test('returns s3 import/export feature if the version supports it', () => { @@ -279,8 +241,6 @@ describe('instance engine', () => { const engineConfig = engineNewerVersion.bindToInstance(new cdk.Stack(), {}); expect(engineConfig.features?.s3Import).toEqual('s3Import'); expect(engineConfig.features?.s3Export).toEqual('s3Export'); - - }); }); }); diff --git a/packages/@aws-cdk/aws-rds/test/instance.test.ts b/packages/@aws-cdk/aws-rds/test/instance.test.ts index 484ad765cb790..6ba08c6d9f2fb 100644 --- a/packages/@aws-cdk/aws-rds/test/instance.test.ts +++ b/packages/@aws-cdk/aws-rds/test/instance.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT, ResourcePart, anything } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as targets from '@aws-cdk/aws-events-targets'; import { ManagedPolicy, Role, ServicePrincipal, AccountPrincipal } from '@aws-cdk/aws-iam'; @@ -49,7 +48,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResource('AWS::RDS::DBInstance', { Properties: { DBInstanceClass: 'db.t2.medium', AllocatedStorage: '100', @@ -117,9 +116,9 @@ describe('instance', () => { }, DeletionPolicy: 'Snapshot', UpdateReplacePolicy: 'Snapshot', - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBSubnetGroup', { DBSubnetGroupDescription: 'Subnet group for Instance database', SubnetIds: [ { @@ -131,11 +130,11 @@ describe('instance', () => { ], }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Security group for Instance database', }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -164,7 +163,7 @@ describe('instance', () => { ], }); - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { Description: { 'Fn::Join': [ '', @@ -184,7 +183,7 @@ describe('instance', () => { }, }); - expect(stack).toHaveResource('AWS::SecretsManager::SecretTargetAttachment', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::SecretTargetAttachment', { SecretId: { Ref: 'InstanceSecret478E0A47', }, @@ -194,9 +193,7 @@ describe('instance', () => { TargetType: 'AWS::RDS::DBInstance', }); - expect(stack).toCountResources('Custom::LogRetention', 4); - - + Template.fromStack(stack).resourceCountIs('Custom::LogRetention', 4); }); test('throws when create database with specific AZ and multiAZ enabled', () => { @@ -239,7 +236,7 @@ describe('instance', () => { parameterGroup, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { DBParameterGroupName: { Ref: 'ParameterGroup5E32DECB', }, @@ -247,8 +244,52 @@ describe('instance', () => { Ref: 'OptionGroupACA43DC1', }, }); + }); + + test('instance with inline parameter group', () => { + // WHEN + new rds.DatabaseInstance(stack, 'Database', { + engine: rds.DatabaseInstanceEngine.sqlServerEe({ version: rds.SqlServerEngineVersion.VER_11 }), + vpc, + parameters: { + locks: '100', + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { + DBParameterGroupName: { + Ref: 'DatabaseParameterGroup2A921026', + }, + }); + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBParameterGroup', { + Family: 'sqlserver-ee-11.0', + Parameters: { + locks: '100', + }, + }); + }); + + test('instance with inline parameter group and parameterGroup arg fails', () => { + const parameterGroup = new rds.ParameterGroup(stack, 'ParameterGroup', { + engine: rds.DatabaseInstanceEngine.sqlServerEe({ + version: rds.SqlServerEngineVersion.VER_11, + }), + parameters: { + key: 'value', + }, + }); + expect(() => { + new rds.DatabaseInstance(stack, 'Database', { + engine: rds.DatabaseInstanceEngine.sqlServerEe({ version: rds.SqlServerEngineVersion.VER_11 }), + vpc, + parameters: { + locks: '100', + }, + parameterGroup, + }); + }).toThrow(/You cannot specify both parameterGroup and parameters/); }); test('can specify subnet type', () => { @@ -263,13 +304,13 @@ describe('instance', () => { }, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { DBSubnetGroupName: { Ref: 'InstanceSubnetGroupF2CBA54F', }, PubliclyAccessible: false, }); - expect(stack).toHaveResource('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBSubnetGroup', { DBSubnetGroupDescription: 'Subnet group for Instance database', SubnetIds: [ { @@ -280,8 +321,6 @@ describe('instance', () => { }, ], }); - - }); describe('DatabaseInstanceFromSnapshot', () => { @@ -293,11 +332,9 @@ describe('instance', () => { vpc, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { DBSnapshotIdentifier: 'my-snapshot', }); - - }); test('can generate a new snapshot password', () => { @@ -310,8 +347,8 @@ describe('instance', () => { }), }); - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { - MasterUsername: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { + MasterUsername: Match.absent(), MasterUserPassword: { 'Fn::Join': ['', [ '{{resolve:secretsmanager:', @@ -320,7 +357,7 @@ describe('instance', () => { ]], }, }); - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { Description: { 'Fn::Join': ['', ['Generated by the CDK for stack: ', { Ref: 'AWS::StackName' }]], }, @@ -331,8 +368,6 @@ describe('instance', () => { SecretStringTemplate: '{"username":"admin"}', }, }); - - }); test('fromGeneratedSecret with replica regions', () => { @@ -345,7 +380,7 @@ describe('instance', () => { }), }); - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { ReplicaRegions: [ { Region: 'eu-west-1', @@ -361,8 +396,6 @@ describe('instance', () => { vpc, credentials: { generatePassword: true }, })).toThrow(/`credentials` `username` must be specified when `generatePassword` is set to true/); - - }); test('can set a new snapshot password from an existing SecretValue', () => { @@ -374,12 +407,10 @@ describe('instance', () => { }); // TODO - Expect this to be broken - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { - MasterUsername: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { + MasterUsername: Match.absent(), MasterUserPassword: 'mysecretpassword', }); - - }); test('can set a new snapshot password from an existing Secret', () => { @@ -394,14 +425,12 @@ describe('instance', () => { credentials: rds.SnapshotCredentials.fromSecret(secret), }); - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { - MasterUsername: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { + MasterUsername: Match.absent(), MasterUserPassword: { 'Fn::Join': ['', ['{{resolve:secretsmanager:', { Ref: 'DBSecretD58955BC' }, ':SecretString:password::}}']], }, }); - - }); test('can create a new database instance with fromDatabaseInstanceAttributes using a token for the port', () => { @@ -425,9 +454,9 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveOutput({ - exportName: 'databaseUrl', - outputValue: { + Template.fromStack(stack).hasOutput('portOutput', { + Export: { Name: 'databaseUrl' }, + Value: { Ref: 'DatabasePort', }, }); @@ -449,7 +478,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { SourceDBInstanceIdentifier: { 'Fn::Join': ['', [ 'arn:', @@ -466,8 +495,6 @@ describe('instance', () => { Ref: 'ReadReplicaSubnetGroup680C605C', }, }); - - }); test('on event', () => { @@ -485,7 +512,7 @@ describe('instance', () => { instance.onEvent('InstanceEvent', { target: new targets.LambdaFunction(fn) }); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { EventPattern: { source: [ 'aws.rds', @@ -528,8 +555,6 @@ describe('instance', () => { }, ], }); - - }); test('on event without target', () => { @@ -542,7 +567,7 @@ describe('instance', () => { instance.onEvent('InstanceEvent'); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { EventPattern: { source: [ 'aws.rds', @@ -574,8 +599,6 @@ describe('instance', () => { ], }, }); - - }); test('can use metricCPUUtilization', () => { @@ -593,8 +616,6 @@ describe('instance', () => { period: cdk.Duration.minutes(5), statistic: 'Average', }); - - }); test('can resolve endpoint port and socket address', () => { @@ -618,8 +639,6 @@ describe('instance', () => { ], ], }); - - }); test('can deactivate backup', () => { @@ -631,11 +650,9 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { BackupRetentionPeriod: 0, }); - - }); test('imported instance with imported security group with allowAllOutbound set to false', () => { @@ -652,11 +669,9 @@ describe('instance', () => { instance.connections.allowToAnyIpv4(ec2.Port.tcp(443)); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: 'sg-123456789', }); - - }); test('create an instance with imported monitoring role', () => { @@ -676,14 +691,12 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { MonitoringInterval: 60, MonitoringRoleArn: { 'Fn::GetAtt': ['MonitoringRole90457BF9', 'Arn'], }, - }, ResourcePart.Properties); - - + }); }); test('create an instance with an existing security group', () => { @@ -700,11 +713,11 @@ describe('instance', () => { instance.connections.allowDefaultPortFromAnyIpv4(); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { VPCSecurityGroups: ['sg-123456789'], }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { FromPort: { 'Fn::GetAtt': [ 'InstanceC1063A87', @@ -719,8 +732,6 @@ describe('instance', () => { ], }, }); - - }); test('addRotationSingleUser()', () => { @@ -734,7 +745,7 @@ describe('instance', () => { instance.addRotationSingleUser(); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { SecretId: { Ref: 'DatabaseSecretAttachmentE5D1B020', }, @@ -762,7 +773,7 @@ describe('instance', () => { instance.addRotationMultiUser('user', { secret: userSecret.attach(instance) }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { SecretId: { Ref: 'UserSecretAttachment16ACBE6D', }, @@ -777,7 +788,7 @@ describe('instance', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Serverless::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::Serverless::Application', { Parameters: { masterSecretArn: { Ref: 'DatabaseSecretAttachmentE5D1B020', @@ -812,13 +823,13 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { RotationRules: { AutomaticallyAfterDays: 15, }, }); - expect(stack).toHaveResource('AWS::Serverless::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::Serverless::Application', { Parameters: { endpoint: { 'Fn::Join': ['', [ @@ -870,7 +881,7 @@ describe('instance', () => { instance.addRotationSingleUser({ endpoint }); // THEN - expect(stack).toHaveResource('AWS::Serverless::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::Serverless::Application', { Parameters: { endpoint: { 'Fn::Join': ['', [ @@ -910,8 +921,6 @@ describe('instance', () => { // THEN expect(() => instance.addRotationSingleUser()).toThrow(/without secret/); - - }); test('throws when trying to add single user rotation multiple times', () => { @@ -927,8 +936,6 @@ describe('instance', () => { // THEN expect(() => instance.addRotationSingleUser()).toThrow(/A single user rotation was already added to this instance/); - - }); test('throws when timezone is set for non-sqlserver database engine', () => { @@ -953,8 +960,6 @@ describe('instance', () => { vpc, })).toThrow(/timezone property can not be configured for/); }); - - }); test('create an instance from snapshot with maximum allocated storage', () => { @@ -967,12 +972,10 @@ describe('instance', () => { maxAllocatedStorage: 200, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { DBSnapshotIdentifier: 'my-snapshot', MaxAllocatedStorage: 200, }); - - }); test('create a DB instance with maximum allocated storage', () => { @@ -985,12 +988,10 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { BackupRetentionPeriod: 0, MaxAllocatedStorage: 250, }); - - }); test('iam authentication - off by default', () => { @@ -999,11 +1000,9 @@ describe('instance', () => { vpc, }); - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { - EnableIAMDatabaseAuthentication: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { + EnableIAMDatabaseAuthentication: Match.absent(), }); - - }); test('createGrant - creates IAM policy and enables IAM auth', () => { @@ -1016,10 +1015,10 @@ describe('instance', () => { }); instance.grantConnect(role); - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { EnableIAMDatabaseAuthentication: true, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [{ Effect: 'Allow', @@ -1031,8 +1030,6 @@ describe('instance', () => { Version: '2012-10-17', }, }); - - }); test('createGrant - throws if IAM auth disabled', () => { @@ -1046,8 +1043,6 @@ describe('instance', () => { }); expect(() => { instance.grantConnect(role); }).toThrow(/Cannot grant connect when IAM authentication is disabled/); - - }); test('domain - sets domain property', () => { @@ -1061,11 +1056,9 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { Domain: domain, }); - - }); test('domain - uses role if provided', () => { @@ -1081,12 +1074,10 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { Domain: domain, DomainIAMRoleName: stack.resolve(role.roleName), }); - - }); test('domain - creates role if not provided', () => { @@ -1100,12 +1091,12 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { Domain: domain, - DomainIAMRoleName: anything(), + DomainIAMRoleName: Match.anyValue(), }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -1133,8 +1124,6 @@ describe('instance', () => { }, ], }); - - }); test('throws when domain is set for mariadb database engine', () => { @@ -1161,8 +1150,6 @@ describe('instance', () => { vpc, })).toThrow(expectedError); }); - - }); describe('performance insights', () => { @@ -1175,13 +1162,11 @@ describe('instance', () => { performanceInsightEncryptionKey: new kms.Key(stack, 'Key'), }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { EnablePerformanceInsights: true, PerformanceInsightsRetentionPeriod: 731, PerformanceInsightsKMSKeyId: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, }); - - }); test('setting performance insights fields enables performance insights', () => { @@ -1191,12 +1176,10 @@ describe('instance', () => { performanceInsightRetention: rds.PerformanceInsightRetention.LONG_TERM, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { EnablePerformanceInsights: true, PerformanceInsightsRetentionPeriod: 731, }); - - }); test('throws if performance insights fields are set but performance insights is disabled', () => { @@ -1208,8 +1191,6 @@ describe('instance', () => { performanceInsightRetention: rds.PerformanceInsightRetention.DEFAULT, }); }).toThrow(/`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set/); - - }); }); @@ -1220,12 +1201,10 @@ describe('instance', () => { subnetGroup: rds.SubnetGroup.fromSubnetGroupName(stack, 'SubnetGroup', 'my-subnet-group'), }); - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { DBSubnetGroupName: 'my-subnet-group', }); - expect(stack).toCountResources('AWS::RDS::DBSubnetGroup', 0); - - + Template.fromStack(stack).resourceCountIs('AWS::RDS::DBSubnetGroup', 0); }); test('defaultChild returns the DB Instance', () => { @@ -1236,8 +1215,6 @@ describe('instance', () => { // THEN expect(instance.node.defaultChild instanceof rds.CfnDBInstance).toBeTruthy(); - - }); test("PostgreSQL database instance uses a different default master username than 'admin', which is a reserved word", () => { @@ -1249,13 +1226,11 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { GenerateSecretString: { SecretStringTemplate: '{"username":"postgres"}', }, }); - - }); describe('S3 Import/Export', () => { @@ -1269,7 +1244,7 @@ describe('instance', () => { s3ExportBuckets: [new s3.Bucket(stack, 'S3Export')], }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { AssociatedRoles: [ { FeatureName: 'S3_INTEGRATION', @@ -1280,7 +1255,7 @@ describe('instance', () => { }); // Can read from import bucket, and read/write from export bucket - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [{ Action: [ @@ -1301,6 +1276,10 @@ describe('instance', () => { 's3:List*', 's3:DeleteObject*', 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', 's3:Abort*', ], Effect: 'Allow', @@ -1312,8 +1291,6 @@ describe('instance', () => { Version: '2012-10-17', }, }); - - }); test('throws if using s3 import on unsupported engine', () => { @@ -1335,8 +1312,6 @@ describe('instance', () => { s3ImportRole, }); }).toThrow(/Engine 'mysql-8.0.19' does not support S3 import/); - - }); test('throws if using s3 export on unsupported engine', () => { @@ -1358,8 +1333,6 @@ describe('instance', () => { s3ExportRole: s3ExportRole, }); }).toThrow(/Engine 'mysql-8.0.19' does not support S3 export/); - - }); test('throws if provided two different roles for import/export', () => { @@ -1378,8 +1351,6 @@ describe('instance', () => { s3ExportRole, }); }).toThrow(/S3 import and export roles must be the same/); - - }); }); @@ -1392,7 +1363,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { MasterUsername: 'postgres', // username is a string MasterUserPassword: { 'Fn::Join': [ @@ -1420,7 +1391,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { ReplicaRegions: [ { Region: 'eu-west-1', @@ -1438,7 +1409,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { MasterUsername: 'postgres', // username is a string MasterUserPassword: '{{resolve:ssm-secure:/dbPassword:1}}', // reference to SSM }); @@ -1460,7 +1431,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { Name: secretName, }); }); @@ -1477,7 +1448,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { Name: secretName, }); }); @@ -1494,11 +1465,9 @@ describe('instance', () => { publiclyAccessible: false, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { PubliclyAccessible: false, }); - - }); test('can set publiclyAccessible to true with private subnets', () => { @@ -1513,7 +1482,7 @@ describe('instance', () => { publiclyAccessible: true, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { PubliclyAccessible: true, }); }); @@ -1537,7 +1506,7 @@ describe('instance', () => { } ); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { DBInstanceIdentifier: instanceIdentifier.toLowerCase(), }); }); @@ -1559,7 +1528,7 @@ describe('instance', () => { } ); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { DBInstanceIdentifier: instanceIdentifier, }); }); @@ -1605,7 +1574,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { DBParameterGroupName: { Ref: 'ParameterGroup5E32DECB', }, @@ -1622,7 +1591,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { Port: '3306', }); }); @@ -1642,7 +1611,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { Port: { Ref: 'Port', }, @@ -1652,8 +1621,8 @@ describe('instance', () => { test.each([ [cdk.RemovalPolicy.RETAIN, 'Retain', 'Retain'], - [cdk.RemovalPolicy.SNAPSHOT, 'Snapshot', ABSENT], - [cdk.RemovalPolicy.DESTROY, 'Delete', ABSENT], + [cdk.RemovalPolicy.SNAPSHOT, 'Snapshot', Match.absent()], + [cdk.RemovalPolicy.DESTROY, 'Delete', Match.absent()], ])('if Instance RemovalPolicy is \'%s\', the instance has DeletionPolicy \'%s\' and the DBSubnetGroup has \'%s\'', (instanceRemovalPolicy, instanceValue, subnetValue) => { // GIVEN stack = new cdk.Stack(); @@ -1670,15 +1639,15 @@ test.each([ }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResource('AWS::RDS::DBInstance', { DeletionPolicy: instanceValue, UpdateReplacePolicy: instanceValue, - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResourceLike('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResource('AWS::RDS::DBSubnetGroup', { DeletionPolicy: subnetValue, UpdateReplacePolicy: subnetValue, - }, ResourcePart.CompleteDefinition); + }); }); describe('cross-account instance', () => { @@ -1707,7 +1676,7 @@ describe('cross-account instance', () => { value: instance.instanceIdentifier, }); - expect(outputStack).toMatchTemplate({ + Template.fromStack(outputStack).templateMatches({ Outputs: { DatabaseInstanceArn: { Value: { diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json index a2631bd5991c8..d3127f0b8fdd9 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json @@ -548,6 +548,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-rds/test/integ.instance-s3-postgres.expected.json b/packages/@aws-cdk/aws-rds/test/integ.instance-s3-postgres.expected.json index c623afaf92464..f811978275863 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.instance-s3-postgres.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.instance-s3-postgres.expected.json @@ -455,6 +455,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-rds/test/integ.instance-s3.expected.json b/packages/@aws-cdk/aws-rds/test/integ.instance-s3.expected.json index 5b851784caef0..f379bde6663f7 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.instance-s3.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.instance-s3.expected.json @@ -454,6 +454,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-rds/test/integ.serverless-cluster-no-vpc.expected.json b/packages/@aws-cdk/aws-rds/test/integ.serverless-cluster-no-vpc.expected.json new file mode 100644 index 0000000000000..7d81d78c4e34f --- /dev/null +++ b/packages/@aws-cdk/aws-rds/test/integ.serverless-cluster-no-vpc.expected.json @@ -0,0 +1,18 @@ +{ + "Resources": { + "ServerlessDatabaseWithoutVPC93F9A752": { + "Type": "AWS::RDS::DBCluster", + "Properties": { + "Engine": "aurora-mysql", + "DBClusterParameterGroupName": "default.aurora-mysql5.7", + "EngineMode": "serverless", + "MasterUsername": "admin", + "MasterUserPassword": "7959866cacc02c2d243ecfe177464fe6", + "StorageEncrypted": true, + "VpcSecurityGroupIds": [] + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-rds/test/integ.serverless-cluster-no-vpc.ts b/packages/@aws-cdk/aws-rds/test/integ.serverless-cluster-no-vpc.ts new file mode 100644 index 0000000000000..807cf1a894601 --- /dev/null +++ b/packages/@aws-cdk/aws-rds/test/integ.serverless-cluster-no-vpc.ts @@ -0,0 +1,17 @@ +import * as cdk from '@aws-cdk/core'; +import * as rds from '../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-sls-cluster-no-vpc-integ'); + +const cluster = new rds.ServerlessCluster(stack, 'Serverless Database Without VPC', { + engine: rds.DatabaseClusterEngine.AURORA_MYSQL, + credentials: { + username: 'admin', + password: cdk.SecretValue.plainText('7959866cacc02c2d243ecfe177464fe6'), + }, + removalPolicy: cdk.RemovalPolicy.DESTROY, +}); +cluster.connections.allowDefaultPortFromAnyIpv4('Open to the world'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-rds/test/option-group.test.ts b/packages/@aws-cdk/aws-rds/test/option-group.test.ts index a0ab91ac0dfdf..5e8fb5b9f5261 100644 --- a/packages/@aws-cdk/aws-rds/test/option-group.test.ts +++ b/packages/@aws-cdk/aws-rds/test/option-group.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import { DatabaseInstanceEngine, OptionGroup, OracleEngineVersion } from '../lib'; @@ -21,7 +21,7 @@ describe('option group', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::OptionGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::OptionGroup', { EngineName: 'oracle-se2', MajorEngineVersion: '12.1', OptionConfigurations: [ @@ -30,8 +30,6 @@ describe('option group', () => { }, ], }); - - }); test('option group with new security group', () => { @@ -55,7 +53,7 @@ describe('option group', () => { optionGroup.optionConnections.OEM.connections.allowDefaultPortFromAnyIpv4(); // THEN - expect(stack).toHaveResource('AWS::RDS::OptionGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::OptionGroup', { OptionConfigurations: [ { OptionName: 'OEM', @@ -72,7 +70,7 @@ describe('option group', () => { ], }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Security group for OEM option', SecurityGroupIngress: [ { @@ -87,8 +85,6 @@ describe('option group', () => { Ref: 'VPCB9E5F0B4', }, }); - - }); test('option group with existing security group', () => { @@ -113,7 +109,7 @@ describe('option group', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::OptionGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::OptionGroup', { OptionConfigurations: [ { OptionName: 'OEM', @@ -129,8 +125,6 @@ describe('option group', () => { }, ], }); - - }); test('throws when using an option with port and no vpc', () => { @@ -149,7 +143,5 @@ describe('option group', () => { }, ], })).toThrow(/`port`.*`vpc`/); - - }); }); diff --git a/packages/@aws-cdk/aws-rds/test/parameter-group.test.ts b/packages/@aws-cdk/aws-rds/test/parameter-group.test.ts index bf8e789aee0ad..594fab914310a 100644 --- a/packages/@aws-cdk/aws-rds/test/parameter-group.test.ts +++ b/packages/@aws-cdk/aws-rds/test/parameter-group.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import { DatabaseClusterEngine, ParameterGroup } from '../lib'; @@ -17,10 +17,8 @@ describe('parameter group', () => { }); // THEN - expect(stack).toCountResources('AWS::RDS::DBParameterGroup', 0); - expect(stack).toCountResources('AWS::RDS::DBClusterParameterGroup', 0); - - + Template.fromStack(stack).resourceCountIs('AWS::RDS::DBParameterGroup', 0); + Template.fromStack(stack).resourceCountIs('AWS::RDS::DBClusterParameterGroup', 0); }); test('create a parameter group when bound to an instance', () => { @@ -38,15 +36,13 @@ describe('parameter group', () => { parameterGroup.bindToInstance({}); // THEN - expect(stack).toHaveResource('AWS::RDS::DBParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBParameterGroup', { Description: 'desc', Family: 'aurora5.6', Parameters: { key: 'value', }, }); - - }); test('create a parameter group when bound to a cluster', () => { @@ -64,15 +60,13 @@ describe('parameter group', () => { parameterGroup.bindToCluster({}); // THEN - expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBClusterParameterGroup', { Description: 'desc', Family: 'aurora5.6', Parameters: { key: 'value', }, }); - - }); test('creates 2 parameter groups when bound to a cluster and an instance', () => { @@ -91,10 +85,8 @@ describe('parameter group', () => { parameterGroup.bindToInstance({}); // THEN - expect(stack).toCountResources('AWS::RDS::DBParameterGroup', 1); - expect(stack).toCountResources('AWS::RDS::DBClusterParameterGroup', 1); - - + Template.fromStack(stack).resourceCountIs('AWS::RDS::DBParameterGroup', 1); + Template.fromStack(stack).resourceCountIs('AWS::RDS::DBClusterParameterGroup', 1); }); test('Add an additional parameter to an existing parameter group', () => { @@ -114,7 +106,7 @@ describe('parameter group', () => { clusterParameterGroup.addParameter('key2', 'value2'); // THEN - expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBClusterParameterGroup', { Description: 'desc', Family: 'aurora5.6', Parameters: { @@ -122,7 +114,5 @@ describe('parameter group', () => { key2: 'value2', }, }); - - }); }); diff --git a/packages/@aws-cdk/aws-rds/test/proxy.test.ts b/packages/@aws-cdk/aws-rds/test/proxy.test.ts index 9cd48e7686dd9..f98e36bdf3647 100644 --- a/packages/@aws-cdk/aws-rds/test/proxy.test.ts +++ b/packages/@aws-cdk/aws-rds/test/proxy.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT, ResourcePart } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import { AccountPrincipal, Role } from '@aws-cdk/aws-iam'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; @@ -15,8 +14,6 @@ describe('proxy', () => { beforeEach(() => { stack = new cdk.Stack(); vpc = new ec2.Vpc(stack, 'VPC'); - - }); test('create a DB proxy from an instance', () => { @@ -36,7 +33,7 @@ describe('proxy', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBProxy', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBProxy', { Auth: [ { AuthScheme: 'SECRETS', @@ -66,7 +63,7 @@ describe('proxy', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBProxyTargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBProxyTargetGroup', { DBProxyName: { Ref: 'ProxyCB0DFB71', }, @@ -78,8 +75,6 @@ describe('proxy', () => { ], TargetGroupName: 'default', }); - - }); test('create a DB proxy from a cluster', () => { @@ -99,7 +94,7 @@ describe('proxy', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBProxy', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBProxy', { Auth: [ { AuthScheme: 'SECRETS', @@ -127,7 +122,7 @@ describe('proxy', () => { }, ], }); - expect(stack).toHaveResourceLike('AWS::RDS::DBProxyTargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBProxyTargetGroup', { DBProxyName: { Ref: 'ProxyCB0DFB71', }, @@ -137,10 +132,10 @@ describe('proxy', () => { Ref: 'DatabaseB269D8BB', }, ], - DBInstanceIdentifiers: ABSENT, + DBInstanceIdentifiers: Match.absent(), TargetGroupName: 'default', }); - expect(stack).toHaveResourceLike('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { IpProtocol: 'tcp', Description: 'Allow connections to the database Cluster from the Proxy', FromPort: { @@ -156,8 +151,6 @@ describe('proxy', () => { 'Fn::GetAtt': ['DatabaseB269D8BB', 'Endpoint.Port'], }, }); - - }); test('One or more secrets are required.', () => { @@ -175,8 +168,6 @@ describe('proxy', () => { vpc, }); }).toThrow('One or more secrets are required.'); - - }); test('fails when trying to create a proxy for a target without an engine', () => { @@ -191,8 +182,6 @@ describe('proxy', () => { secrets: [new secretsmanager.Secret(stack, 'Secret')], }); }).toThrow(/Could not determine engine for proxy target 'Default\/Cluster'\. Please provide it explicitly when importing the resource/); - - }); test("fails when trying to create a proxy for a target with an engine that doesn't have engineFamily", () => { @@ -213,8 +202,6 @@ describe('proxy', () => { secrets: [new secretsmanager.Secret(stack, 'Secret')], }); }).toThrow(/Engine 'mariadb-10\.0\.24' does not support proxies/); - - }); test('correctly creates a proxy for an imported Cluster if its engine is known', () => { @@ -232,20 +219,18 @@ describe('proxy', () => { secrets: [new secretsmanager.Secret(stack, 'Secret')], }); - expect(stack).toHaveResourceLike('AWS::RDS::DBProxy', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBProxy', { EngineFamily: 'POSTGRESQL', }); - expect(stack).toHaveResourceLike('AWS::RDS::DBProxyTargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBProxyTargetGroup', { DBClusterIdentifiers: [ 'my-cluster', ], }); - expect(stack).toHaveResourceLike('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'SecurityGroup for Database Proxy', VpcId: { Ref: 'VPCB9E5F0B4' }, }); - - }); describe('imported Proxies', () => { @@ -256,8 +241,6 @@ describe('proxy', () => { endpoint: 'my-endpoint', securityGroups: [], }); - - }); test('grant rds-db:connect in grantConnect() with a dbUser explicitly passed', () => { @@ -269,7 +252,7 @@ describe('proxy', () => { importedDbProxy.grantConnect(role, databaseUser); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [{ Effect: 'Allow', @@ -289,8 +272,6 @@ describe('proxy', () => { Version: '2012-10-17', }, }); - - }); test('throws when grantConnect() is used without a dbUser', () => { @@ -303,8 +284,6 @@ describe('proxy', () => { expect(() => { importedDbProxy.grantConnect(role); }).toThrow(/For imported Database Proxies, the dbUser is required in grantConnect/); - - }); }); @@ -328,7 +307,7 @@ describe('proxy', () => { proxy.grantConnect(role); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [{ Effect: 'Allow', @@ -362,8 +341,6 @@ describe('proxy', () => { Version: '2012-10-17', }, }); - - }); test('new Proxy with multiple Secrets cannot use grantConnect() without a dbUser passed', () => { @@ -391,8 +368,6 @@ describe('proxy', () => { expect(() => { proxy.grantConnect(role); }).toThrow(/When the Proxy contains multiple Secrets, you must pass a dbUser explicitly to grantConnect/); - - }); test('DBProxyTargetGroup should have dependency on the proxy targets', () => { @@ -412,7 +387,7 @@ describe('proxy', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBProxyTargetGroup', { + Template.fromStack(stack).hasResource('AWS::RDS::DBProxyTargetGroup', { Properties: { DBProxyName: { Ref: 'proxy3A1DA9C7', @@ -429,8 +404,6 @@ describe('proxy', () => { 'clusterSecurityGroupF441DCEA', 'clusterSubnets81E3593F', ], - }, ResourcePart.CompleteDefinition); - - + }); }); }); diff --git a/packages/@aws-cdk/aws-rds/test/serverless-cluster-from-snapshot.test.ts b/packages/@aws-cdk/aws-rds/test/serverless-cluster-from-snapshot.test.ts index 4953fa6da7a71..489cd91ae861d 100644 --- a/packages/@aws-cdk/aws-rds/test/serverless-cluster-from-snapshot.test.ts +++ b/packages/@aws-cdk/aws-rds/test/serverless-cluster-from-snapshot.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT, ResourcePart } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as kms from '@aws-cdk/aws-kms'; import * as cdk from '@aws-cdk/core'; @@ -18,7 +17,7 @@ describe('serverless cluster from snapshot', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResource('AWS::RDS::DBCluster', { Properties: { Engine: 'aurora-mysql', DBClusterParameterGroupName: 'default.aurora-mysql5.7', @@ -39,7 +38,7 @@ describe('serverless cluster from snapshot', () => { }, DeletionPolicy: 'Snapshot', UpdateReplacePolicy: 'Snapshot', - }, ResourcePart.CompleteDefinition); + }); }); test('can generate a new snapshot password', () => { @@ -57,8 +56,8 @@ describe('serverless cluster from snapshot', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { - MasterUsername: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { + MasterUsername: Match.absent(), MasterUserPassword: { 'Fn::Join': ['', [ '{{resolve:secretsmanager:', @@ -67,7 +66,7 @@ describe('serverless cluster from snapshot', () => { ]], }, }); - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { Description: { 'Fn::Join': ['', ['Generated by the CDK for stack: ', { Ref: 'AWS::StackName' }]], }, @@ -95,7 +94,7 @@ describe('serverless cluster from snapshot', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { ReplicaRegions: [ { Region: 'eu-west-1', @@ -130,8 +129,8 @@ describe('serverless cluster from snapshot', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { - MasterUsername: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { + MasterUsername: Match.absent(), MasterUserPassword: 'mysecretpassword', }); }); @@ -153,8 +152,8 @@ describe('serverless cluster from snapshot', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { - MasterUsername: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { + MasterUsername: Match.absent(), MasterUserPassword: { 'Fn::Join': ['', ['{{resolve:secretsmanager:', { Ref: 'DBSecretD58955BC' }, ':SecretString:password::}}']], }, @@ -162,8 +161,8 @@ describe('serverless cluster from snapshot', () => { }); }); -function testStack(app?: cdk.App, id?: string): cdk.Stack { - const stack = new cdk.Stack(app, id, { env: { account: '12345', region: 'us-test-1' } }); +function testStack(): cdk.Stack { + const stack = new cdk.Stack(undefined, undefined, { env: { account: '12345', region: 'us-test-1' } }); stack.node.setContext('availability-zones:12345:us-test-1', ['us-test-1a', 'us-test-1b']); return stack; } diff --git a/packages/@aws-cdk/aws-rds/test/serverless-cluster.test.ts b/packages/@aws-cdk/aws-rds/test/serverless-cluster.test.ts index dff8d035b78a3..a55a23df02421 100644 --- a/packages/@aws-cdk/aws-rds/test/serverless-cluster.test.ts +++ b/packages/@aws-cdk/aws-rds/test/serverless-cluster.test.ts @@ -1,11 +1,10 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart, SynthUtils } from '@aws-cdk/assert-internal'; +import { Template, Match } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import { AuroraPostgresEngineVersion, ServerlessCluster, DatabaseClusterEngine, ParameterGroup, AuroraCapacityUnit, DatabaseSecret } from '../lib'; +import { AuroraPostgresEngineVersion, ServerlessCluster, DatabaseClusterEngine, ParameterGroup, AuroraCapacityUnit, DatabaseSecret, SubnetGroup } from '../lib'; describe('serverless cluster', () => { test('can create a Serverless Cluster with Aurora Postgres database engine', () => { @@ -25,7 +24,7 @@ describe('serverless cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResource('AWS::RDS::DBCluster', { Properties: { Engine: 'aurora-postgresql', DBClusterParameterGroupName: 'default.aurora-postgresql10', @@ -47,9 +46,7 @@ describe('serverless cluster', () => { }, DeletionPolicy: 'Snapshot', UpdateReplacePolicy: 'Snapshot', - }, ResourcePart.CompleteDefinition); - - + }); }); test('can create a Serverless Cluster with Aurora Mysql database engine', () => { @@ -64,7 +61,7 @@ describe('serverless cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResource('AWS::RDS::DBCluster', { Properties: { Engine: 'aurora-mysql', DBClusterParameterGroupName: 'default.aurora-mysql5.7', @@ -73,28 +70,18 @@ describe('serverless cluster', () => { }, EngineMode: 'serverless', MasterUsername: { - 'Fn::Join': [ - '', - [ - '{{resolve:secretsmanager:', - { - Ref: 'ServerlessDatabaseSecret1C9BF4F1', - }, - ':SecretString:username::}}', - ], - ], + 'Fn::Join': ['', [ + '{{resolve:secretsmanager:', + { Ref: 'ServerlessDatabaseSecret1C9BF4F1' }, + ':SecretString:username::}}', + ]], }, MasterUserPassword: { - 'Fn::Join': [ - '', - [ - '{{resolve:secretsmanager:', - { - Ref: 'ServerlessDatabaseSecret1C9BF4F1', - }, - ':SecretString:password::}}', - ], - ], + 'Fn::Join': ['', [ + '{{resolve:secretsmanager:', + { Ref: 'ServerlessDatabaseSecret1C9BF4F1' }, + ':SecretString:password::}}', + ]], }, StorageEncrypted: true, VpcSecurityGroupIds: [ @@ -108,8 +95,7 @@ describe('serverless cluster', () => { }, DeletionPolicy: 'Snapshot', UpdateReplacePolicy: 'Snapshot', - }, ResourcePart.CompleteDefinition); - + }); }); test('can create a Serverless cluster with imported vpc and security group', () => { @@ -129,39 +115,27 @@ describe('serverless cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { Engine: 'aurora-postgresql', DBClusterParameterGroupName: 'default.aurora-postgresql10', EngineMode: 'serverless', DBSubnetGroupName: { Ref: 'DatabaseSubnets56F17B9A' }, MasterUsername: { - 'Fn::Join': [ - '', - [ - '{{resolve:secretsmanager:', - { - Ref: 'DatabaseSecret3B817195', - }, - ':SecretString:username::}}', - ], - ], + 'Fn::Join': ['', [ + '{{resolve:secretsmanager:', + { Ref: 'DatabaseSecret3B817195' }, + ':SecretString:username::}}', + ]], }, MasterUserPassword: { - 'Fn::Join': [ - '', - [ - '{{resolve:secretsmanager:', - { - Ref: 'DatabaseSecret3B817195', - }, - ':SecretString:password::}}', - ], - ], + 'Fn::Join': ['', [ + '{{resolve:secretsmanager:', + { Ref: 'DatabaseSecret3B817195' }, + ':SecretString:password::}}', + ]], }, VpcSecurityGroupIds: ['SecurityGroupId12345'], }); - - }); test("sets the retention policy of the SubnetGroup to 'Retain' if the Serverless Cluster is created with 'Retain'", () => { @@ -174,12 +148,10 @@ describe('serverless cluster', () => { removalPolicy: cdk.RemovalPolicy.RETAIN, }); - expect(stack).toHaveResourceLike('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResource('AWS::RDS::DBSubnetGroup', { DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition); - - + }); }); test('creates a secret when master credentials are not specified', () => { @@ -198,34 +170,24 @@ describe('serverless cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { MasterUsername: { - 'Fn::Join': [ - '', - [ - '{{resolve:secretsmanager:', - { - Ref: 'DatabaseSecret3B817195', - }, - ':SecretString:username::}}', - ], - ], + 'Fn::Join': ['', [ + '{{resolve:secretsmanager:', + { Ref: 'DatabaseSecret3B817195' }, + ':SecretString:username::}}', + ]], }, MasterUserPassword: { - 'Fn::Join': [ - '', - [ - '{{resolve:secretsmanager:', - { - Ref: 'DatabaseSecret3B817195', - }, - ':SecretString:password::}}', - ], - ], + 'Fn::Join': ['', [ + '{{resolve:secretsmanager:', + { Ref: 'DatabaseSecret3B817195' }, + ':SecretString:password::}}', + ]], }, }); - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { GenerateSecretString: { ExcludeCharacters: '"@/\\', GenerateStringKey: 'password', @@ -233,8 +195,6 @@ describe('serverless cluster', () => { SecretStringTemplate: '{"username":"myuser"}', }, }); - - }); test('create an Serverless cluster with custom KMS key for storage', () => { @@ -250,7 +210,7 @@ describe('serverless cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { KmsKeyId: { 'Fn::GetAtt': [ 'Key961B73FD', @@ -258,8 +218,6 @@ describe('serverless cluster', () => { ], }, }); - - }); test('create a cluster using a specific version of Postgresql', () => { @@ -276,13 +234,11 @@ describe('serverless cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { Engine: 'aurora-postgresql', EngineMode: 'serverless', EngineVersion: '10.7', }); - - }); test('cluster exposes different read and write endpoints', () => { @@ -302,8 +258,6 @@ describe('serverless cluster', () => { // THEN expect(stack.resolve(cluster.clusterEndpoint)).not .toEqual(stack.resolve(cluster.clusterReadEndpoint)); - - }); test('imported cluster with imported security group honors allowAllOutbound', () => { @@ -324,11 +278,9 @@ describe('serverless cluster', () => { cluster.connections.allowToAnyIpv4(ec2.Port.tcp(443)); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: 'sg-123456789', }); - - }); test('can import a serverless cluster with minimal attributes', () => { @@ -339,8 +291,6 @@ describe('serverless cluster', () => { }); expect(cluster.clusterIdentifier).toEqual('identifier'); - - }); test('minimal imported cluster throws on accessing attributes for missing parameters', () => { @@ -352,8 +302,6 @@ describe('serverless cluster', () => { expect(() => cluster.clusterEndpoint).toThrow(/Cannot access `clusterEndpoint` of an imported cluster/); expect(() => cluster.clusterReadEndpoint).toThrow(/Cannot access `clusterReadEndpoint` of an imported cluster/); - - }); test('imported cluster can access properties if attributes are provided', () => { @@ -371,11 +319,9 @@ describe('serverless cluster', () => { expect(cluster.clusterEndpoint.socketAddress).toEqual('addr:3306'); expect(cluster.clusterReadEndpoint.socketAddress).toEqual('reader-address:3306'); - - }); - test('throws when trying to add rotation to a serverless cluster without secret', () => { + test('throws when trying to add single-user rotation to a serverless cluster without secret', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -392,8 +338,6 @@ describe('serverless cluster', () => { // THEN expect(() => cluster.addRotationSingleUser()).toThrow(/without secret/); - - }); test('throws when trying to add single user rotation multiple times', () => { @@ -411,8 +355,39 @@ describe('serverless cluster', () => { // THEN expect(() => cluster.addRotationSingleUser()).toThrow(/A single user rotation was already added to this cluster/); + }); + + test('throws when trying to add single-user rotation to a serverless cluster without VPC', () => { + // GIVEN + const stack = new cdk.Stack(); + // WHEN + const cluster = new ServerlessCluster(stack, 'Database', { + engine: DatabaseClusterEngine.AURORA_MYSQL, + }); + // THEN + expect(() => { + cluster.addRotationSingleUser(); + }).toThrow(/Cannot add single user rotation for a cluster without VPC/); + }); + + test('throws when trying to add multi-user rotation to a serverless cluster without VPC', () => { + // GIVEN + const stack = new cdk.Stack(); + const secret = new DatabaseSecret(stack, 'Secret', { + username: 'admin', + }); + + // WHEN + const cluster = new ServerlessCluster(stack, 'Database', { + engine: DatabaseClusterEngine.AURORA_MYSQL, + }); + + // THEN + expect(() => { + cluster.addRotationMultiUser('someId', { secret }); + }).toThrow(/Cannot add multi user rotation for a cluster without VPC/); }); test('can set deletion protection', () => { @@ -428,11 +403,9 @@ describe('serverless cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { DeletionProtection: true, }); - - }); test('can set backup retention', () => { @@ -448,16 +421,15 @@ describe('serverless cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { BackupRetentionPeriod: 2, }); - - }); test('does not throw (but adds a node error) if a (dummy) VPC does not have sufficient subnets', () => { // GIVEN - const stack = testStack(); + const app = new cdk.App(); + const stack = testStack(app, 'TestStack'); const vpc = ec2.Vpc.fromLookup(stack, 'VPC', { isDefault: true }); // WHEN @@ -470,11 +442,9 @@ describe('serverless cluster', () => { }); // THEN - const art = SynthUtils.synthesize(stack); + const art = app.synth().getStackArtifact('TestStack'); const meta = art.findMetadataByType('aws:cdk:error'); expect(meta[0].data).toEqual('Cluster requires at least 2 subnets, got 0'); - - }); test('can set scaling configuration', () => { @@ -494,7 +464,7 @@ describe('serverless cluster', () => { }); //THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { ScalingConfiguration: { AutoPause: true, MaxCapacity: 128, @@ -502,8 +472,6 @@ describe('serverless cluster', () => { SecondsUntilAutoPause: 600, }, }); - - }); test('can enable Data API', () => { @@ -519,11 +487,9 @@ describe('serverless cluster', () => { }); //THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { EnableHttpEndpoint: true, }); - - }); test('default scaling options', () => { @@ -539,13 +505,11 @@ describe('serverless cluster', () => { }); //THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { ScalingConfiguration: { AutoPause: true, }, }); - - }); test('auto pause is disabled if a time of zero is specified', () => { @@ -563,13 +527,11 @@ describe('serverless cluster', () => { }); //THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { ScalingConfiguration: { AutoPause: false, }, }); - - }); test('throws when invalid auto pause time is specified', () => { @@ -595,8 +557,6 @@ describe('serverless cluster', () => { autoPause: cdk.Duration.days(2), }, })).toThrow(/auto pause time must be between 5 minutes and 1 day./); - - }); test('throws when invalid backup retention period is specified', () => { @@ -618,8 +578,6 @@ describe('serverless cluster', () => { vpc, backupRetention: cdk.Duration.days(36), })).toThrow(/backup retention period must be between 1 and 35 days. received: 36/); - - }); test('throws error when min capacity is greater than max capacity', () => { @@ -637,8 +595,6 @@ describe('serverless cluster', () => { maxCapacity: AuroraCapacityUnit.ACU_1, }, })).toThrow(/maximum capacity must be greater than or equal to minimum capacity./); - - }); test('check that clusterArn property works', () => { @@ -659,17 +615,13 @@ describe('serverless cluster', () => { // THEN expect(stack.resolve(cluster.clusterArn)).toEqual({ - 'Fn::Join': [ - '', - [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':rds:us-test-1:12345:cluster:', - { Ref: 'DatabaseB269D8BB' }, - ], - ], + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':rds:us-test-1:12345:cluster:', + { Ref: 'DatabaseB269D8BB' }, + ]], }); - }); test('can grant Data API access', () => { @@ -687,7 +639,7 @@ describe('serverless cluster', () => { cluster.grantDataApiAccess(user); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -721,8 +673,6 @@ describe('serverless cluster', () => { }, ], }); - - }); test('can grant Data API access on imported cluster with given secret', () => { @@ -742,7 +692,7 @@ describe('serverless cluster', () => { cluster.grantDataApiAccess(user); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -776,8 +726,6 @@ describe('serverless cluster', () => { }, ], }); - - }); test('grant Data API access enables the Data API', () => { @@ -794,11 +742,9 @@ describe('serverless cluster', () => { cluster.grantDataApiAccess(user); //THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { EnableHttpEndpoint: true, }); - - }); test('grant Data API access throws if the Data API is disabled', () => { @@ -814,8 +760,6 @@ describe('serverless cluster', () => { // WHEN expect(() => cluster.grantDataApiAccess(user)).toThrow(/Cannot grant Data API access when the Data API is disabled/); - - }); test('changes the case of the cluster identifier if the lowercaseDbIdentifier feature flag is enabled', () => { @@ -835,11 +779,9 @@ describe('serverless cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { DBClusterIdentifier: clusterIdentifier.toLowerCase(), }); - - }); test('does not change the case of the cluster identifier if the lowercaseDbIdentifier feature flag is disabled', () => { @@ -857,11 +799,68 @@ describe('serverless cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { DBClusterIdentifier: clusterIdentifier, }); + }); + test('can create a Serverless cluster without VPC', () => { + // GIVEN + const stack = testStack(); + + // WHEN + new ServerlessCluster(stack, 'Database', { + engine: DatabaseClusterEngine.AURORA_MYSQL, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { + Engine: 'aurora-mysql', + EngineMode: 'serverless', + DbSubnetGroupName: Match.absent(), + VpcSecurityGroupIds: [], + }); + }); + test('cannot create a Serverless cluster without VPC but specifying a security group', () => { + // GIVEN + const stack = testStack(); + const sg = ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'SecurityGroupId12345'); + + // THEN + expect(() => new ServerlessCluster(stack, 'Database', { + engine: DatabaseClusterEngine.AURORA_MYSQL, + securityGroups: [sg], + })).toThrow(/A VPC is required to use securityGroups in ServerlessCluster. Please add a VPC or remove securityGroups/); + }); + + test('cannot create a Serverless cluster without VPC but specifying a subnet group', () => { + // GIVEN + const stack = testStack(); + const SubnetGroupName = 'SubnetGroupId12345'; + const subnetGroup = SubnetGroup.fromSubnetGroupName(stack, 'SubnetGroup12345', SubnetGroupName); + + // THEN + expect(() => new ServerlessCluster(stack, 'Database', { + engine: DatabaseClusterEngine.AURORA_MYSQL, + subnetGroup, + })).toThrow(/A VPC is required to use subnetGroup in ServerlessCluster. Please add a VPC or remove subnetGroup/); + }); + + test('cannot create a Serverless cluster without VPC but specifying VPC subnets', () => { + // GIVEN + const stack = testStack(); + + // WHEN + const vpcSubnets = { + subnetName: 'AVpcSubnet', + }; + + // THEN + expect(() => new ServerlessCluster(stack, 'Database', { + engine: DatabaseClusterEngine.AURORA_MYSQL, + vpcSubnets, + })).toThrow(/A VPC is required to use vpcSubnets in ServerlessCluster. Please add a VPC or remove vpcSubnets/); }); }); diff --git a/packages/@aws-cdk/aws-rds/test/sql-server/sql-server.instance-engine.test.ts b/packages/@aws-cdk/aws-rds/test/sql-server/sql-server.instance-engine.test.ts index 72615d08f2093..a591dfd4a7bf1 100644 --- a/packages/@aws-cdk/aws-rds/test/sql-server/sql-server.instance-engine.test.ts +++ b/packages/@aws-cdk/aws-rds/test/sql-server/sql-server.instance-engine.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as core from '@aws-cdk/core'; import * as rds from '../../lib'; @@ -12,11 +12,9 @@ describe('sql server instance engine', () => { }), }).bindToInstance({}); - expect(stack).toHaveResourceLike('AWS::RDS::DBParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBParameterGroup', { Family: 'sqlserver-web-11.0', }); - - }); test("has MajorEngineVersion ending in '11.00' for major version 11", () => { @@ -35,11 +33,9 @@ describe('sql server instance engine', () => { ], }); - expect(stack).toHaveResourceLike('AWS::RDS::OptionGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::OptionGroup', { MajorEngineVersion: '11.00', }); - - }); }); }); diff --git a/packages/@aws-cdk/aws-rds/test/subnet-group.test.ts b/packages/@aws-cdk/aws-rds/test/subnet-group.test.ts index 1dc9718258deb..44fd4e24482a8 100644 --- a/packages/@aws-cdk/aws-rds/test/subnet-group.test.ts +++ b/packages/@aws-cdk/aws-rds/test/subnet-group.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as rds from '../lib'; @@ -10,7 +10,6 @@ describe('subnet group', () => { beforeEach(() => { stack = new cdk.Stack(); vpc = new ec2.Vpc(stack, 'VPC'); - }); test('creates a subnet group from minimal properties', () => { @@ -19,15 +18,13 @@ describe('subnet group', () => { vpc, }); - expect(stack).toHaveResource('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBSubnetGroup', { DBSubnetGroupDescription: 'MyGroup', SubnetIds: [ { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, ], }); - - }); test('creates a subnet group from all properties', () => { @@ -38,7 +35,7 @@ describe('subnet group', () => { vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE }, }); - expect(stack).toHaveResource('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBSubnetGroup', { DBSubnetGroupDescription: 'My Shared Group', DBSubnetGroupName: 'sharedgroup', SubnetIds: [ @@ -46,8 +43,6 @@ describe('subnet group', () => { { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, ], }); - - }); test('correctly creates a subnet group with a deploy-time value for its name', () => { @@ -59,13 +54,11 @@ describe('subnet group', () => { vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE }, }); - expect(stack).toHaveResourceLike('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBSubnetGroup', { DBSubnetGroupName: { Ref: 'Parameter', }, }); - - }); describe('subnet selection', () => { @@ -75,15 +68,13 @@ describe('subnet group', () => { vpc, }); - expect(stack).toHaveResource('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBSubnetGroup', { DBSubnetGroupDescription: 'MyGroup', SubnetIds: [ { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, ], }); - - }); test('can specify subnet type', () => { @@ -93,14 +84,13 @@ describe('subnet group', () => { vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, }); - expect(stack).toHaveResource('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBSubnetGroup', { DBSubnetGroupDescription: 'MyGroup', SubnetIds: [ { Ref: 'VPCPublicSubnet1SubnetB4246D30' }, { Ref: 'VPCPublicSubnet2Subnet74179F39' }, ], }); - }); }); @@ -108,8 +98,5 @@ describe('subnet group', () => { const subnetGroup = rds.SubnetGroup.fromSubnetGroupName(stack, 'Group', 'my-subnet-group'); expect(subnetGroup.subnetGroupName).toEqual('my-subnet-group'); - - }); - }); diff --git a/packages/@aws-cdk/aws-redshift/package.json b/packages/@aws-cdk/aws-redshift/package.json index f812d5b0e9737..d6841a31195d5 100644 --- a/packages/@aws-cdk/aws-redshift/package.json +++ b/packages/@aws-cdk/aws-redshift/package.json @@ -84,9 +84,9 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "aws-sdk": "^2.848.0", - "jest": "^27.4.5" + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-ec2": "0.0.0", diff --git a/packages/@aws-cdk/aws-rekognition/package.json b/packages/@aws-cdk/aws-rekognition/package.json index 30956b54c44fb..1c92e782a9e64 100644 --- a/packages/@aws-cdk/aws-rekognition/package.json +++ b/packages/@aws-cdk/aws-rekognition/package.json @@ -7,6 +7,13 @@ "jsii": { "outdir": "dist", "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + }, "targets": { "dotnet": { "namespace": "Amazon.CDK.AWS.Rekognition", @@ -30,6 +37,13 @@ "distName": "aws-cdk.aws-rekognition", "module": "aws_cdk.aws_rekognition" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -81,7 +95,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-resourcegroups/package.json b/packages/@aws-cdk/aws-resourcegroups/package.json index b7624761212f6..a17a9aa169e15 100644 --- a/packages/@aws-cdk/aws-resourcegroups/package.json +++ b/packages/@aws-cdk/aws-resourcegroups/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-robomaker/package.json b/packages/@aws-cdk/aws-robomaker/package.json index 58e20a0b7e87b..b96c0b59e92b4 100644 --- a/packages/@aws-cdk/aws-robomaker/package.json +++ b/packages/@aws-cdk/aws-robomaker/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53-patterns/package.json b/packages/@aws-cdk/aws-route53-patterns/package.json index 79c15a899e034..2d4e248234582 100644 --- a/packages/@aws-cdk/aws-route53-patterns/package.json +++ b/packages/@aws-cdk/aws-route53-patterns/package.json @@ -72,13 +72,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-certificatemanager": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53-patterns/test/bucket-website-target.test.ts b/packages/@aws-cdk/aws-route53-patterns/test/bucket-website-target.test.ts index a0046ab98d286..91f81a467a43f 100644 --- a/packages/@aws-cdk/aws-route53-patterns/test/bucket-website-target.test.ts +++ b/packages/@aws-cdk/aws-route53-patterns/test/bucket-website-target.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Certificate } from '@aws-cdk/aws-certificatemanager'; import { HostedZone } from '@aws-cdk/aws-route53'; import { App, Stack } from '@aws-cdk/core'; @@ -20,7 +20,7 @@ test('create HTTPS redirect', () => { }); // THEN - expect(stack).toHaveResource('AWS::S3::Bucket', { + Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { WebsiteConfiguration: { RedirectAllRequestsTo: { HostName: 'bar.example.com', @@ -34,28 +34,28 @@ test('create HTTPS redirect', () => { RestrictPublicBuckets: true, }, }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { Aliases: ['foo.example.com', 'baz.example.com'], DefaultRootObject: '', }, }); - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Type: 'A', Name: 'foo.example.com.', HostedZoneId: 'ID', }); - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Type: 'AAAA', Name: 'foo.example.com.', HostedZoneId: 'ID', }); - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Type: 'A', Name: 'baz.example.com.', HostedZoneId: 'ID', }); - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Type: 'AAAA', Name: 'baz.example.com.', HostedZoneId: 'ID', @@ -77,7 +77,7 @@ test('create HTTPS redirect for apex', () => { }); // THEN - expect(stack).toHaveResource('AWS::S3::Bucket', { + Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { WebsiteConfiguration: { RedirectAllRequestsTo: { HostName: 'bar.example.com', @@ -85,11 +85,11 @@ test('create HTTPS redirect for apex', () => { }, }, }); - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Type: 'A', Name: 'example.com.', }); - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Type: 'AAAA', Name: 'example.com.', }); @@ -114,7 +114,7 @@ test('create HTTPS redirect with existing cert', () => { }); // THEN - expect(stack).toHaveResource('AWS::S3::Bucket', { + Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { WebsiteConfiguration: { RedirectAllRequestsTo: { HostName: 'bar.example.com', @@ -122,7 +122,7 @@ test('create HTTPS redirect with existing cert', () => { }, }, }); - expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { DistributionConfig: { ViewerCertificate: { AcmCertificateArn: 'arn:aws:acm:us-east-1:1111111:certificate/11-3336f1-44483d-adc7-9cd375c5169d', diff --git a/packages/@aws-cdk/aws-route53-targets/README.md b/packages/@aws-cdk/aws-route53-targets/README.md index 73269abb6d6ed..925b07048a59b 100644 --- a/packages/@aws-cdk/aws-route53-targets/README.md +++ b/packages/@aws-cdk/aws-route53-targets/README.md @@ -108,7 +108,7 @@ See [the documentation on DNS addressing](https://docs.aws.amazon.com/global-acc * InterfaceVpcEndpoints -**Important:** Based on the CFN docs for VPCEndpoints - [see here](attrDnsEntries) - the attributes returned for DnsEntries in CloudFormation is a combination of the hosted zone ID and the DNS name. The entries are ordered as follows: regional public DNS, zonal public DNS, private DNS, and wildcard DNS. This order is not enforced for AWS Marketplace services, and therefore this CDK construct is ONLY guaranteed to work with non-marketplace services. +**Important:** Based on the CFN docs for VPCEndpoints - [see here](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpcendpoint.html#aws-resource-ec2-vpcendpoint-return-values) - the attributes returned for DnsEntries in CloudFormation is a combination of the hosted zone ID and the DNS name. The entries are ordered as follows: regional public DNS, zonal public DNS, private DNS, and wildcard DNS. This order is not enforced for AWS Marketplace services, and therefore this CDK construct is ONLY guaranteed to work with non-marketplace services. ```ts import * as ec2 from '@aws-cdk/aws-ec2'; diff --git a/packages/@aws-cdk/aws-route53-targets/package.json b/packages/@aws-cdk/aws-route53-targets/package.json index 51a5c1d39452d..7aca456bffc24 100644 --- a/packages/@aws-cdk/aws-route53-targets/package.json +++ b/packages/@aws-cdk/aws-route53-targets/package.json @@ -71,7 +71,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-apigatewayv2": "0.0.0", "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", @@ -79,8 +79,8 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-apigateway": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53-targets/test/apigateway-target.test.ts b/packages/@aws-cdk/aws-route53-targets/test/apigateway-target.test.ts index 5658a1f56323a..f1561bd336d34 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/apigateway-target.test.ts +++ b/packages/@aws-cdk/aws-route53-targets/test/apigateway-target.test.ts @@ -1,4 +1,4 @@ -import { expect as expectStack, haveResource } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as apigw from '@aws-cdk/aws-apigateway'; import * as acm from '@aws-cdk/aws-certificatemanager'; import * as route53 from '@aws-cdk/aws-route53'; @@ -27,7 +27,7 @@ test('targets.ApiGateway can be used to the default domain of an APIGW', () => { }); // THEN - expectStack(stack).to(haveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'example.com.', Type: 'A', AliasTarget: { @@ -47,7 +47,7 @@ test('targets.ApiGateway can be used to the default domain of an APIGW', () => { HostedZoneId: { Ref: 'zoneEB40FF1E', }, - })); + }); }); test('targets.ApiGatewayDomain can be used to directly reference a domain', () => { @@ -66,7 +66,7 @@ test('targets.ApiGatewayDomain can be used to directly reference a domain', () = }); // THEN - expectStack(stack).to(haveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'example.com.', Type: 'A', AliasTarget: { @@ -86,7 +86,7 @@ test('targets.ApiGatewayDomain can be used to directly reference a domain', () = HostedZoneId: { Ref: 'zoneEB40FF1E', }, - })); + }); }); test('fails if an ApiGateway is used with an API that does not define a domain name', () => { @@ -132,7 +132,7 @@ test('targets.ApiGateway accepts a SpecRestApi', () => { }); // THEN - expectStack(stack).to(haveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'example.com.', Type: 'A', AliasTarget: { @@ -152,5 +152,5 @@ test('targets.ApiGateway accepts a SpecRestApi', () => { HostedZoneId: { Ref: 'zoneEB40FF1E', }, - })); + }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53-targets/test/apigatewayv2-target.test.ts b/packages/@aws-cdk/aws-route53-targets/test/apigatewayv2-target.test.ts index d2a66e41bde26..1d8df0eb756b7 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/apigatewayv2-target.test.ts +++ b/packages/@aws-cdk/aws-route53-targets/test/apigatewayv2-target.test.ts @@ -1,4 +1,4 @@ -import { expect as expectStack, haveResource } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as apigwv2 from '@aws-cdk/aws-apigatewayv2'; import * as acm from '@aws-cdk/aws-certificatemanager'; import * as route53 from '@aws-cdk/aws-route53'; @@ -25,7 +25,7 @@ test('targets.ApiGatewayv2Domain can be used to directly reference a domain', () }); // THEN - expectStack(stack).to(haveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'example.com.', Type: 'A', AliasTarget: { @@ -45,5 +45,5 @@ test('targets.ApiGatewayv2Domain can be used to directly reference a domain', () HostedZoneId: { Ref: 'zoneEB40FF1E', }, - })); + }); }); diff --git a/packages/@aws-cdk/aws-route53-targets/test/bucket-website-target.test.ts b/packages/@aws-cdk/aws-route53-targets/test/bucket-website-target.test.ts index ff1938e66447b..2d4e77210abdf 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/bucket-website-target.test.ts +++ b/packages/@aws-cdk/aws-route53-targets/test/bucket-website-target.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as route53 from '@aws-cdk/aws-route53'; import * as s3 from '@aws-cdk/aws-s3'; import { App, Stack } from '@aws-cdk/core'; @@ -23,7 +23,7 @@ test('use S3 bucket website as record target', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { AliasTarget: { DNSName: 's3-website-us-east-1.amazonaws.com', HostedZoneId: 'Z3AQBSTGFYJSTF', @@ -47,7 +47,7 @@ test('use S3 bucket website as record target (fromBucketName)', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { AliasTarget: { DNSName: 's3-website-us-east-1.amazonaws.com', HostedZoneId: 'Z3AQBSTGFYJSTF', diff --git a/packages/@aws-cdk/aws-route53-targets/test/classic-load-balancer-target.test.ts b/packages/@aws-cdk/aws-route53-targets/test/classic-load-balancer-target.test.ts index a89e1f194b1a6..afeeb7640cd65 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/classic-load-balancer-target.test.ts +++ b/packages/@aws-cdk/aws-route53-targets/test/classic-load-balancer-target.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elb from '@aws-cdk/aws-elasticloadbalancing'; import * as route53 from '@aws-cdk/aws-route53'; @@ -26,7 +26,7 @@ test('use classic ELB as record target', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { AliasTarget: { DNSName: { 'Fn::Join': ['', ['dualstack.', { 'Fn::GetAtt': ['LB8A12904C', 'DNSName'] }]] }, HostedZoneId: { 'Fn::GetAtt': ['LB8A12904C', 'CanonicalHostedZoneNameID'] }, diff --git a/packages/@aws-cdk/aws-route53-targets/test/cloudfront-target.test.ts b/packages/@aws-cdk/aws-route53-targets/test/cloudfront-target.test.ts index 646547cb76bda..321726a893eec 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/cloudfront-target.test.ts +++ b/packages/@aws-cdk/aws-route53-targets/test/cloudfront-target.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { SynthUtils } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as cloudfront from '@aws-cdk/aws-cloudfront'; import * as route53 from '@aws-cdk/aws-route53'; import * as s3 from '@aws-cdk/aws-s3'; @@ -14,16 +13,12 @@ test('use CloudFrontTarget partition hosted zone id mapping', () => { targets.CloudFrontTarget.getHostedZoneId(stack); // THEN - expect(SynthUtils.toCloudFormation(stack)).toEqual({ - Mappings: { - AWSCloudFrontPartitionHostedZoneIdMap: { - 'aws': { - zoneId: 'Z2FDTNDATAQYW2', - }, - 'aws-cn': { - zoneId: 'Z3RFFRIM2A3IF5', - }, - }, + Template.fromStack(stack).hasMapping('AWSCloudFrontPartitionHostedZoneIdMap', { + 'aws': { + zoneId: 'Z2FDTNDATAQYW2', + }, + 'aws-cn': { + zoneId: 'Z3RFFRIM2A3IF5', }, }); }); @@ -40,16 +35,12 @@ test('use CloudFrontTarget hosted zone id mappings in nested stacks', () => { // THEN for (let nestedStack of [nestedStackA, nestedStackB]) { - expect(SynthUtils.toCloudFormation(nestedStack)).toEqual({ - Mappings: { - AWSCloudFrontPartitionHostedZoneIdMap: { - 'aws': { - zoneId: 'Z2FDTNDATAQYW2', - }, - 'aws-cn': { - zoneId: 'Z3RFFRIM2A3IF5', - }, - }, + Template.fromStack(nestedStack).hasMapping('AWSCloudFrontPartitionHostedZoneIdMap', { + 'aws': { + zoneId: 'Z2FDTNDATAQYW2', + }, + 'aws-cn': { + zoneId: 'Z3RFFRIM2A3IF5', }, }); } @@ -81,7 +72,7 @@ test('use CloudFront as record target', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { AliasTarget: { DNSName: { 'Fn::GetAtt': ['MyDistributionCFDistributionDE147309', 'DomainName'] }, HostedZoneId: { diff --git a/packages/@aws-cdk/aws-route53-targets/test/elastic-beanstalk-environment-target.test.ts b/packages/@aws-cdk/aws-route53-targets/test/elastic-beanstalk-environment-target.test.ts index ed18f49362118..44cadae6e3b9f 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/elastic-beanstalk-environment-target.test.ts +++ b/packages/@aws-cdk/aws-route53-targets/test/elastic-beanstalk-environment-target.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as route53 from '@aws-cdk/aws-route53'; import { Stack } from '@aws-cdk/core'; import * as targets from '../lib'; @@ -16,7 +16,7 @@ test('use EBS environment as record target', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { AliasTarget: { DNSName: 'mysampleenvironment.xyz.us-east-1.elasticbeanstalk.com', HostedZoneId: 'Z117KPS5GTRQ2G', diff --git a/packages/@aws-cdk/aws-route53-targets/test/global-accelerator-target.test.ts b/packages/@aws-cdk/aws-route53-targets/test/global-accelerator-target.test.ts index e39862e280951..0dc473d38c352 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/global-accelerator-target.test.ts +++ b/packages/@aws-cdk/aws-route53-targets/test/global-accelerator-target.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as globalaccelerator from '@aws-cdk/aws-globalaccelerator'; import * as route53 from '@aws-cdk/aws-route53'; import { Stack } from '@aws-cdk/core'; @@ -22,7 +22,7 @@ test('GlobalAcceleratorTarget creates an alias resource with a string domain nam }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { AliasTarget: { DNSName: 'xyz.awsglobalaccelerator.com', HostedZoneId: 'Z2BJ6XQ5FK7U4H', @@ -45,7 +45,7 @@ test('GlobalAcceleratorTarget creates an alias resource with a Global Accelerato }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { AliasTarget: { DNSName: { 'Fn::GetAtt': [ diff --git a/packages/@aws-cdk/aws-route53-targets/test/interface-vpc-endpoint-target.test.ts b/packages/@aws-cdk/aws-route53-targets/test/interface-vpc-endpoint-target.test.ts index bd13011e4700d..bcec3e3459a3a 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/interface-vpc-endpoint-target.test.ts +++ b/packages/@aws-cdk/aws-route53-targets/test/interface-vpc-endpoint-target.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as route53 from '@aws-cdk/aws-route53'; import { Stack } from '@aws-cdk/core'; @@ -29,7 +29,7 @@ test('use InterfaceVpcEndpoint as record target', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { AliasTarget: { HostedZoneId: { 'Fn::Select': [ diff --git a/packages/@aws-cdk/aws-route53-targets/test/load-balancer-target.test.ts b/packages/@aws-cdk/aws-route53-targets/test/load-balancer-target.test.ts index d16592d3ec183..00c3b1dde24ed 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/load-balancer-target.test.ts +++ b/packages/@aws-cdk/aws-route53-targets/test/load-balancer-target.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as route53 from '@aws-cdk/aws-route53'; @@ -26,7 +26,7 @@ test('use ALB as record target', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { AliasTarget: { DNSName: { 'Fn::Join': ['', ['dualstack.', { 'Fn::GetAtt': ['LB8A12904C', 'DNSName'] }]] }, HostedZoneId: { 'Fn::GetAtt': ['LB8A12904C', 'CanonicalHostedZoneID'] }, diff --git a/packages/@aws-cdk/aws-route53-targets/test/route53-record.test.ts b/packages/@aws-cdk/aws-route53-targets/test/route53-record.test.ts index 9b9bc7585a433..e4940198a6927 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/route53-record.test.ts +++ b/packages/@aws-cdk/aws-route53-targets/test/route53-record.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { ARecord, PublicHostedZone, RecordTarget } from '@aws-cdk/aws-route53'; import { Stack } from '@aws-cdk/core'; import { Route53RecordTarget } from '../lib'; @@ -19,7 +19,7 @@ test('use another route 53 record as record target', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { AliasTarget: { DNSName: { Ref: 'HostedZoneRecordB6AB510D', diff --git a/packages/@aws-cdk/aws-route53-targets/test/userpool-domain.test.ts b/packages/@aws-cdk/aws-route53-targets/test/userpool-domain.test.ts index 8badcaed9c047..d098ef1293f49 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/userpool-domain.test.ts +++ b/packages/@aws-cdk/aws-route53-targets/test/userpool-domain.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { UserPool, UserPoolDomain } from '@aws-cdk/aws-cognito'; import { ARecord, PublicHostedZone, RecordTarget } from '@aws-cdk/aws-route53'; import { Stack } from '@aws-cdk/core'; @@ -21,7 +21,7 @@ test('use user pool domain as record target', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { AliasTarget: { DNSName: { 'Fn::GetAtt': ['UserPoolDomainCloudFrontDomainName0B254952', 'DomainDescription.CloudFrontDistribution'], diff --git a/packages/@aws-cdk/aws-route53/lib/record-set.ts b/packages/@aws-cdk/aws-route53/lib/record-set.ts index 6f49f06b5c70c..a10cb05c37e67 100644 --- a/packages/@aws-cdk/aws-route53/lib/record-set.ts +++ b/packages/@aws-cdk/aws-route53/lib/record-set.ts @@ -683,15 +683,22 @@ export class CrossAccountZoneDelegationRecord extends CoreConstruct { throw Error('Only one of parentHostedZoneName and parentHostedZoneId is supported'); } - const serviceToken = CustomResourceProvider.getOrCreate(this, CROSS_ACCOUNT_ZONE_DELEGATION_RESOURCE_TYPE, { + const provider = CustomResourceProvider.getOrCreateProvider(this, CROSS_ACCOUNT_ZONE_DELEGATION_RESOURCE_TYPE, { codeDirectory: path.join(__dirname, 'cross-account-zone-delegation-handler'), runtime: CustomResourceProviderRuntime.NODEJS_12_X, - policyStatements: [{ Effect: 'Allow', Action: 'sts:AssumeRole', Resource: props.delegationRole.roleArn }], }); - new CustomResource(this, 'CrossAccountZoneDelegationCustomResource', { + const role = iam.Role.fromRoleArn(this, 'cross-account-zone-delegation-handler-role', provider.roleArn); + + const addToPrinciplePolicyResult = role.addToPrincipalPolicy(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['sts:AssumeRole'], + resources: [props.delegationRole.roleArn], + })); + + const customResource = new CustomResource(this, 'CrossAccountZoneDelegationCustomResource', { resourceType: CROSS_ACCOUNT_ZONE_DELEGATION_RESOURCE_TYPE, - serviceToken, + serviceToken: provider.serviceToken, removalPolicy: props.removalPolicy, properties: { AssumeRoleArn: props.delegationRole.roleArn, @@ -702,5 +709,9 @@ export class CrossAccountZoneDelegationRecord extends CoreConstruct { TTL: (props.ttl || Duration.days(2)).toSeconds(), }, }); + + if (addToPrinciplePolicyResult.policyDependable) { + customResource.node.addDependency(addToPrinciplePolicyResult.policyDependable); + } } } diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index b3e12466c510b..9cb18851a5db3 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -79,15 +79,15 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/aws-lambda": "^8.10.89", - "@types/jest": "^27.0.3", + "@types/aws-lambda": "^8.10.92", + "@types/jest": "^27.4.1", "aws-sdk": "^2.848.0", - "jest": "^27.4.5" + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-ec2": "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 0b66c9589cd50..e5f8fbe21d6c9 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,5 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; -import { SynthUtils } from '@aws-cdk/assert-internal'; import * as cdk from '@aws-cdk/core'; import { HostedZone } from '../lib'; @@ -7,14 +5,16 @@ describe('hosted zone provider', () => { describe('Hosted Zone Provider', () => { test('HostedZoneProvider will return context values if available', () => { // GIVEN - const stack = new cdk.Stack(undefined, 'TestStack', { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'TestStack', { env: { account: '12345', region: 'us-east-1' }, }); const filter = { domainName: 'test.com' }; HostedZone.fromLookup(stack, 'Ref', filter); - const missing = SynthUtils.synthesize(stack).assembly.manifest.missing!; + const assembly = app.synth().getStackArtifact(stack.artifactId); + const missing = assembly.assembly.manifest.missing!; expect(missing && missing.length === 1).toEqual(true); const fakeZoneId = '11111111111111'; @@ -39,18 +39,20 @@ describe('hosted zone provider', () => { // THEN 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', { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'TestStack', { env: { account: '12345', region: 'us-east-1' }, }); const filter = { domainName: 'test.com' }; HostedZone.fromLookup(stack, 'Ref', filter); - const missing = SynthUtils.synthesize(stack).assembly.manifest.missing!; + const assembly = app.synth().getStackArtifact(stack.artifactId); + const missing = assembly.assembly.manifest.missing!; expect(missing && missing.length === 1).toEqual(true); const fakeZoneId = '11111111111111'; 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 7939452fd3421..a60d277788691 100644 --- a/packages/@aws-cdk/aws-route53/test/hosted-zone.test.ts +++ b/packages/@aws-cdk/aws-route53/test/hosted-zone.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import { HostedZone, PublicHostedZone } from '../lib'; @@ -26,8 +26,6 @@ describe('hosted zone', () => { ], ], }); - - }); }); @@ -42,7 +40,7 @@ describe('hosted zone', () => { cdk.Tags.of(hostedZone).add('zoneTag', 'inMyZone'); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { HostedZoneDB99F866: { Type: 'AWS::Route53::HostedZone', @@ -58,8 +56,6 @@ describe('hosted zone', () => { }, }, }); - - }); test('with crossAccountZoneDelegationPrincipal', () => { @@ -76,7 +72,7 @@ describe('hosted zone', () => { }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { HostedZoneDB99F866: { Type: 'AWS::Route53::HostedZone', @@ -149,8 +145,6 @@ describe('hosted zone', () => { }, }, }); - - }); test('with crossAccountZoneDelegationPrincipal, throws if name provided without principal', () => { @@ -166,7 +160,5 @@ describe('hosted zone', () => { crossAccountZoneDelegationRoleName: 'myrole', }); }).toThrow(/crossAccountZoneDelegationRoleName property is not supported without crossAccountZoneDelegationPrincipal/); - - }); }); diff --git a/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.expected.json b/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.expected.json index 281fc984d0756..3ad72296cd685 100644 --- a/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.expected.json +++ b/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.expected.json @@ -78,6 +78,55 @@ "Name": "sub.myzone.com." } }, + "DelegationWithZoneIdcrossaccountzonedelegationhandlerrolePolicy5170A69B": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "ParentHostedZoneCrossAccountZoneDelegationRole95B1C36E", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "DelegationWithZoneIdcrossaccountzonedelegationhandlerrolePolicy5170A69B", + "Roles": [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "CustomCrossAccountZoneDelegationCustomResourceProviderRoleED64687B", + "Arn" + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + }, "DelegationWithZoneIdCrossAccountZoneDelegationCustomResourceFFD766E7": { "Type": "Custom::CrossAccountZoneDelegation", "Properties": { @@ -105,6 +154,9 @@ }, "TTL": 172800 }, + "DependsOn": [ + "DelegationWithZoneIdcrossaccountzonedelegationhandlerrolePolicy5170A69B" + ], "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, @@ -127,26 +179,6 @@ { "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } - ], - "Policies": [ - { - "PolicyName": "Inline", - "PolicyDocument": { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": "sts:AssumeRole", - "Resource": { - "Fn::GetAtt": [ - "ParentHostedZoneCrossAccountZoneDelegationRole95B1C36E", - "Arn" - ] - } - } - ] - } - } ] } }, @@ -155,7 +187,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersd17df4f90e07a972e8f7b00dddbae8e3eba45a212226d2b714dcd28dded69602S3Bucket200D9216" + "Ref": "AssetParameters7625bcc3bbd65c490a92d42790a563e31dc02c18006ef272338c8c788849bb8aS3BucketC1366C27" }, "S3Key": { "Fn::Join": [ @@ -168,7 +200,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersd17df4f90e07a972e8f7b00dddbae8e3eba45a212226d2b714dcd28dded69602S3VersionKey0E5C26F0" + "Ref": "AssetParameters7625bcc3bbd65c490a92d42790a563e31dc02c18006ef272338c8c788849bb8aS3VersionKeyEE72CEF8" } ] } @@ -181,7 +213,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersd17df4f90e07a972e8f7b00dddbae8e3eba45a212226d2b714dcd28dded69602S3VersionKey0E5C26F0" + "Ref": "AssetParameters7625bcc3bbd65c490a92d42790a563e31dc02c18006ef272338c8c788849bb8aS3VersionKeyEE72CEF8" } ] } @@ -212,6 +244,55 @@ "Name": "anothersub.myzone.com." } }, + "DelegationWithZoneNamecrossaccountzonedelegationhandlerrolePolicy86996882": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "ParentHostedZoneCrossAccountZoneDelegationRole95B1C36E", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "DelegationWithZoneNamecrossaccountzonedelegationhandlerrolePolicy86996882", + "Roles": [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "CustomCrossAccountZoneDelegationCustomResourceProviderRoleED64687B", + "Arn" + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + }, "DelegationWithZoneNameCrossAccountZoneDelegationCustomResourceA1A1C94A": { "Type": "Custom::CrossAccountZoneDelegation", "Properties": { @@ -237,22 +318,25 @@ }, "TTL": 172800 }, + "DependsOn": [ + "DelegationWithZoneNamecrossaccountzonedelegationhandlerrolePolicy86996882" + ], "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" } }, "Parameters": { - "AssetParametersd17df4f90e07a972e8f7b00dddbae8e3eba45a212226d2b714dcd28dded69602S3Bucket200D9216": { + "AssetParameters7625bcc3bbd65c490a92d42790a563e31dc02c18006ef272338c8c788849bb8aS3BucketC1366C27": { "Type": "String", - "Description": "S3 bucket for asset \"d17df4f90e07a972e8f7b00dddbae8e3eba45a212226d2b714dcd28dded69602\"" + "Description": "S3 bucket for asset \"7625bcc3bbd65c490a92d42790a563e31dc02c18006ef272338c8c788849bb8a\"" }, - "AssetParametersd17df4f90e07a972e8f7b00dddbae8e3eba45a212226d2b714dcd28dded69602S3VersionKey0E5C26F0": { + "AssetParameters7625bcc3bbd65c490a92d42790a563e31dc02c18006ef272338c8c788849bb8aS3VersionKeyEE72CEF8": { "Type": "String", - "Description": "S3 key for asset version \"d17df4f90e07a972e8f7b00dddbae8e3eba45a212226d2b714dcd28dded69602\"" + "Description": "S3 key for asset version \"7625bcc3bbd65c490a92d42790a563e31dc02c18006ef272338c8c788849bb8a\"" }, - "AssetParametersd17df4f90e07a972e8f7b00dddbae8e3eba45a212226d2b714dcd28dded69602ArtifactHash37FB4D0C": { + "AssetParameters7625bcc3bbd65c490a92d42790a563e31dc02c18006ef272338c8c788849bb8aArtifactHashAADF3168": { "Type": "String", - "Description": "Artifact hash for asset \"d17df4f90e07a972e8f7b00dddbae8e3eba45a212226d2b714dcd28dded69602\"" + "Description": "Artifact hash for asset \"7625bcc3bbd65c490a92d42790a563e31dc02c18006ef272338c8c788849bb8a\"" } } } \ No newline at end of file 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 8e380e3dfcd81..fbf3d56517d3b 100644 --- a/packages/@aws-cdk/aws-route53/test/record-set.test.ts +++ b/packages/@aws-cdk/aws-route53/test/record-set.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import { Duration, RemovalPolicy, Stack } from '@aws-cdk/core'; import * as route53 from '../lib'; @@ -22,7 +21,7 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'CNAME', HostedZoneId: { @@ -33,7 +32,6 @@ describe('record set', () => { ], TTL: '1800', }); - }); test('with custom ttl', () => { @@ -54,7 +52,7 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'aa.myzone.', Type: 'CNAME', HostedZoneId: { @@ -65,7 +63,6 @@ describe('record set', () => { ], TTL: '6077', }); - }); test('with ttl of 0', () => { @@ -86,10 +83,9 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { TTL: '0', }); - }); test('defaults to zone root', () => { @@ -108,7 +104,7 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'myzone.', Type: 'A', HostedZoneId: { @@ -118,7 +114,6 @@ describe('record set', () => { '1.2.3.4', ], }); - }); test('A record with ip addresses', () => { @@ -137,7 +132,7 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'A', HostedZoneId: { @@ -149,7 +144,6 @@ describe('record set', () => { ], TTL: '1800', }); - }); test('A record with alias', () => { @@ -177,7 +171,7 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: '_foo.myzone.', HostedZoneId: { Ref: 'HostedZoneDB99F866', @@ -188,8 +182,6 @@ describe('record set', () => { DNSName: 'foo.example.com', }, }); - - }); test('AAAA record with ip addresses', () => { @@ -208,7 +200,7 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'AAAA', HostedZoneId: { @@ -219,7 +211,6 @@ describe('record set', () => { ], TTL: '1800', }); - }); test('AAAA record with alias on zone root', () => { @@ -245,7 +236,7 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'myzone.', HostedZoneId: { Ref: 'HostedZoneDB99F866', @@ -256,8 +247,6 @@ describe('record set', () => { DNSName: 'foo.example.com', }, }); - - }); test('CNAME record', () => { @@ -276,7 +265,7 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'CNAME', HostedZoneId: { @@ -287,7 +276,6 @@ describe('record set', () => { ], TTL: '1800', }); - }); test('TXT record', () => { @@ -306,7 +294,7 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'TXT', HostedZoneId: { @@ -317,7 +305,6 @@ describe('record set', () => { ], TTL: '1800', }); - }); test('TXT record with value longer than 255 chars', () => { @@ -336,7 +323,7 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'TXT', HostedZoneId: { @@ -347,7 +334,6 @@ describe('record set', () => { ], TTL: '1800', }); - }); test('SRV record', () => { @@ -371,7 +357,7 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'SRV', HostedZoneId: { @@ -382,7 +368,6 @@ describe('record set', () => { ], TTL: '1800', }); - }); test('CAA record', () => { @@ -405,7 +390,7 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'CAA', HostedZoneId: { @@ -416,7 +401,6 @@ describe('record set', () => { ], TTL: '1800', }); - }); test('CAA Amazon record', () => { @@ -433,7 +417,7 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'myzone.', Type: 'CAA', HostedZoneId: { @@ -444,7 +428,6 @@ describe('record set', () => { ], TTL: '1800', }); - }); test('CAA Amazon record with record name', () => { @@ -462,7 +445,7 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'CAA', HostedZoneId: { @@ -473,7 +456,6 @@ describe('record set', () => { ], TTL: '1800', }); - }); test('MX record', () => { @@ -495,7 +477,7 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'mail.myzone.', Type: 'MX', HostedZoneId: { @@ -506,7 +488,6 @@ describe('record set', () => { ], TTL: '1800', }); - }); test('NS record', () => { @@ -525,7 +506,7 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'NS', HostedZoneId: { @@ -537,7 +518,6 @@ describe('record set', () => { ], TTL: '1800', }); - }); test('DS record', () => { @@ -556,7 +536,7 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'DS', HostedZoneId: { @@ -567,7 +547,6 @@ describe('record set', () => { ], TTL: '1800', }); - }); test('Zone delegation record', () => { @@ -586,7 +565,7 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Name: 'foo.myzone.', Type: 'NS', HostedZoneId: { @@ -597,7 +576,6 @@ describe('record set', () => { ], TTL: '172800', }); - }); test('Cross account zone delegation record with parentHostedZoneId', () => { @@ -621,7 +599,7 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('Custom::CrossAccountZoneDelegation', { + Template.fromStack(stack).hasResourceProperties('Custom::CrossAccountZoneDelegation', { ServiceToken: { 'Fn::GetAtt': [ 'CustomCrossAccountZoneDelegationCustomResourceProviderHandler44A84265', @@ -646,11 +624,10 @@ describe('record set', () => { }, TTL: 60, }); - expect(stack).toHaveResource('Custom::CrossAccountZoneDelegation', { + Template.fromStack(stack).hasResource('Custom::CrossAccountZoneDelegation', { DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition); - + }); }); test('Cross account zone delegation record with parentHostedZoneName', () => { @@ -673,7 +650,7 @@ describe('record set', () => { }); // THEN - expect(stack).toHaveResource('Custom::CrossAccountZoneDelegation', { + Template.fromStack(stack).hasResourceProperties('Custom::CrossAccountZoneDelegation', { ServiceToken: { 'Fn::GetAtt': [ 'CustomCrossAccountZoneDelegationCustomResourceProviderHandler44A84265', @@ -696,7 +673,6 @@ describe('record set', () => { }, TTL: 60, }); - }); test('Cross account zone delegation record throws when parent id and name both/nither are supplied', () => { @@ -729,7 +705,143 @@ describe('record set', () => { ttl: Duration.seconds(60), }); }).toThrow(/Only one of parentHostedZoneName and parentHostedZoneId is supported/); + }); + + test('Multiple cross account zone delegation records', () => { + // GIVEN + const stack = new Stack(); + const parentZone = new route53.PublicHostedZone(stack, 'ParentHostedZone', { + zoneName: 'myzone.com', + crossAccountZoneDelegationPrincipal: new iam.AccountPrincipal('123456789012'), + }); + + // WHEN + const childZone = new route53.PublicHostedZone(stack, 'ChildHostedZone', { + zoneName: 'sub.myzone.com', + }); + new route53.CrossAccountZoneDelegationRecord(stack, 'Delegation', { + delegatedZone: childZone, + parentHostedZoneName: 'myzone.com', + delegationRole: parentZone.crossAccountZoneDelegationRole!, + ttl: Duration.seconds(60), + }); + const childZone2 = new route53.PublicHostedZone(stack, 'ChildHostedZone2', { + zoneName: 'anothersub.myzone.com', + }); + new route53.CrossAccountZoneDelegationRecord(stack, 'Delegation2', { + delegatedZone: childZone2, + parentHostedZoneName: 'myzone.com', + delegationRole: parentZone.crossAccountZoneDelegationRole!, + ttl: Duration.seconds(60), + }); + + // THEN + const childHostedZones = [ + { name: 'sub.myzone.com', id: 'ChildHostedZone4B14AC71', dependsOn: 'DelegationcrossaccountzonedelegationhandlerrolePolicy1E157602' }, + { name: 'anothersub.myzone.com', id: 'ChildHostedZone2A37198F0', dependsOn: 'Delegation2crossaccountzonedelegationhandlerrolePolicy713BEAC3' }, + ]; + + for (var childHostedZone of childHostedZones) { + Template.fromStack(stack).hasResource('Custom::CrossAccountZoneDelegation', { + Properties: { + ServiceToken: { + 'Fn::GetAtt': [ + 'CustomCrossAccountZoneDelegationCustomResourceProviderHandler44A84265', + 'Arn', + ], + }, + AssumeRoleArn: { + 'Fn::GetAtt': [ + 'ParentHostedZoneCrossAccountZoneDelegationRole95B1C36E', + 'Arn', + ], + }, + ParentZoneName: 'myzone.com', + DelegatedZoneName: childHostedZone.name, + DelegatedZoneNameServers: { + 'Fn::GetAtt': [ + childHostedZone.id, + 'NameServers', + ], + }, + TTL: 60, + }, + DependsOn: [ + childHostedZone.dependsOn, + ], + }); + } + }); + test('Cross account zone delegation policies', () => { + // GIVEN + const stack = new Stack(); + const parentZone = new route53.PublicHostedZone(stack, 'ParentHostedZone', { + zoneName: 'myzone.com', + crossAccountZoneDelegationPrincipal: new iam.AccountPrincipal('123456789012'), + }); + // WHEN + const childZone = new route53.PublicHostedZone(stack, 'ChildHostedZone', { + zoneName: 'sub.myzone.com', + }); + new route53.CrossAccountZoneDelegationRecord(stack, 'Delegation', { + delegatedZone: childZone, + parentHostedZoneName: 'myzone.com', + delegationRole: parentZone.crossAccountZoneDelegationRole!, + ttl: Duration.seconds(60), + }); + const childZone2 = new route53.PublicHostedZone(stack, 'ChildHostedZone2', { + zoneName: 'anothersub.myzone.com', + }); + new route53.CrossAccountZoneDelegationRecord(stack, 'Delegation2', { + delegatedZone: childZone2, + parentHostedZoneName: 'myzone.com', + delegationRole: parentZone.crossAccountZoneDelegationRole!, + ttl: Duration.seconds(60), + }); + + // THEN + const policyNames = [ + 'DelegationcrossaccountzonedelegationhandlerrolePolicy1E157602', + 'Delegation2crossaccountzonedelegationhandlerrolePolicy713BEAC3', + ]; + + for (var policyName of policyNames) { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyName: policyName, + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'ParentHostedZoneCrossAccountZoneDelegationRole95B1C36E', + 'Arn', + ], + }, + }, + ], + }, + Roles: [ + { + 'Fn::Select': [1, { + 'Fn::Split': ['/', { + 'Fn::Select': [5, { + 'Fn::Split': [':', { + 'Fn::GetAtt': [ + 'CustomCrossAccountZoneDelegationCustomResourceProviderRoleED64687B', + 'Arn', + ], + }], + }], + }], + }], + }, + ], + }); + } }); }); diff --git a/packages/@aws-cdk/aws-route53/test/route53.test.ts b/packages/@aws-cdk/aws-route53/test/route53.test.ts index f86b155592db7..d6a8a7ba63135 100644 --- a/packages/@aws-cdk/aws-route53/test/route53.test.ts +++ b/packages/@aws-cdk/aws-route53/test/route53.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { MatchStyle } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import { HostedZone, PrivateHostedZone, PublicHostedZone, TxtRecord } from '../lib'; @@ -9,15 +8,8 @@ describe('route53', () => { test('public hosted zone', () => { const app = new TestApp(); new PublicHostedZone(app.stack, 'HostedZone', { zoneName: 'test.public' }); - expect(app.stack).toMatchTemplate({ - Resources: { - HostedZoneDB99F866: { - Type: 'AWS::Route53::HostedZone', - Properties: { - Name: 'test.public.', - }, - }, - }, + Template.fromStack(app.stack).hasResourceProperties('AWS::Route53::HostedZone', { + Name: 'test.public.', }); }); @@ -25,47 +17,32 @@ describe('route53', () => { 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).toMatchTemplate({ - Resources: { - HostedZoneDB99F866: { - Type: 'AWS::Route53::HostedZone', - Properties: { - Name: 'test.private.', - VPCs: [{ - VPCId: { Ref: 'VPCB9E5F0B4' }, - VPCRegion: 'bermuda-triangle', - }], - }, - }, - }, - }, MatchStyle.SUPERSET); - + Template.fromStack(app.stack).hasResourceProperties('AWS::Route53::HostedZone', { + Name: 'test.private.', + VPCs: [{ + VPCId: { Ref: 'VPCB9E5F0B4' }, + VPCRegion: 'bermuda-triangle', + }], + }); }); + 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).toMatchTemplate({ - Resources: { - HostedZoneDB99F866: { - Type: 'AWS::Route53::HostedZone', - Properties: { - Name: 'test.private.', - VPCs: [{ - VPCId: { Ref: 'VPC17DE2CF87' }, - VPCRegion: 'bermuda-triangle', - }, - { - VPCId: { Ref: 'VPC2C1F0E711' }, - VPCRegion: 'bermuda-triangle', - }], - }, - }, + Template.fromStack(app.stack).hasResourceProperties('AWS::Route53::HostedZone', { + Name: 'test.private.', + VPCs: [{ + VPCId: { Ref: 'VPC17DE2CF87' }, + VPCRegion: 'bermuda-triangle', }, - }, MatchStyle.SUPERSET); - + { + VPCId: { Ref: 'VPC2C1F0E711' }, + VPCRegion: 'bermuda-triangle', + }], + }); }); }); @@ -83,14 +60,12 @@ describe('route53', () => { values: ['SeeThere'], }); - expect(stack2).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack2).hasResourceProperties('AWS::Route53::RecordSet', { HostedZoneId: 'hosted-zone-id', Name: 'lookHere.cdk.local.', ResourceRecords: ['"SeeThere"'], Type: 'TXT', }); - - }); test('adds period to name if not provided', () => { @@ -103,10 +78,9 @@ describe('route53', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::HostedZone', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::HostedZone', { Name: 'zonename.', }); - }); test('fails if zone name ends with a trailing dot', () => { @@ -115,7 +89,6 @@ describe('route53', () => { // THEN expect(() => new HostedZone(stack, 'MyHostedZone', { zoneName: 'zonename.' })).toThrow(/zone name must not end with a trailing dot/); - }); test('a hosted zone can be assiciated with a VPC either upon creation or using "addVpc"', () => { @@ -133,7 +106,7 @@ describe('route53', () => { zone.addVpc(vpc3); // THEN - expect(stack).toHaveResource('AWS::Route53::HostedZone', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::HostedZone', { VPCs: [ { VPCId: { @@ -161,7 +134,6 @@ describe('route53', () => { }, ], }); - }); test('public zone cannot be associated with a vpc (runtime error)', () => { @@ -172,7 +144,6 @@ describe('route53', () => { // THEN expect(() => zone.addVpc(vpc)).toThrow(/Cannot associate public hosted zones with a VPC/); - }); test('setting up zone delegation', () => { @@ -185,14 +156,13 @@ describe('route53', () => { zone.addDelegation(delegate, { ttl: cdk.Duration.seconds(1337) }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Type: 'NS', Name: 'sub.top.test.', HostedZoneId: stack.resolve(zone.hostedZoneId), ResourceRecords: stack.resolve(delegate.hostedZoneNameServers), TTL: '1337', }); - }); test('public hosted zone wiht caaAmazon set to true', () => { @@ -206,14 +176,13 @@ describe('route53', () => { }); // THEN - expect(stack).toHaveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', { Type: 'CAA', Name: 'protected.com.', ResourceRecords: [ '0 issue "amazon.com"', ], }); - }); }); diff --git a/packages/@aws-cdk/aws-route53/test/util.test.ts b/packages/@aws-cdk/aws-route53/test/util.test.ts index f6ec2b7cd36a6..3f791d1a5bb63 100644 --- a/packages/@aws-cdk/aws-route53/test/util.test.ts +++ b/packages/@aws-cdk/aws-route53/test/util.test.ts @@ -5,13 +5,11 @@ import * as util from '../lib/util'; describe('util', () => { test('throws when zone name ending with a \'.\'', () => { expect(() => util.validateZoneName('zone.name.')).toThrow(/trailing dot/); - }); test('accepts a valid domain name', () => { const domainName = 'amazonaws.com'; util.validateZoneName(domainName); - }); test('providedName ending with a dot returns the name', () => { @@ -27,7 +25,6 @@ describe('util', () => { // THEN expect(qualified).toEqual('test.domain.com.'); - }); test('providedName that matches zoneName returns providedName with a trailing dot', () => { @@ -43,7 +40,6 @@ describe('util', () => { // THEN expect(qualified).toEqual('test.domain.com.'); - }); test('providedName that ends with zoneName returns providedName with a trailing dot', () => { @@ -59,7 +55,6 @@ describe('util', () => { // THEN expect(qualified).toEqual('test.domain.com.'); - }); test('providedName that does not match zoneName concatenates providedName and zoneName', () => { @@ -75,6 +70,5 @@ describe('util', () => { // THEN 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 506a25e2a26df..e4155136af75a 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,5 +1,4 @@ -import { expect as cdkExpect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { IVpcEndpointServiceLoadBalancer, VpcEndpointService } from '@aws-cdk/aws-ec2'; import { Stack } from '@aws-cdk/core'; import { PublicHostedZone, VpcEndpointServiceDomainName } from '../lib'; @@ -44,7 +43,7 @@ test('create domain name resource', () => { }); // THEN - cdkExpect(stack).to(haveResourceLike('Custom::AWS', { + Template.fromStack(stack).hasResource('Custom::AWS', { Properties: { Create: { 'Fn::Join': [ @@ -87,10 +86,10 @@ test('create domain name resource', () => { 'EndpointDomainEnableDnsCustomResourcePolicy5E6DE7EB', 'VPCES3AE7D565', ], - }, ResourcePart.CompleteDefinition)); + }); // Have to use `haveResourceLike` because there is a property that, by design, changes on every build - cdkExpect(stack).to(haveResourceLike('Custom::AWS', { + Template.fromStack(stack).hasResource('Custom::AWS', { Properties: { Create: { 'Fn::Join': [ @@ -123,9 +122,9 @@ test('create domain name resource', () => { 'EndpointDomainGetNamesCustomResourcePolicy141775B1', 'VPCES3AE7D565', ], - }, ResourcePart.CompleteDefinition)); + }); - cdkExpect(stack).to(haveResource('AWS::Route53::RecordSet', { + Template.fromStack(stack).hasResource('AWS::Route53::RecordSet', { Properties: { Name: { 'Fn::Join': [ @@ -167,9 +166,9 @@ test('create domain name resource', () => { DependsOn: [ 'VPCES3AE7D565', ], - }, ResourcePart.CompleteDefinition)); + }); - cdkExpect(stack).to(haveResourceLike('Custom::AWS', { + Template.fromStack(stack).hasResource('Custom::AWS', { Properties: { Create: { 'Fn::Join': [ @@ -241,7 +240,7 @@ test('create domain name resource', () => { 'EndpointDomainStartVerificationCustomResourcePolicyD2BAC9A6', 'VPCES3AE7D565', ], - }, ResourcePart.CompleteDefinition)); + }); }); test('throws if creating multiple domains for a single service', () => { @@ -266,7 +265,6 @@ test('throws if creating multiple domains for a single service', () => { }).toThrow(/Cannot create a VpcEndpointServiceDomainName for service/); }); - test('endpoint domain name property equals input domain name', () => { // GIVEN vpces = new VpcEndpointService(stack, 'NameTest', { @@ -279,5 +277,4 @@ test('endpoint domain name property equals input domain name', () => { publicHostedZone: zone, }); expect(dn.domainName).toEqual('name-test.aws-cdk.dev'); - }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53recoverycontrol/package.json b/packages/@aws-cdk/aws-route53recoverycontrol/package.json index 30202a7ee547c..cde17f02ec86e 100644 --- a/packages/@aws-cdk/aws-route53recoverycontrol/package.json +++ b/packages/@aws-cdk/aws-route53recoverycontrol/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-route53recoverycontrol", "module": "aws_cdk.aws_route53recoverycontrol" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-route53recoveryreadiness/package.json b/packages/@aws-cdk/aws-route53recoveryreadiness/package.json index 221b8584c0e36..8feeeca412adb 100644 --- a/packages/@aws-cdk/aws-route53recoveryreadiness/package.json +++ b/packages/@aws-cdk/aws-route53recoveryreadiness/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-route53recoveryreadiness", "module": "aws_cdk.aws_route53recoveryreadiness" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-route53resolver/README.md b/packages/@aws-cdk/aws-route53resolver/README.md index a3bf5946ead7d..b4cef6765d7bf 100644 --- a/packages/@aws-cdk/aws-route53resolver/README.md +++ b/packages/@aws-cdk/aws-route53resolver/README.md @@ -61,7 +61,11 @@ Use `FirewallDomainList.fromFirewallDomainListId()` to import an existing or [AW ```ts // AWSManagedDomainsMalwareDomainList in us-east-1 -const malwareList = route53resolver.FirewallDomainList.fromFirewallDomainListId(this, 'Malware', 'rslvr-fdl-2c46f2ecbfec4dcc'); +const malwareList = route53resolver.FirewallDomainList.fromFirewallDomainListId( + this, + 'Malware', + 'rslvr-fdl-2c46f2ecbfec4dcc', +); ``` ### Rule group @@ -69,6 +73,7 @@ const malwareList = route53resolver.FirewallDomainList.fromFirewallDomainListId( Create a rule group: ```ts +declare const myBlockList: route53resolver.FirewallDomainList; new route53resolver.FirewallRuleGroup(this, 'RuleGroup', { rules: [ { @@ -84,16 +89,19 @@ new route53resolver.FirewallRuleGroup(this, 'RuleGroup', { Rules can be added at construction time or using `addRule()`: ```ts +declare const myBlockList: route53resolver.FirewallDomainList; +declare const ruleGroup: route53resolver.FirewallRuleGroup; + ruleGroup.addRule({ priority: 10, - firewallDomainList: blockList, + firewallDomainList: myBlockList, // block and reply with NXDOMAIN action: route53resolver.FirewallRuleAction.block(route53resolver.DnsBlockResponse.nxDomain()), }); ruleGroup.addRule({ priority: 20, - firewallDomainList: blockList, + firewallDomainList: myBlockList, // block and override DNS response with a custom domain action: route53resolver.FirewallRuleAction.block(route53resolver.DnsBlockResponse.override('amazon.com')), }); @@ -102,7 +110,12 @@ ruleGroup.addRule({ Use `associate()` to associate a rule group with a VPC: ```ts -ruleGroup.associate({ +import * as ec2 from '@aws-cdk/aws-ec2'; + +declare const ruleGroup: route53resolver.FirewallRuleGroup; +declare const myVpc: ec2.Vpc; + +ruleGroup.associate('Association', { priority: 101, vpc: myVpc, }) diff --git a/packages/@aws-cdk/aws-route53resolver/package.json b/packages/@aws-cdk/aws-route53resolver/package.json index a249530cccf09..be0c3e79cea04 100644 --- a/packages/@aws-cdk/aws-route53resolver/package.json +++ b/packages/@aws-cdk/aws-route53resolver/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,7 +85,7 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-ec2": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53resolver/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-route53resolver/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..fa40ebdd01cc4 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/rosetta/default.ts-fixture @@ -0,0 +1,11 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as route53resolver from '@aws-cdk/aws-route53resolver'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-assets/README.md b/packages/@aws-cdk/aws-s3-assets/README.md index a73cbf0919642..bf005eba020d4 100644 --- a/packages/@aws-cdk/aws-s3-assets/README.md +++ b/packages/@aws-cdk/aws-s3-assets/README.md @@ -95,18 +95,21 @@ method `tryBundle()` which should return `true` if local bundling was performed. If `false` is returned, docker bundling will be done: ```ts +class MyBundle implements ILocalBundling { + public tryBundle(outputDir: string, options: BundlingOptions) { + const canRunLocally = true // replace with actual logic + if (canRunLocally) { + // perform local bundling here + return true; + } + return false; + } +} + new assets.Asset(this, 'BundledAsset', { path: '/path/to/asset', bundling: { - local: { - tryBundle(outputDir: string, options: BundlingOptions) { - if (canRunLocally) { - // perform local bundling here - return true; - } - return false; - }, - }, + local: new MyBundle(), // Docker bundling fallback image: DockerImage.fromRegistry('alpine'), entrypoint: ['/bin/sh', '-c'], diff --git a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts index 484e04e4a9cb2..2f04f4532b36e 100644 --- a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts +++ b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts @@ -76,13 +76,13 @@ export class Asset extends CoreConstruct implements cdk.IAsset { /** * Attribute which represents the S3 HTTP URL of this asset. - * @example https://s3.us-west-1.amazonaws.com/bucket/key + * For example, `https://s3.us-west-1.amazonaws.com/bucket/key` */ public readonly httpUrl: string; /** * Attribute which represents the S3 URL of this asset. - * @example s3://bucket/key + * For example, `s3://bucket/key` */ public readonly s3ObjectUrl: string; diff --git a/packages/@aws-cdk/aws-s3-assets/package.json b/packages/@aws-cdk/aws-s3-assets/package.json index 084b0968e7306..5291ae6cb726e 100644 --- a/packages/@aws-cdk/aws-s3-assets/package.json +++ b/packages/@aws-cdk/aws-s3-assets/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -70,12 +77,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/assets": "0.0.0", diff --git a/packages/@aws-cdk/aws-s3-assets/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-s3-assets/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..52f4c907d8b07 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-assets/rosetta/default.ts-fixture @@ -0,0 +1,12 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { BundlingOptions, BundlingOutput, DockerImage, ILocalBundling, Stack } from '@aws-cdk/core'; +import * as assets from '@aws-cdk/aws-s3-assets'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts b/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts index 7a335ab3b7bb2..f04218f95929d 100644 --- a/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts +++ b/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts @@ -1,12 +1,11 @@ -import { ResourcePart, SynthUtils } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; import { Asset } from '../lib/asset'; const SAMPLE_ASSET_DIR = path.join(__dirname, 'sample-asset-directory'); @@ -79,7 +78,7 @@ test('"file" assets', () => { expect(entry).toBeTruthy(); // synthesize first so "prepare" is called - const template = SynthUtils.synthesize(stack).template; + const template = Template.fromStack(stack); expect(stack.resolve(entry!.data)).toEqual({ path: 'asset.78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197.txt', @@ -92,8 +91,12 @@ test('"file" assets', () => { }); // verify that now the template contains parameters for this asset - expect(template.Parameters.AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3Bucket2C60F94A.Type).toBe('String'); - expect(template.Parameters.AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3VersionKey9482DC35.Type).toBe('String'); + expect(template.findParameters('AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3Bucket2C60F94A') + .AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3Bucket2C60F94A.Type) + .toBe('String'); + expect(template.findParameters('AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3VersionKey9482DC35') + .AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3VersionKey9482DC35.Type) + .toBe('String'); }); test('"readers" or "grantRead" can be used to grant read permissions on the asset to a principal', () => { @@ -108,7 +111,7 @@ test('"readers" or "grantRead" can be used to grant read permissions on the asse asset.grantRead(group); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -200,13 +203,13 @@ test('addResourceMetadata can be used to add CFN metadata to resources', () => { asset.addResourceMetadata(resource, 'PropName'); // THEN - expect(stack).toHaveResource('My::Resource::Type', { + Template.fromStack(stack).hasResource('My::Resource::Type', { Metadata: { 'aws:asset:path': 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', 'aws:asset:is-bundled': false, 'aws:asset:property': 'PropName', }, - }, ResourcePart.CompleteDefinition); + }); }); test('asset metadata is only emitted if ASSET_RESOURCE_METADATA_ENABLED_CONTEXT is defined', () => { @@ -220,13 +223,13 @@ test('asset metadata is only emitted if ASSET_RESOURCE_METADATA_ENABLED_CONTEXT asset.addResourceMetadata(resource, 'PropName'); // THEN - expect(stack).not.toHaveResource('My::Resource::Type', { + Template.fromStack(stack).hasResource('My::Resource::Type', Match.not({ Metadata: { 'aws:asset:path': SAMPLE_ASSET_DIR, 'aws:asset:is-bundled': false, 'aws:asset:property': 'PropName', }, - }, ResourcePart.CompleteDefinition); + })); }); test('nested assemblies share assets: legacy synth edition', () => { @@ -350,8 +353,8 @@ describe('staging', () => { // WHEN asset.addResourceMetadata(resource, 'PropName'); - const template = SynthUtils.synthesize(stack).template; - expect(template.Resources.MyResource.Metadata).toEqual({ + const template = Template.fromStack(stack); + expect(template.findResources('My::Resource::Type').MyResource.Metadata).toEqual({ 'aws:asset:path': 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', 'aws:asset:is-bundled': false, 'aws:asset:property': 'PropName', @@ -377,8 +380,8 @@ describe('staging', () => { // WHEN asset.addResourceMetadata(resource, 'PropName'); - const template = SynthUtils.synthesize(stack).template; - expect(template.Resources.MyResource.Metadata).toEqual({ + const template = Template.fromStack(stack); + expect(template.findResources('My::Resource::Type').MyResource.Metadata).toEqual({ 'aws:asset:path': SAMPLE_ASSET_DIR, 'aws:asset:is-bundled': false, 'aws:asset:property': 'PropName', diff --git a/packages/@aws-cdk/aws-s3-deployment/README.md b/packages/@aws-cdk/aws-s3-deployment/README.md index 37a571b075f40..84ccec9e23c55 100644 --- a/packages/@aws-cdk/aws-s3-deployment/README.md +++ b/packages/@aws-cdk/aws-s3-deployment/README.md @@ -9,8 +9,6 @@ -> __Status: Experimental__ - This library allows populating an S3 bucket with the contents of .zip files from other S3 buckets or from local disk. @@ -20,13 +18,13 @@ enabled and populates it from a local directory on disk. ```ts const websiteBucket = new s3.Bucket(this, 'WebsiteBucket', { websiteIndexDocument: 'index.html', - publicReadAccess: true + publicReadAccess: true, }); new s3deploy.BucketDeployment(this, 'DeployWebsite', { sources: [s3deploy.Source.asset('./website-dist')], destinationBucket: websiteBucket, - destinationKeyPrefix: 'web/static' // optional prefix in destination bucket + destinationKeyPrefix: 'web/static', // optional prefix in destination bucket }); ``` @@ -44,6 +42,25 @@ This is what happens under the hood: `websiteBucket`). If there is more than one source, the sources will be downloaded and merged pre-deployment at this step. +If you are referencing the filled bucket in another construct that depends on +the files already be there, be sure to use `deployment.deployedBucket`. This +will ensure the bucket deployment has finished before the resource that uses +the bucket is created: + +```ts +declare const websiteBucket: s3.Bucket; + +const deployment = new s3deploy.BucketDeployment(this, 'DeployWebsite', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], + destinationBucket: websiteBucket, +}); + +new ConstructThatReadsFromTheBucket(this, 'Consumer', { + // Use 'deployment.deployedBucket' instead of 'websiteBucket' here + bucket: deployment.deployedBucket, +}); +``` + ## Supported sources The following source types are supported for bucket deployments: @@ -51,6 +68,10 @@ The following source types are supported for bucket deployments: - Local .zip file: `s3deploy.Source.asset('/path/to/local/file.zip')` - Local directory: `s3deploy.Source.asset('/path/to/local/directory')` - Another bucket: `s3deploy.Source.bucket(bucket, zipObjectKey)` +- String data: `s3deploy.Source.data('object-key.txt', 'hello, world!')` + (supports [deploy-time values](#data-with-deploy-time-values)) +- JSON data: `s3deploy.Source.jsonData('object-key.json', { json: 'object' })` + (supports [deploy-time values](#data-with-deploy-time-values)) To create a source from a single file, you can pass `AssetOptions` to exclude all but a single file: @@ -110,6 +131,7 @@ when the `BucketDeployment` resource is created or updated. You can use the opti this behavior, in which case the files will not be deleted. ```ts +declare const destinationBucket: s3.Bucket; new s3deploy.BucketDeployment(this, 'DeployMeWithoutDeletingFilesOnDestination', { sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], destinationBucket, @@ -122,17 +144,18 @@ each with its own characteristics. For example, you can set different cache-cont based on file extensions: ```ts -new BucketDeployment(this, 'BucketDeployment', { - sources: [Source.asset('./website', { exclude: ['index.html'] })], - destinationBucket: bucket, - cacheControl: [CacheControl.fromString('max-age=31536000,public,immutable')], +declare const destinationBucket: s3.Bucket; +new s3deploy.BucketDeployment(this, 'BucketDeployment', { + sources: [s3deploy.Source.asset('./website', { exclude: ['index.html'] })], + destinationBucket, + cacheControl: [s3deploy.CacheControl.fromString('max-age=31536000,public,immutable')], prune: false, }); -new BucketDeployment(this, 'HTMLBucketDeployment', { - sources: [Source.asset('./website', { exclude: ['*', '!index.html'] })], - destinationBucket: bucket, - cacheControl: [CacheControl.fromString('max-age=0,no-cache,no-store,must-revalidate')], +new s3deploy.BucketDeployment(this, 'HTMLBucketDeployment', { + sources: [s3deploy.Source.asset('./website', { exclude: ['*', '!index.html'] })], + destinationBucket, + cacheControl: [s3deploy.CacheControl.fromString('max-age=0,no-cache,no-store,must-revalidate')], prune: false, }); ``` @@ -142,19 +165,21 @@ new BucketDeployment(this, 'HTMLBucketDeployment', { There are two points at which filters are evaluated in a deployment: asset bundling and the actual deployment. If you simply want to exclude files in the asset bundling process, you should leverage the `exclude` property of `AssetOptions` when defining your source: ```ts -new BucketDeployment(this, 'HTMLBucketDeployment', { - sources: [Source.asset('./website', { exclude: ['*', '!index.html'] })], - destinationBucket: bucket, +declare const destinationBucket: s3.Bucket; +new s3deploy.BucketDeployment(this, 'HTMLBucketDeployment', { + sources: [s3deploy.Source.asset('./website', { exclude: ['*', '!index.html'] })], + destinationBucket, }); ``` If you want to specify filters to be used in the deployment process, you can use the `exclude` and `include` filters on `BucketDeployment`. If excluded, these files will not be deployed to the destination bucket. In addition, if the file already exists in the destination bucket, it will not be deleted if you are using the `prune` option: ```ts +declare const destinationBucket: s3.Bucket; new s3deploy.BucketDeployment(this, 'DeployButExcludeSpecificFiles', { sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], destinationBucket, - exclude: ['*.txt'] + exclude: ['*.txt'], }); ``` @@ -189,7 +214,7 @@ and [`aws s3 sync` documentation](https://docs.aws.amazon.com/cli/latest/referen ```ts const websiteBucket = new s3.Bucket(this, 'WebsiteBucket', { websiteIndexDocument: 'index.html', - publicReadAccess: true + publicReadAccess: true, }); new s3deploy.BucketDeployment(this, 'DeployWebsite', { @@ -201,9 +226,12 @@ new s3deploy.BucketDeployment(this, 'DeployWebsite', { // system-defined metadata contentType: "text/html", contentLanguage: "en", - storageClass: StorageClass.INTELLIGENT_TIERING, - serverSideEncryption: ServerSideEncryption.AES_256, - cacheControl: [CacheControl.setPublic(), CacheControl.maxAge(cdk.Duration.hours(1))], + storageClass: s3deploy.StorageClass.INTELLIGENT_TIERING, + serverSideEncryption: s3deploy.ServerSideEncryption.AES_256, + cacheControl: [ + s3deploy.CacheControl.setPublic(), + s3deploy.CacheControl.maxAge(Duration.hours(1)), + ], accessControl: s3.BucketAccessControl.BUCKET_OWNER_FULL_CONTROL, }); ``` @@ -250,30 +278,63 @@ Please note that creating VPC inline may cause stack deletion failures. It is sh To avoid such condition, keep your network infra (VPC) in a separate stack and pass as props. ```ts +declare const destinationBucket: s3.Bucket; +declare const vpc: ec2.Vpc; + new s3deploy.BucketDeployment(this, 'DeployMeWithEfsStorage', { - sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], - destinationBucket, - destinationKeyPrefix: 'efs/', - useEfs: true, - vpc: new ec2.Vpc(this, 'Vpc'), - retainOnDelete: false, + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], + destinationBucket, + destinationKeyPrefix: 'efs/', + useEfs: true, + vpc, + retainOnDelete: false, +}); +``` + +## Data with deploy-time values + +The content passed to `Source.data()` or `Source.jsonData()` can include +references that will get resolved only during deployment. + +For example: + +```ts +import * as sns from '@aws-cdk/aws-sns'; + +declare const destinationBucket: s3.Bucket; +declare const topic: sns.Topic; + +const appConfig = { + topic_arn: topic.topicArn, + base_url: 'https://my-endpoint', +}; + +new s3deploy.BucketDeployment(this, 'BucketDeployment', { + sources: [s3deploy.Source.jsonData('config.json', appConfig)], + destinationBucket, }); ``` +The value in `topic.topicArn` is a deploy-time value. It only gets resolved +during deployment by placing a marker in the generated source file and +substituting it when its deployed to the destination with the actual value. + ## Notes -- This library uses an AWS CloudFormation custom resource which about 10MiB in +- This library uses an AWS CloudFormation custom resource which is about 10MiB in size. The code of this resource is bundled with this library. - AWS Lambda execution time is limited to 15min. This limits the amount of data which can be deployed into the bucket by this timeout. - When the `BucketDeployment` is removed from the stack, the contents are retained in the destination bucket ([#952](https://github.com/aws/aws-cdk/issues/952)). -- Bucket deployment _only happens_ during stack create/update. This means that - if you wish to update the contents of the destination, you will need to - change the source s3 key (or bucket), so that the resource will be updated. - This is inline with best practices. If you use local disk assets, this will - happen automatically whenever you modify the asset, since the S3 key is based - on a hash of the asset contents. +- If you are using `s3deploy.Source.bucket()` to take the file source from + another bucket: the deployed files will only be updated if the key (file name) + of the file in the source bucket changes. Mutating the file in place will not + be good enough: the custom resource will simply not run if the properties don't + change. + - If you use assets (`s3deploy.Source.asset()`) you don't need to worry + about this: the asset system will make sure that if the files have changed, + the file name is unique and the deployment will run. ## Development diff --git a/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts b/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts index 76bbae6a4b290..31a7ec92e2db5 100644 --- a/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts +++ b/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts @@ -239,6 +239,10 @@ export interface BucketDeploymentProps { * other S3 buckets or from local disk */ export class BucketDeployment extends CoreConstruct { + private readonly cr: cdk.CustomResource; + private _deployedBucket?: s3.IBucket; + private requestDestinationArn: boolean = false; + constructor(scope: Construct, id: string, props: BucketDeploymentProps) { super(scope, id); @@ -323,13 +327,18 @@ export class BucketDeployment extends CoreConstruct { })); } + // to avoid redundant stack updates, only include "SourceMarkers" if one of + // the sources actually has markers. + const hasMarkers = sources.some(source => source.markers); + const crUniqueId = `CustomResource${this.renderUniqueId(props.memoryLimit, props.vpc)}`; - const cr = new cdk.CustomResource(this, crUniqueId, { + this.cr = new cdk.CustomResource(this, crUniqueId, { serviceToken: handler.functionArn, resourceType: 'Custom::CDKBucketDeployment', properties: { SourceBucketNames: sources.map(source => source.bucket.bucketName), SourceObjectKeys: sources.map(source => source.zipObjectKey), + SourceMarkers: hasMarkers ? sources.map(source => source.markers ?? {}) : undefined, DestinationBucketName: props.destinationBucket.bucketName, DestinationBucketKeyPrefix: props.destinationKeyPrefix, RetainOnDelete: props.retainOnDelete, @@ -340,13 +349,15 @@ export class BucketDeployment extends CoreConstruct { SystemMetadata: mapSystemMetadata(props), DistributionId: props.distribution?.distributionId, DistributionPaths: props.distributionPaths, + // Passing through the ARN sequences dependencees on the deployment + DestinationBucketArn: cdk.Lazy.string({ produce: () => this.requestDestinationArn ? props.destinationBucket.bucketArn : undefined }), }, }); let prefix: string = props.destinationKeyPrefix ? `:${props.destinationKeyPrefix}` : ''; - prefix += `:${cr.node.addr.substr(-8)}`; + prefix += `:${this.cr.node.addr.substr(-8)}`; const tagKey = CUSTOM_RESOURCE_OWNER_TAG + prefix; // destinationKeyPrefix can be 104 characters before we hit @@ -400,6 +411,21 @@ export class BucketDeployment extends CoreConstruct { } + /** + * The bucket after the deployment + * + * If you want to reference the destination bucket in another construct and make sure the + * bucket deployment has happened before the next operation is started, pass the other construct + * a reference to `deployment.deployedBucket`. + * + * Doing this replaces calling `otherResource.node.addDependency(deployment)`. + */ + public get deployedBucket(): s3.IBucket { + this.requestDestinationArn = true; + this._deployedBucket = this._deployedBucket ?? s3.Bucket.fromBucketArn(this, 'DestinationBucket', cdk.Token.asString(this.cr.getAtt('DestinationBucketArn'))); + return this._deployedBucket; + } + private renderUniqueId(memoryLimit?: number, vpc?: ec2.IVpc) { let uuid = ''; diff --git a/packages/@aws-cdk/aws-s3-deployment/lib/lambda/index.py b/packages/@aws-cdk/aws-s3-deployment/lib/lambda/index.py index 466218b0fa134..877b78a8452ee 100644 --- a/packages/@aws-cdk/aws-s3-deployment/lib/lambda/index.py +++ b/packages/@aws-cdk/aws-s3-deployment/lib/lambda/index.py @@ -20,6 +20,7 @@ CFN_SUCCESS = "SUCCESS" CFN_FAILED = "FAILED" ENV_KEY_MOUNT_PATH = "MOUNT_PATH" +ENV_KEY_SKIP_CLEANUP = "SKIP_CLEANUP" CUSTOM_RESOURCE_OWNER_TAG = "aws-cdk:cr-owned" @@ -45,6 +46,7 @@ def cfn_error(message=None): try: source_bucket_names = props['SourceBucketNames'] source_object_keys = props['SourceObjectKeys'] + source_markers = props.get('SourceMarkers', None) dest_bucket_name = props['DestinationBucketName'] dest_bucket_prefix = props.get('DestinationBucketKeyPrefix', '') retain_on_delete = props.get('RetainOnDelete', "true") == "true" @@ -55,6 +57,11 @@ def cfn_error(message=None): exclude = props.get('Exclude', []) include = props.get('Include', []) + # backwards compatibility - if "SourceMarkers" is not specified, + # assume all sources have an empty market map + if source_markers is None: + source_markers = [{} for i in range(len(source_bucket_names))] + default_distribution_path = dest_bucket_prefix if not default_distribution_path.endswith("/"): default_distribution_path += "/" @@ -71,7 +78,7 @@ def cfn_error(message=None): if dest_bucket_prefix == "/": dest_bucket_prefix = "" - s3_source_zips = map(lambda name, key: "s3://%s/%s" % (name, key), source_bucket_names, source_object_keys) + s3_source_zips = list(map(lambda name, key: "s3://%s/%s" % (name, key), source_bucket_names, source_object_keys)) s3_dest = "s3://%s/%s" % (dest_bucket_name, dest_bucket_prefix) old_s3_dest = "s3://%s/%s" % (old_props.get("DestinationBucketName", ""), old_props.get("DestinationBucketKeyPrefix", "")) @@ -106,12 +113,15 @@ def cfn_error(message=None): aws_command("s3", "rm", old_s3_dest, "--recursive") if request_type == "Update" or request_type == "Create": - s3_deploy(s3_source_zips, s3_dest, user_metadata, system_metadata, prune, exclude, include) + s3_deploy(s3_source_zips, s3_dest, user_metadata, system_metadata, prune, exclude, include, source_markers) if distribution_id: cloudfront_invalidate(distribution_id, distribution_paths) - cfn_send(event, context, CFN_SUCCESS, physicalResourceId=physical_id) + cfn_send(event, context, CFN_SUCCESS, physicalResourceId=physical_id, responseData={ + # Passing through the ARN sequences dependencees on the deployment + 'DestinationBucketArn': props.get('DestinationBucketArn') + }) except KeyError as e: cfn_error("invalid request. Missing key %s" % str(e)) except Exception as e: @@ -120,7 +130,11 @@ def cfn_error(message=None): #--------------------------------------------------------------------------------------------------- # populate all files from s3_source_zips to a destination bucket -def s3_deploy(s3_source_zips, s3_dest, user_metadata, system_metadata, prune, exclude, include): +def s3_deploy(s3_source_zips, s3_dest, user_metadata, system_metadata, prune, exclude, include, source_markers): + # list lengths are equal + if len(s3_source_zips) != len(source_markers): + raise Exception("'source_markers' and 's3_source_zips' must be the same length") + # create a temporary working directory in /tmp or if enabled an attached efs volume if ENV_KEY_MOUNT_PATH in os.environ: workdir = os.getenv(ENV_KEY_MOUNT_PATH) + "/" + str(uuid4()) @@ -136,13 +150,16 @@ def s3_deploy(s3_source_zips, s3_dest, user_metadata, system_metadata, prune, ex try: # download the archive from the source and extract to "contents" - for s3_source_zip in s3_source_zips: + for i in range(len(s3_source_zips)): + s3_source_zip = s3_source_zips[i] + markers = source_markers[i] + archive=os.path.join(workdir, str(uuid4())) logger.info("archive: %s" % archive) aws_command("s3", "cp", s3_source_zip, archive) logger.info("| extracting archive to: %s\n" % contents_dir) - with ZipFile(archive, "r") as zip: - zip.extractall(contents_dir) + logger.info("| markers: %s" % markers) + extract_and_replace_markers(archive, contents_dir, markers) # sync from "contents" to destination @@ -163,7 +180,8 @@ def s3_deploy(s3_source_zips, s3_dest, user_metadata, system_metadata, prune, ex s3_command.extend(create_metadata_args(user_metadata, system_metadata)) aws_command(*s3_command) finally: - shutil.rmtree(workdir) + if not os.getenv(ENV_KEY_SKIP_CLEANUP): + shutil.rmtree(workdir) #--------------------------------------------------------------------------------------------------- # invalidate files in the CloudFront distribution edge caches @@ -257,3 +275,29 @@ def bucket_owned(bucketName, keyPrefix): logger.info("| error getting tags from bucket") logger.exception(e) return False + +# extract archive and replace markers in output files +def extract_and_replace_markers(archive, contents_dir, markers): + with ZipFile(archive, "r") as zip: + zip.extractall(contents_dir) + + # replace markers for this source + for file in zip.namelist(): + file_path = os.path.join(contents_dir, file) + if os.path.isdir(file_path): continue + replace_markers(file_path, markers) + +def replace_markers(filename, markers): + # convert the dict of string markers to binary markers + replace_tokens = dict([(k.encode('utf-8'), v.encode('utf-8')) for k, v in markers.items()]) + + outfile = filename + '.new' + with open(filename, 'rb') as fi, open(outfile, 'wb') as fo: + for line in fi: + for token in replace_tokens: + line = line.replace(token, replace_tokens[token]) + fo.write(line) + + # # delete the original file and rename the new one to the original + os.remove(filename) + os.rename(outfile, filename) \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/lib/render-data.ts b/packages/@aws-cdk/aws-s3-deployment/lib/render-data.ts new file mode 100644 index 0000000000000..fa73c55dc2fba --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/lib/render-data.ts @@ -0,0 +1,81 @@ +import { Stack } from '@aws-cdk/core'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +export interface Content { + readonly text: string; + readonly markers: Record; +} + +/** + * Renders the given string data as deployable content with markers substituted + * for all "Ref" and "Fn::GetAtt" objects. + * + * @param scope Construct scope + * @param data The input data + * @returns The markered text (`text`) and a map that maps marker names to their + * values (`markers`). + */ +export function renderData(scope: Construct, data: string): Content { + const obj = Stack.of(scope).resolve(data); + if (typeof(obj) === 'string') { + return { text: obj, markers: {} }; + } + + if (typeof(obj) !== 'object') { + throw new Error(`Unexpected: after resolve() data must either be a string or a CloudFormation intrinsic. Got: ${JSON.stringify(obj)}`); + } + + let markerIndex = 0; + const markers: Record = {}; + const result = new Array(); + const fnJoin: FnJoin | undefined = obj['Fn::Join']; + + if (fnJoin) { + const sep = fnJoin[0]; + const parts = fnJoin[1]; + + if (sep !== '') { + throw new Error(`Unexpected "Fn::Join", expecting separator to be an empty string but got "${sep}"`); + } + + for (const part of parts) { + if (typeof (part) === 'string') { + result.push(part); + continue; + } + + if (typeof (part) === 'object') { + addMarker(part); + continue; + } + + throw new Error(`Unexpected "Fn::Join" part, expecting string or object but got ${typeof (part)}`); + } + + } else if (obj.Ref || obj['Fn::GetAtt']) { + addMarker(obj); + } else { + throw new Error('Unexpected: Expecting `resolve()` to return "Fn::Join", "Ref" or "Fn::GetAtt"'); + } + + function addMarker(part: Ref | GetAtt) { + const keys = Object.keys(part); + if (keys.length !== 1 || (keys[0] != 'Ref' && keys[0] != 'Fn::GetAtt')) { + throw new Error(`Invalid CloudFormation reference. "Ref" or "Fn::GetAtt". Got ${JSON.stringify(part)}`); + } + + const marker = `<>`; + result.push(marker); + markers[marker] = part; + } + + return { text: result.join(''), markers }; +} + +type FnJoin = [string, FnJoinPart[]]; +type FnJoinPart = string | Ref | GetAtt; +type Ref = { Ref: string }; +type GetAtt = { 'Fn::GetAtt': [string, string] }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/lib/source.ts b/packages/@aws-cdk/aws-s3-deployment/lib/source.ts index 8c22f49d791e3..0a02dd4503b01 100644 --- a/packages/@aws-cdk/aws-s3-deployment/lib/source.ts +++ b/packages/@aws-cdk/aws-s3-deployment/lib/source.ts @@ -1,6 +1,10 @@ +import * as fs from 'fs'; +import { join, dirname } from 'path'; import * as iam from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; import * as s3_assets from '@aws-cdk/aws-s3-assets'; +import { FileSystem, Stack } from '@aws-cdk/core'; +import { renderData } from './render-data'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main // eslint-disable-next-line no-duplicate-imports, import/order @@ -19,6 +23,12 @@ export interface SourceConfig { * An S3 object key in the source bucket that points to a zip file. */ readonly zipObjectKey: string; + + /** + * A set of markers to substitute in the source content. + * @default - no markers + */ + readonly markers?: Record; } /** @@ -50,6 +60,8 @@ export interface ISource { * Source.bucket(bucket, key) * Source.asset('/local/path/to/directory') * Source.asset('/local/path/to/a/file.zip') + * Source.data('hello/world/file.txt', 'Hello, world!') + * Source.data('config.json', { baz: topic.topicArn }) * */ export class Source { @@ -110,5 +122,51 @@ export class Source { }; } + /** + * Deploys an object with the specified string contents into the bucket. The + * content can include deploy-time values (such as `snsTopic.topicArn`) that + * will get resolved only during deployment. + * + * To store a JSON object use `Source.jsonData()`. + * + * @param objectKey The destination S3 object key (relative to the root of the + * S3 deployment). + * @param data The data to be stored in the object. + */ + public static data(objectKey: string, data: string): ISource { + return { + bind: (scope: Construct, context?: DeploymentSourceContext) => { + const workdir = FileSystem.mkdtemp('s3-deployment'); + const outputPath = join(workdir, objectKey); + const rendered = renderData(scope, data); + fs.mkdirSync(dirname(outputPath), { recursive: true }); + fs.writeFileSync(outputPath, rendered.text); + const asset = this.asset(workdir).bind(scope, context); + return { + bucket: asset.bucket, + zipObjectKey: asset.zipObjectKey, + markers: rendered.markers, + }; + }, + }; + } + + /** + * Deploys an object with the specified JSON object into the bucket. The + * object can include deploy-time values (such as `snsTopic.topicArn`) that + * will get resolved only during deployment. + * + * @param objectKey The destination S3 object key (relative to the root of the + * S3 deployment). + * @param obj A JSON object. + */ + public static jsonData(objectKey: string, obj: any): ISource { + return { + bind: (scope: Construct, context?: DeploymentSourceContext) => { + return Source.data(objectKey, Stack.of(scope).toJsonString(obj)).bind(scope, context); + }, + }; + } + private constructor() { } } diff --git a/packages/@aws-cdk/aws-s3-deployment/package.json b/packages/@aws-cdk/aws-s3-deployment/package.json index df08ee72901ab..104ff0bc223d2 100644 --- a/packages/@aws-cdk/aws-s3-deployment/package.json +++ b/packages/@aws-cdk/aws-s3-deployment/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -78,13 +85,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-cloudfront": "0.0.0", diff --git a/packages/@aws-cdk/aws-s3-deployment/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-s3-deployment/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..8733f436c5e29 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/rosetta/default.ts-fixture @@ -0,0 +1,20 @@ +// Fixture with packages imported, but nothing else +import { Duration, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as s3deploy from '@aws-cdk/aws-s3-deployment'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as ec2 from'@aws-cdk/aws-ec2'; +import * as path from 'path'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} + +class ConstructThatReadsFromTheBucket { + constructor(...args: any[]) { + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts b/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts index 38023cc0154e6..9706f9d9d33f1 100644 --- a/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts +++ b/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts @@ -1,6 +1,6 @@ +import { readFileSync } from 'fs'; import * as path from 'path'; -import { MatchStyle, objectLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cloudfront from '@aws-cdk/aws-cloudfront'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; @@ -27,7 +27,7 @@ test('deploy from local directory asset', () => { }); // THEN - expect(stack).toHaveResource('Custom::CDKBucketDeployment', { + Template.fromStack(stack).hasResourceProperties('Custom::CDKBucketDeployment', { ServiceToken: { 'Fn::GetAtt': [ 'CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536', @@ -89,7 +89,7 @@ test('deploy with configured log retention', () => { }); // THEN - expect(stack).toHaveResourceLike('Custom::LogRetention', { RetentionInDays: 7 }); + Template.fromStack(stack).hasResourceProperties('Custom::LogRetention', { RetentionInDays: 7 }); }); test('deploy from local directory assets', () => { @@ -107,7 +107,7 @@ test('deploy from local directory assets', () => { }); // THEN - expect(stack).toHaveResource('Custom::CDKBucketDeployment', { + Template.fromStack(stack).hasResourceProperties('Custom::CDKBucketDeployment', { ServiceToken: { 'Fn::GetAtt': [ 'CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536', @@ -235,7 +235,7 @@ test('deploy from a local .zip file when efs is enabled', () => { }); //THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Environment: { Variables: { MOUNT_PATH: '/mnt/lambda', @@ -315,7 +315,7 @@ testDeprecated('honors passed asset options', () => { }); // THEN - expect(stack).toHaveResource('Custom::CDKBucketDeployment', { + Template.fromStack(stack).hasResourceProperties('Custom::CDKBucketDeployment', { ServiceToken: { 'Fn::GetAtt': [ 'CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536', @@ -377,7 +377,7 @@ test('retainOnDelete can be used to retain files when resource is deleted', () = }); // THEN - expect(stack).toHaveResource('Custom::CDKBucketDeployment', { + Template.fromStack(stack).hasResourceProperties('Custom::CDKBucketDeployment', { RetainOnDelete: true, }); }); @@ -398,7 +398,7 @@ test('user metadata is correctly transformed', () => { }); // THEN - expect(stack).toHaveResource('Custom::CDKBucketDeployment', { + Template.fromStack(stack).hasResourceProperties('Custom::CDKBucketDeployment', { UserMetadata: { a: '1', b: '2' }, }); }); @@ -427,7 +427,7 @@ test('system metadata is correctly transformed', () => { }); // THEN - expect(stack).toHaveResource('Custom::CDKBucketDeployment', { + Template.fromStack(stack).hasResourceProperties('Custom::CDKBucketDeployment', { SystemMetadata: { 'content-type': 'text/html', 'content-language': 'en', @@ -472,7 +472,7 @@ test.each(Object.entries(accessControlMap) as [s3.BucketAccessControl, string][] }); // THEN - expect(stack).toHaveResource('Custom::CDKBucketDeployment', { + Template.fromStack(stack).hasResourceProperties('Custom::CDKBucketDeployment', { SystemMetadata: { acl: systemMetadataKeyword, }, @@ -537,7 +537,7 @@ test('distribution can be used to provide a CloudFront distribution for invalida distributionPaths: ['/images/*'], }); - expect(stack).toHaveResource('Custom::CDKBucketDeployment', { + Template.fromStack(stack).hasResourceProperties('Custom::CDKBucketDeployment', { DistributionId: { Ref: 'DistributionCFDistribution882A7313', }, @@ -567,7 +567,7 @@ test('invalidation can happen without distributionPaths provided', () => { distribution, }); - expect(stack).toHaveResource('Custom::CDKBucketDeployment', { + Template.fromStack(stack).hasResourceProperties('Custom::CDKBucketDeployment', { DistributionId: { Ref: 'DistributionCFDistribution882A7313', }, @@ -626,7 +626,7 @@ testFutureBehavior('lambda execution role gets permissions to read from the sour }); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -666,6 +666,10 @@ testFutureBehavior('lambda execution role gets permissions to read from the sour 's3:List*', 's3:DeleteObject*', 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', 's3:Abort*', ], Effect: 'Allow', @@ -735,9 +739,9 @@ test('memoryLimit can be used to specify the memory limit for the deployment res // we expect to find only two handlers, one for each configuration - expect(stack).toCountResources('AWS::Lambda::Function', 2); - expect(stack).toHaveResource('AWS::Lambda::Function', { MemorySize: 256 }); - expect(stack).toHaveResource('AWS::Lambda::Function', { MemorySize: 1024 }); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 2); + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { MemorySize: 256 }); + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { MemorySize: 1024 }); }); test('deployment allows custom role to be supplied', () => { @@ -757,9 +761,9 @@ test('deployment allows custom role to be supplied', () => { }); // THEN - expect(stack).toCountResources('AWS::IAM::Role', 1); - expect(stack).toCountResources('AWS::Lambda::Function', 1); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Role: { 'Fn::GetAtt': [ 'Role1ABCC5F0', @@ -782,7 +786,7 @@ test('deploy without deleting missing files from destination', () => { prune: false, }); - expect(stack).toHaveResourceLike('Custom::CDKBucketDeployment', { + Template.fromStack(stack).hasResourceProperties('Custom::CDKBucketDeployment', { Prune: false, }); }); @@ -799,7 +803,7 @@ test('deploy with excluded files from destination', () => { exclude: ['sample.js'], }); - expect(stack).toHaveResourceLike('Custom::CDKBucketDeployment', { + Template.fromStack(stack).hasResourceProperties('Custom::CDKBucketDeployment', { Exclude: ['sample.js'], }); }); @@ -817,7 +821,7 @@ test('deploy with included files from destination', () => { include: ['sample.js'], }); - expect(stack).toHaveResourceLike('Custom::CDKBucketDeployment', { + Template.fromStack(stack).hasResourceProperties('Custom::CDKBucketDeployment', { Include: ['sample.js'], }); }); @@ -836,7 +840,7 @@ test('deploy with excluded and included files from destination', () => { include: ['sample/include.json'], }); - expect(stack).toHaveResourceLike('Custom::CDKBucketDeployment', { + Template.fromStack(stack).hasResourceProperties('Custom::CDKBucketDeployment', { Exclude: ['sample/*'], Include: ['sample/include.json'], }); @@ -856,7 +860,7 @@ test('deploy with multiple exclude and include filters', () => { include: ['sample/include.json', 'another/include.json'], }); - expect(stack).toHaveResourceLike('Custom::CDKBucketDeployment', { + Template.fromStack(stack).hasResourceProperties('Custom::CDKBucketDeployment', { Exclude: ['sample/*', 'another/*'], Include: ['sample/include.json', 'another/include.json'], }); @@ -877,7 +881,7 @@ test('deployment allows vpc to be implicitly supplied to lambda', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { VpcConfig: { SecurityGroupIds: [ { @@ -922,7 +926,7 @@ test('deployment allows vpc and subnets to be implicitly supplied to lambda', () }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { VpcConfig: { SecurityGroupIds: [ { @@ -941,6 +945,68 @@ test('deployment allows vpc and subnets to be implicitly supplied to lambda', () }); }); +test('s3 deployment bucket is identical to destination bucket', () => { + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Dest'); + + // WHEN + const bd = new s3deploy.BucketDeployment(stack, 'Deployment', { + destinationBucket: bucket, + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], + }); + + // Call this function + void(bd.deployedBucket); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('Custom::CDKBucketDeployment', { + // Since this utilizes GetAtt, we know CFN will deploy the bucket first + // before deploying other resources that rely call the destination bucket. + DestinationBucketArn: { 'Fn::GetAtt': ['DestC383B82A', 'Arn'] }, + }); +}); + +test('using deployment bucket references the destination bucket by means of the CustomResource', () => { + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Dest'); + const deployment = new s3deploy.BucketDeployment(stack, 'Deployment', { + destinationBucket: bucket, + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], + }); + + // WHEN + new cdk.CfnOutput(stack, 'DestinationArn', { + value: deployment.deployedBucket.bucketArn, + }); + new cdk.CfnOutput(stack, 'DestinationName', { + value: deployment.deployedBucket.bucketName, + }); + + // THEN + + const template = Template.fromStack(stack); + expect(template.findOutputs('*')).toEqual({ + DestinationArn: { + Value: { 'Fn::GetAtt': ['DeploymentCustomResource47E8B2E6', 'DestinationBucketArn'] }, + }, + DestinationName: { + Value: { + 'Fn::Select': [0, { + 'Fn::Split': ['/', { + 'Fn::Select': [5, { + 'Fn::Split': [':', + { 'Fn::GetAtt': ['DeploymentCustomResource47E8B2E6', 'DestinationBucketArn'] }], + }], + }], + }], + }, + }, + }); +}); + test('resource id includes memory and vpc', () => { // GIVEN @@ -957,13 +1023,13 @@ test('resource id includes memory and vpc', () => { }); // THEN - expect(stack).toMatchTemplate({ - Resources: objectLike({ - DeployWithVpc2CustomResource256MiBc8a39596cb8641929fcf6a288bc9db5ab7b0f656ad3C5F6E78: objectLike({ + Template.fromStack(stack).templateMatches({ + Resources: Match.objectLike({ + DeployWithVpc2CustomResource256MiBc8a39596cb8641929fcf6a288bc9db5ab7b0f656ad3C5F6E78: Match.objectLike({ Type: 'Custom::CDKBucketDeployment', }), }), - }, MatchStyle.SUPERSET); + }); }); test('bucket includes custom resource owner tag', () => { @@ -983,7 +1049,7 @@ test('bucket includes custom resource owner tag', () => { }); // THEN - expect(stack).toHaveResource('AWS::S3::Bucket', { + Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { Tags: [{ Key: 'aws-cdk:cr-owned:/a/b/c:971e1fa8', Value: 'true', @@ -1040,7 +1106,7 @@ test('bucket has multiple deployments', () => { }); // THEN - expect(stack).toHaveResource('AWS::S3::Bucket', { + Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { Tags: [ { Key: 'aws-cdk:cr-owned:/a/b/c:6da0a4ab', @@ -1057,3 +1123,82 @@ test('bucket has multiple deployments', () => { ], }); }); + +test('"SourceMarkers" is not included if none of the sources have markers', () => { + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Bucket'); + new s3deploy.BucketDeployment(stack, 'DeployWithVpc3', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], + destinationBucket: bucket, + }); + + const map = Template.fromStack(stack).findResources('Custom::CDKBucketDeployment'); + expect(map).toBeDefined(); + const resource = map[Object.keys(map)[0]]; + expect(Object.keys(resource.Properties)).toStrictEqual([ + 'ServiceToken', + 'SourceBucketNames', + 'SourceObjectKeys', + 'DestinationBucketName', + 'Prune', + ]); +}); + +test('Source.data() can be used to create a file with string contents', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Test'); + const bucket = new s3.Bucket(stack, 'Bucket'); + + const source = s3deploy.Source.data('my/path.txt', 'hello, world'); + + new s3deploy.BucketDeployment(stack, 'DeployWithVpc3', { + sources: [source], + destinationBucket: bucket, + destinationKeyPrefix: '/x/z', + }); + + const result = app.synth(); + const content = readDataFile(result, 'c5b1c01fc092abf1da35f6772e7c507e566aaa69404025c080ba074c69741755', 'my/path.txt'); + expect(content).toStrictEqual('hello, world'); +}); + +test('Source.jsonData() can be used to create a file with a JSON object', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Test'); + const bucket = new s3.Bucket(stack, 'Bucket'); + + const config = { + foo: 'bar', + sub: { + hello: bucket.bucketArn, + }, + }; + + new s3deploy.BucketDeployment(stack, 'DeployWithVpc3', { + sources: [s3deploy.Source.jsonData('app-config.json', config)], + destinationBucket: bucket, + }); + + const result = app.synth(); + const obj = JSON.parse(readDataFile(result, '6a9e1763f42401799363d87d16b238c89bf75a56f2a3f67498a3224573062b0c', 'app-config.json')); + expect(obj).toStrictEqual({ + foo: 'bar', + sub: { + hello: '<>', + }, + }); + + // verify marker is mapped to the bucket ARN in the resource props + Template.fromJSON(result.stacks[0].template).hasResourceProperties('Custom::CDKBucketDeployment', { + SourceMarkers: [ + { '<>': { 'Fn::GetAtt': ['Bucket83908E77', 'Arn'] } }, + ], + }); +}); + + +function readDataFile(casm: cxapi.CloudAssembly, assetId: string, filePath: string): string { + const asset = casm.stacks[0].assets.find(a => a.id === assetId); + if (!asset) { throw new Error('Asset not found'); } + return readFileSync(path.join(casm.directory, asset.path, filePath), 'utf-8'); +} diff --git a/packages/@aws-cdk/aws-s3-deployment/test/content.test.ts b/packages/@aws-cdk/aws-s3-deployment/test/content.test.ts new file mode 100644 index 0000000000000..3cc27a2dc7262 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/content.test.ts @@ -0,0 +1,122 @@ +import * as lambda from '@aws-cdk/aws-lambda'; +import * as s3 from '@aws-cdk/aws-s3'; +import { Lazy, Stack } from '@aws-cdk/core'; +import { Source } from '../lib'; +import { renderData } from '../lib/render-data'; + +test('simple string', () => { + const stack = new Stack(); + expect(renderData(stack, 'foo')).toStrictEqual({ + markers: {}, + text: 'foo', + }); +}); + +test('string with a "Ref" token', () => { + const stack = new Stack(); + const bucket = new s3.Bucket(stack, 'Bucket'); + + expect(renderData(stack, `foo-${bucket.bucketName}`)).toStrictEqual({ + text: 'foo-<>', + markers: { '<>': { Ref: 'Bucket83908E77' } }, + }); +}); + +test('string is a single "Ref" token', () => { + const stack = new Stack(); + const bucket = new s3.Bucket(stack, 'Bucket'); + + expect(renderData(stack, bucket.bucketName)).toStrictEqual({ + text: '<>', + markers: { '<>': { Ref: 'Bucket83908E77' } }, + }); +}); + +test('string is a single "Fn::GetAtt" token', () => { + const stack = new Stack(); + const bucket = new s3.Bucket(stack, 'Bucket'); + + expect(renderData(stack, bucket.bucketRegionalDomainName)).toStrictEqual({ + text: '<>', + markers: { '<>': { 'Fn::GetAtt': ['Bucket83908E77', 'RegionalDomainName'] } }, + }); +}); + +test('string with a "Fn::GetAtt" token', () => { + const stack = new Stack(); + const bucket = new s3.Bucket(stack, 'Bucket'); + + expect(renderData(stack, `foo-${bucket.bucketArn}`)).toStrictEqual({ + text: 'foo-<>', + markers: { '<>': { 'Fn::GetAtt': ['Bucket83908E77', 'Arn'] } }, + }); +}); + +test('multiple markers', () => { + const stack = new Stack(); + const bucket = new s3.Bucket(stack, 'Bucket'); + + expect(renderData(stack, `boom-${bucket.bucketName}-bam-${bucket.bucketArn}`)).toStrictEqual({ + text: 'boom-<>-bam-<>', + markers: { + '<>': { Ref: 'Bucket83908E77' }, + '<>': { 'Fn::GetAtt': ['Bucket83908E77', 'Arn'] }, + }, + }); +}); + +test('json-encoded string', () => { + const stack = new Stack(); + const bucket = new s3.Bucket(stack, 'Bucket'); + const json = { + BucketArn: bucket.bucketArn, + BucketName: bucket.bucketName, + }; + + expect(renderData(stack, JSON.stringify(json))).toStrictEqual({ + text: JSON.stringify({ BucketArn: '<>', BucketName: '<>' }), + markers: { + '<>': { 'Fn::GetAtt': ['Bucket83908E77', 'Arn'] }, + '<>': { Ref: 'Bucket83908E77' }, + }, + }); +}); + +test('markers are returned in the source config', () => { + const stack = new Stack(); + const handler = new lambda.Function(stack, 'Handler', { runtime: lambda.Runtime.NODEJS_14_X, code: lambda.Code.fromInline('foo'), handler: 'index.handler' }); + const actual = Source.data('file1.txt', `boom-${stack.account}`).bind(stack, { handlerRole: handler.role! }); + expect(actual.markers).toStrictEqual({ + '<>': { Ref: 'AWS::AccountId' }, + }); +}); + +test('lazy string which can be fully resolved', () => { + const stack = new Stack(); + + expect(renderData(stack, Lazy.string({ produce: () => 'resolved!' }))).toStrictEqual({ + text: 'resolved!', + markers: { }, + }); +}); + +test('lazy within a string which can be fully resolved', () => { + const stack = new Stack(); + const token = Lazy.string({ produce: () => 'resolved!' }); + + expect(renderData(stack, `hello, ${token}`)).toStrictEqual({ + text: 'hello, resolved!', + markers: { }, + }); +}); + +test('lazy string which resolves to something with a deploy-time value', () => { + const stack = new Stack(); + const token = Lazy.string({ produce: () => 'resolved!' }); + + expect(renderData(stack, `hello, ${token}`)).toStrictEqual({ + text: 'hello, resolved!', + markers: { }, + }); + +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-cloudfront.expected.json b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-cloudfront.expected.json index ae66a97b871bc..54fa70b01ed7f 100644 --- a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-cloudfront.expected.json +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-cloudfront.expected.json @@ -114,7 +114,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3Bucket2C6C817C" + "Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3Bucket09A62232" }, "S3Key": { "Fn::Join": [ @@ -127,7 +127,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6" + "Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE" } ] } @@ -140,7 +140,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6" + "Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE" } ] } @@ -230,7 +230,7 @@ "Properties": { "Content": { "S3Bucket": { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3Bucket59E5CFEF" }, "S3Key": { "Fn::Join": [ @@ -243,7 +243,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3VersionKey7EE70F5C" } ] } @@ -256,7 +256,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3VersionKey7EE70F5C" } ] } @@ -417,6 +417,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -467,7 +471,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3Bucket1BE31DB0" + "Ref": "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eS3BucketC3F9EAA2" }, "S3Key": { "Fn::Join": [ @@ -480,7 +484,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3VersionKeyDC38E49C" + "Ref": "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eS3VersionKey030ACBFF" } ] } @@ -493,7 +497,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3VersionKeyDC38E49C" + "Ref": "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eS3VersionKey030ACBFF" } ] } @@ -525,41 +529,41 @@ } }, "Parameters": { - "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3Bucket2C6C817C": { + "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3Bucket09A62232": { "Type": "String", - "Description": "S3 bucket for asset \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\"" + "Description": "S3 bucket for asset \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\"" }, - "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6": { + "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE": { "Type": "String", - "Description": "S3 key for asset version \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\"" + "Description": "S3 key for asset version \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\"" }, - "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709ArtifactHash17D48178": { + "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824ArtifactHash76F8FCF2": { "Type": "String", - "Description": "Artifact hash for asset \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\"" + "Description": "Artifact hash for asset \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\"" }, - "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7": { + "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3Bucket59E5CFEF": { "Type": "String", - "Description": "S3 bucket for asset \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\"" + "Description": "S3 bucket for asset \"187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0\"" }, - "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F": { + "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3VersionKey7EE70F5C": { "Type": "String", - "Description": "S3 key for asset version \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\"" + "Description": "S3 key for asset version \"187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0\"" }, - "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68ArtifactHashD9A515C3": { + "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0ArtifactHash8F73A2B0": { "Type": "String", - "Description": "Artifact hash for asset \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\"" + "Description": "Artifact hash for asset \"187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0\"" }, - "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3Bucket1BE31DB0": { + "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eS3BucketC3F9EAA2": { "Type": "String", - "Description": "S3 bucket for asset \"983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4c\"" + "Description": "S3 bucket for asset \"4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282e\"" }, - "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3VersionKeyDC38E49C": { + "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eS3VersionKey030ACBFF": { "Type": "String", - "Description": "S3 key for asset version \"983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4c\"" + "Description": "S3 key for asset version \"4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282e\"" }, - "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cArtifactHashBA6352EA": { + "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eArtifactHashE8052809": { "Type": "String", - "Description": "Artifact hash for asset \"983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4c\"" + "Description": "Artifact hash for asset \"4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282e\"" }, "AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3Bucket9CD8B20A": { "Type": "String", diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-data.expected.json b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-data.expected.json new file mode 100644 index 0000000000000..51b9a179e65c5 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-data.expected.json @@ -0,0 +1,544 @@ +{ + "Resources": { + "Bucket83908E77": { + "Type": "AWS::S3::Bucket", + "Properties": { + "Tags": [ + { + "Key": "aws-cdk:cr-owned:deploy/here/:588fbb1f", + "Value": "true" + } + ] + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "DeployMeAwsCliLayer5F9219E9": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": { + "S3Bucket": { + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3Bucket59E5CFEF" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3VersionKey7EE70F5C" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3VersionKey7EE70F5C" + } + ] + } + ] + } + ] + ] + } + }, + "Description": "/opt/awscli/aws" + } + }, + "DeployMeCustomResource4455EE35": { + "Type": "Custom::CDKBucketDeployment", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536", + "Arn" + ] + }, + "SourceBucketNames": [ + { + "Ref": "AssetParametersd09271be89b6cb0398f793b40c1531fd9b076aa92ba80b5e436914b1808fe18dS3BucketBC52CF96" + }, + { + "Ref": "AssetParameters0f14dedeaf4386031c978375cbda0f65d7b52b29452cabb8873eb8f0d0fa936bS3BucketE46D7C76" + }, + { + "Ref": "AssetParameters0d7be86c2a7d62be64fcbe2cbaa36c912a72d445022cc17c37af4f99f1b97a5aS3Bucket485B8F98" + } + ], + "SourceObjectKeys": [ + { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersd09271be89b6cb0398f793b40c1531fd9b076aa92ba80b5e436914b1808fe18dS3VersionKeyED6BBB32" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersd09271be89b6cb0398f793b40c1531fd9b076aa92ba80b5e436914b1808fe18dS3VersionKeyED6BBB32" + } + ] + } + ] + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0f14dedeaf4386031c978375cbda0f65d7b52b29452cabb8873eb8f0d0fa936bS3VersionKey9FBF1498" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0f14dedeaf4386031c978375cbda0f65d7b52b29452cabb8873eb8f0d0fa936bS3VersionKey9FBF1498" + } + ] + } + ] + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0d7be86c2a7d62be64fcbe2cbaa36c912a72d445022cc17c37af4f99f1b97a5aS3VersionKeyC8D63B0C" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0d7be86c2a7d62be64fcbe2cbaa36c912a72d445022cc17c37af4f99f1b97a5aS3VersionKeyC8D63B0C" + } + ] + } + ] + } + ] + ] + } + ], + "SourceMarkers": [ + {}, + { + "<>": { + "Ref": "Bucket83908E77" + } + }, + { + "<>": { + "Fn::GetAtt": [ + "Bucket83908E77", + "WebsiteURL" + ] + } + } + ], + "DestinationBucketName": { + "Ref": "Bucket83908E77" + }, + "DestinationBucketKeyPrefix": "deploy/here/", + "Prune": true + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParametersd09271be89b6cb0398f793b40c1531fd9b076aa92ba80b5e436914b1808fe18dS3BucketBC52CF96" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParametersd09271be89b6cb0398f793b40c1531fd9b076aa92ba80b5e436914b1808fe18dS3BucketBC52CF96" + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParameters0f14dedeaf4386031c978375cbda0f65d7b52b29452cabb8873eb8f0d0fa936bS3BucketE46D7C76" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParameters0f14dedeaf4386031c978375cbda0f65d7b52b29452cabb8873eb8f0d0fa936bS3BucketE46D7C76" + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParameters0d7be86c2a7d62be64fcbe2cbaa36c912a72d445022cc17c37af4f99f1b97a5aS3Bucket485B8F98" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParameters0d7be86c2a7d62be64fcbe2cbaa36c912a72d445022cc17c37af4f99f1b97a5aS3Bucket485B8F98" + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", + "Roles": [ + { + "Ref": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" + } + ] + } + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eS3BucketC3F9EAA2" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eS3VersionKey030ACBFF" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eS3VersionKey030ACBFF" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265", + "Arn" + ] + }, + "Handler": "index.handler", + "Layers": [ + { + "Ref": "DeployMeAwsCliLayer5F9219E9" + } + ], + "Runtime": "python3.7", + "Timeout": 900 + }, + "DependsOn": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" + ] + } + }, + "Parameters": { + "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3Bucket59E5CFEF": { + "Type": "String", + "Description": "S3 bucket for asset \"187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0\"" + }, + "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3VersionKey7EE70F5C": { + "Type": "String", + "Description": "S3 key for asset version \"187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0\"" + }, + "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0ArtifactHash8F73A2B0": { + "Type": "String", + "Description": "Artifact hash for asset \"187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0\"" + }, + "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eS3BucketC3F9EAA2": { + "Type": "String", + "Description": "S3 bucket for asset \"4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282e\"" + }, + "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eS3VersionKey030ACBFF": { + "Type": "String", + "Description": "S3 key for asset version \"4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282e\"" + }, + "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eArtifactHashE8052809": { + "Type": "String", + "Description": "Artifact hash for asset \"4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282e\"" + }, + "AssetParametersd09271be89b6cb0398f793b40c1531fd9b076aa92ba80b5e436914b1808fe18dS3BucketBC52CF96": { + "Type": "String", + "Description": "S3 bucket for asset \"d09271be89b6cb0398f793b40c1531fd9b076aa92ba80b5e436914b1808fe18d\"" + }, + "AssetParametersd09271be89b6cb0398f793b40c1531fd9b076aa92ba80b5e436914b1808fe18dS3VersionKeyED6BBB32": { + "Type": "String", + "Description": "S3 key for asset version \"d09271be89b6cb0398f793b40c1531fd9b076aa92ba80b5e436914b1808fe18d\"" + }, + "AssetParametersd09271be89b6cb0398f793b40c1531fd9b076aa92ba80b5e436914b1808fe18dArtifactHashF6480042": { + "Type": "String", + "Description": "Artifact hash for asset \"d09271be89b6cb0398f793b40c1531fd9b076aa92ba80b5e436914b1808fe18d\"" + }, + "AssetParameters0f14dedeaf4386031c978375cbda0f65d7b52b29452cabb8873eb8f0d0fa936bS3BucketE46D7C76": { + "Type": "String", + "Description": "S3 bucket for asset \"0f14dedeaf4386031c978375cbda0f65d7b52b29452cabb8873eb8f0d0fa936b\"" + }, + "AssetParameters0f14dedeaf4386031c978375cbda0f65d7b52b29452cabb8873eb8f0d0fa936bS3VersionKey9FBF1498": { + "Type": "String", + "Description": "S3 key for asset version \"0f14dedeaf4386031c978375cbda0f65d7b52b29452cabb8873eb8f0d0fa936b\"" + }, + "AssetParameters0f14dedeaf4386031c978375cbda0f65d7b52b29452cabb8873eb8f0d0fa936bArtifactHash1774126C": { + "Type": "String", + "Description": "Artifact hash for asset \"0f14dedeaf4386031c978375cbda0f65d7b52b29452cabb8873eb8f0d0fa936b\"" + }, + "AssetParameters0d7be86c2a7d62be64fcbe2cbaa36c912a72d445022cc17c37af4f99f1b97a5aS3Bucket485B8F98": { + "Type": "String", + "Description": "S3 bucket for asset \"0d7be86c2a7d62be64fcbe2cbaa36c912a72d445022cc17c37af4f99f1b97a5a\"" + }, + "AssetParameters0d7be86c2a7d62be64fcbe2cbaa36c912a72d445022cc17c37af4f99f1b97a5aS3VersionKeyC8D63B0C": { + "Type": "String", + "Description": "S3 key for asset version \"0d7be86c2a7d62be64fcbe2cbaa36c912a72d445022cc17c37af4f99f1b97a5a\"" + }, + "AssetParameters0d7be86c2a7d62be64fcbe2cbaa36c912a72d445022cc17c37af4f99f1b97a5aArtifactHashA3962596": { + "Type": "String", + "Description": "Artifact hash for asset \"0d7be86c2a7d62be64fcbe2cbaa36c912a72d445022cc17c37af4f99f1b97a5a\"" + } + }, + "Outputs": { + "BucketName": { + "Value": { + "Ref": "Bucket83908E77" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-data.ts b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-data.ts new file mode 100644 index 0000000000000..45db0762527fd --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-data.ts @@ -0,0 +1,21 @@ +import { Bucket } from '@aws-cdk/aws-s3'; +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { BucketDeployment, Source } from '../lib'; + +const app = new App(); +const stack = new Stack(app, 'TestBucketDeploymentContent'); +const bucket = new Bucket(stack, 'Bucket'); + +const file1 = Source.data('file1.txt', 'boom'); +const file2 = Source.data('path/to/file2.txt', `bam! ${bucket.bucketName}`); +const file3 = Source.jsonData('my/config.json', { website_url: bucket.bucketWebsiteUrl }); + +new BucketDeployment(stack, 'DeployMe', { + destinationBucket: bucket, + sources: [file1, file2, file3], + destinationKeyPrefix: 'deploy/here/', +}); + +new CfnOutput(stack, 'BucketName', { value: bucket.bucketName }); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json index 26209e97d225e..bf4c180c9b559 100644 --- a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json @@ -129,7 +129,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3Bucket2C6C817C" + "Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3Bucket09A62232" }, "S3Key": { "Fn::Join": [ @@ -142,7 +142,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6" + "Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE" } ] } @@ -155,7 +155,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6" + "Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE" } ] } @@ -197,7 +197,7 @@ "Properties": { "Content": { "S3Bucket": { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3Bucket59E5CFEF" }, "S3Key": { "Fn::Join": [ @@ -210,7 +210,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3VersionKey7EE70F5C" } ] } @@ -223,7 +223,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3VersionKey7EE70F5C" } ] } @@ -378,6 +378,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -411,6 +415,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -444,6 +452,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -486,7 +498,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3Bucket1BE31DB0" + "Ref": "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eS3BucketC3F9EAA2" }, "S3Key": { "Fn::Join": [ @@ -499,7 +511,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3VersionKeyDC38E49C" + "Ref": "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eS3VersionKey030ACBFF" } ] } @@ -512,7 +524,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3VersionKeyDC38E49C" + "Ref": "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eS3VersionKey030ACBFF" } ] } @@ -1061,7 +1073,7 @@ "Properties": { "Content": { "S3Bucket": { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3Bucket59E5CFEF" }, "S3Key": { "Fn::Join": [ @@ -1074,7 +1086,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3VersionKey7EE70F5C" } ] } @@ -1087,7 +1099,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3VersionKey7EE70F5C" } ] } @@ -1528,6 +1540,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -1596,7 +1612,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3Bucket1BE31DB0" + "Ref": "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eS3BucketC3F9EAA2" }, "S3Key": { "Fn::Join": [ @@ -1609,7 +1625,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3VersionKeyDC38E49C" + "Ref": "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eS3VersionKey030ACBFF" } ] } @@ -1622,7 +1638,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3VersionKeyDC38E49C" + "Ref": "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eS3VersionKey030ACBFF" } ] } @@ -1802,7 +1818,7 @@ "Properties": { "Content": { "S3Bucket": { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3Bucket59E5CFEF" }, "S3Key": { "Fn::Join": [ @@ -1815,7 +1831,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3VersionKey7EE70F5C" } ] } @@ -1828,7 +1844,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3VersionKey7EE70F5C" } ] } @@ -1992,7 +2008,7 @@ "Properties": { "Content": { "S3Bucket": { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3Bucket59E5CFEF" }, "S3Key": { "Fn::Join": [ @@ -2005,7 +2021,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3VersionKey7EE70F5C" } ] } @@ -2018,7 +2034,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3VersionKey7EE70F5C" } ] } @@ -2103,7 +2119,7 @@ "Properties": { "Content": { "S3Bucket": { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3Bucket59E5CFEF" }, "S3Key": { "Fn::Join": [ @@ -2116,7 +2132,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3VersionKey7EE70F5C" } ] } @@ -2129,7 +2145,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3VersionKey7EE70F5C" } ] } @@ -2205,7 +2221,7 @@ "Properties": { "Content": { "S3Bucket": { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3Bucket59E5CFEF" }, "S3Key": { "Fn::Join": [ @@ -2218,7 +2234,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3VersionKey7EE70F5C" } ] } @@ -2231,7 +2247,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + "Ref": "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3VersionKey7EE70F5C" } ] } @@ -2307,41 +2323,41 @@ } }, "Parameters": { - "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3Bucket2C6C817C": { + "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3Bucket09A62232": { "Type": "String", - "Description": "S3 bucket for asset \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\"" + "Description": "S3 bucket for asset \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\"" }, - "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6": { + "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE": { "Type": "String", - "Description": "S3 key for asset version \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\"" + "Description": "S3 key for asset version \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\"" }, - "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709ArtifactHash17D48178": { + "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824ArtifactHash76F8FCF2": { "Type": "String", - "Description": "Artifact hash for asset \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\"" + "Description": "Artifact hash for asset \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\"" }, - "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7": { + "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3Bucket59E5CFEF": { "Type": "String", - "Description": "S3 bucket for asset \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\"" + "Description": "S3 bucket for asset \"187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0\"" }, - "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F": { + "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0S3VersionKey7EE70F5C": { "Type": "String", - "Description": "S3 key for asset version \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\"" + "Description": "S3 key for asset version \"187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0\"" }, - "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68ArtifactHashD9A515C3": { + "AssetParameters187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0ArtifactHash8F73A2B0": { "Type": "String", - "Description": "Artifact hash for asset \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\"" + "Description": "Artifact hash for asset \"187e7a21dd5d55d36f1f45007ff6bbc5713cb0866ca86224c0f1f86b3d1e76a0\"" }, - "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3Bucket1BE31DB0": { + "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eS3BucketC3F9EAA2": { "Type": "String", - "Description": "S3 bucket for asset \"983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4c\"" + "Description": "S3 bucket for asset \"4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282e\"" }, - "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3VersionKeyDC38E49C": { + "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eS3VersionKey030ACBFF": { "Type": "String", - "Description": "S3 key for asset version \"983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4c\"" + "Description": "S3 key for asset version \"4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282e\"" }, - "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cArtifactHashBA6352EA": { + "AssetParameters4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282eArtifactHashE8052809": { "Type": "String", - "Description": "Artifact hash for asset \"983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4c\"" + "Description": "Artifact hash for asset \"4e09e63403b235ffda9db09367996f2d4c9fe1f7aa19b402908d8221614a282e\"" }, "AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3Bucket9CD8B20A": { "Type": "String", diff --git a/packages/@aws-cdk/aws-s3-deployment/test/lambda/.gitignore b/packages/@aws-cdk/aws-s3-deployment/test/lambda/.gitignore new file mode 100644 index 0000000000000..3a5f4f826d408 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/lambda/.gitignore @@ -0,0 +1,4 @@ +# symlinked by debug.sh +index.py +__pycache__ +aws.out \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/test/lambda/Dockerfile.debug b/packages/@aws-cdk/aws-s3-deployment/test/lambda/Dockerfile.debug new file mode 100644 index 0000000000000..791343ea39cf8 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/lambda/Dockerfile.debug @@ -0,0 +1,6 @@ +FROM public.ecr.aws/lambda/python:latest + +# install boto3, which is available on Lambda +RUN pip3 install boto3 + +ENTRYPOINT [ "/bin/bash" ] \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/test/lambda/debug.sh b/packages/@aws-cdk/aws-s3-deployment/test/lambda/debug.sh new file mode 100755 index 0000000000000..459116e55ab84 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/lambda/debug.sh @@ -0,0 +1,10 @@ +#!/bin/sh +# starts a debugging container for the python lambda function and tests + +tag="s3-deployment-test-environment" +docker build -f Dockerfile.debug -t $tag . + +echo "To iterate, run python3 ./test.py inside the container (source code is mapped into the container)." + +ln -fs /opt/lambda/index.py index.py +docker run -v $PWD:/opt/awscli -v $PWD/../../lib/lambda:/opt/lambda --workdir /opt/awscli -it $tag \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.py b/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.py index 30b59c26374f3..b999aa5a4e797 100644 --- a/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.py +++ b/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.py @@ -7,6 +7,7 @@ import traceback import logging import botocore +import tempfile from botocore.vendored import requests from botocore.exceptions import ClientError from unittest.mock import MagicMock @@ -584,7 +585,45 @@ def mock_make_api_call(self, operation_name, kwarg): self.assertAwsCommands( ["s3", "rm", "s3:///", "--recursive"] ) + + def test_replace_markers(self): + index.extract_and_replace_markers("test.zip", "/tmp/out", { + "_marker2_": "boom-marker2-replaced", + "_marker1_": "<>", + }) + + # assert that markers were replaced in the output + with open("/tmp/out/subfolder/boom.txt", "r") as file: + self.assertEqual(file.read().rstrip(), "Another <> file with boom-marker2-replaced hey!\nLine 2 with <> again :-)") + + with open("/tmp/out/test.txt") as file: + self.assertEqual(file.read().rstrip(), "Hello, <> world") + def test_marker_substitution(self): + outdir = tempfile.mkdtemp() + + invoke_handler("Create", { + "SourceBucketNames": ["", ""], + "SourceObjectKeys": ["", ""], + "DestinationBucketName": "", + "Prune": "false", + "SourceMarkers": [ + { "_marker1_": "value1-source1", "_marker2_": "value2-source1" }, + { "_marker1_": "value1-source2" }, + ], + }, outdir=outdir) + + # outdir is expected to have a single directory that contains the workdir + files = os.listdir(outdir) + self.assertEqual(len(files), 1) # defensive + + workdir = os.path.join(outdir, files[0], "contents") + + with open(os.path.join(workdir, "test.txt"), "r") as file: + self.assertEqual(file.read().rstrip(), "Hello, value1-source2 world") + + with open(os.path.join(workdir, "subfolder", "boom.txt"), "r") as file: + self.assertEqual(file.read().rstrip(), "Another value1-source2 file with _marker2_ hey!\nLine 2 with value1-source2 again :-)") # asserts that a given list of "aws xxx" commands have been invoked (in order) @@ -609,7 +648,7 @@ def read_aws_out(): # requestType: CloudFormation request type ("Create", "Update", "Delete") # resourceProps: map to pass to "ResourceProperties" # expected_status: "SUCCESS" or "FAILED" -def invoke_handler(requestType, resourceProps, old_resource_props=None, physical_id=None, expected_status='SUCCESS'): +def invoke_handler(requestType, resourceProps, old_resource_props=None, physical_id=None, expected_status='SUCCESS', outdir=None): response_url = 'http://' event={ @@ -636,6 +675,11 @@ class ResponseMock: context = ContextMock() index.urlopen = MagicMock(return_value=ResponseMock()) + # control the output directory and skip cleanup so we can examine the output + if outdir: + os.environ[index.ENV_KEY_MOUNT_PATH] = outdir + os.environ[index.ENV_KEY_SKIP_CLEANUP] = "1" + #-------------------- # invoke the handler #-------------------- diff --git a/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.sh b/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.sh index a094c8ae16cfa..8f0c0d2b473b7 100755 --- a/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.sh +++ b/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.sh @@ -6,6 +6,9 @@ set -e scriptdir=$(cd $(dirname $0) && pwd) +rm -f ${scriptdir}/index.py +rm -fr ${scriptdir}/__pycache__ + # prepare staging directory staging=$(mktemp -d) mkdir -p ${staging} diff --git a/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.zip b/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.zip index 56829f65a2681..784486f7d04cd 100644 Binary files a/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.zip and b/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.zip differ diff --git a/packages/@aws-cdk/aws-s3-notifications/README.md b/packages/@aws-cdk/aws-s3-notifications/README.md index f054708f437fb..75fba9152e2b8 100644 --- a/packages/@aws-cdk/aws-s3-notifications/README.md +++ b/packages/@aws-cdk/aws-s3-notifications/README.md @@ -18,24 +18,36 @@ The following example shows how to send a notification to an SNS topic when an object is created in an S3 bucket: ```ts -import * as s3n from '@aws-cdk/aws-s3-notifications'; +import * as sns from '@aws-cdk/aws-sns'; -const bucket = new s3.Bucket(stack, 'Bucket'); -const topic = new sns.Topic(stack, 'Topic'); +const bucket = new s3.Bucket(this, 'Bucket'); +const topic = new sns.Topic(this, 'Topic'); bucket.addEventNotification(s3.EventType.OBJECT_CREATED_PUT, new s3n.SnsDestination(topic)); ``` +The following example shows how to send a notification to an SQS queue +when an object is created in an S3 bucket: + +```ts +import * as sqs from '@aws-cdk/aws-sqs'; + +const bucket = new s3.Bucket(this, 'Bucket'); +const queue = new sqs.Queue(this, 'Queue'); + +bucket.addEventNotification(s3.EventType.OBJECT_CREATED_PUT, new s3n.SqsDestination(queue)); +``` + The following example shows how to send a notification to a Lambda function when an object is created in an S3 bucket: ```ts -import * as s3n from '@aws-cdk/aws-s3-notifications'; +import * as lambda from '@aws-cdk/aws-lambda'; -const bucket = new s3.Bucket(stack, 'Bucket'); -const fn = new Function(this, 'MyFunction', { - runtime: Runtime.NODEJS_12_X, +const bucket = new s3.Bucket(this, 'Bucket'); +const fn = new lambda.Function(this, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', - code: Code.fromAsset(path.join(__dirname, 'lambda-handler')), + code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), }); bucket.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.LambdaDestination(fn)); diff --git a/packages/@aws-cdk/aws-s3-notifications/lib/sqs.ts b/packages/@aws-cdk/aws-s3-notifications/lib/sqs.ts index 330a941a9ae86..5562a07c5182a 100644 --- a/packages/@aws-cdk/aws-s3-notifications/lib/sqs.ts +++ b/packages/@aws-cdk/aws-s3-notifications/lib/sqs.ts @@ -1,6 +1,10 @@ import * as iam from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; import * as sqs from '@aws-cdk/aws-sqs'; +import { Annotations } from '@aws-cdk/core'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order import { Construct } from '@aws-cdk/core'; /** @@ -24,11 +28,15 @@ export class SqsDestination implements s3.IBucketNotificationDestination { // if this queue is encrypted, we need to allow S3 to read messages since that's how // it verifies that the notification destination configuration is valid. if (this.queue.encryptionMasterKey) { - this.queue.encryptionMasterKey.addToResourcePolicy(new iam.PolicyStatement({ + const statement = new iam.PolicyStatement({ principals: [new iam.ServicePrincipal('s3.amazonaws.com')], actions: ['kms:GenerateDataKey*', 'kms:Decrypt'], resources: ['*'], - }), /* allowNoOp */ false); + }); + const addResult = this.queue.encryptionMasterKey.addToResourcePolicy(statement, /* allowNoOp */ true); + if (!addResult.statementAdded) { + Annotations.of(this.queue.encryptionMasterKey).addWarning(`Can not change key policy of imported kms key. Ensure that your key policy contains the following permissions: \n${JSON.stringify(statement.toJSON(), null, 2)}`); + } } return { diff --git a/packages/@aws-cdk/aws-s3-notifications/package.json b/packages/@aws-cdk/aws-s3-notifications/package.json index 7c04d633bee5d..32738cd1b0d93 100644 --- a/packages/@aws-cdk/aws-s3-notifications/package.json +++ b/packages/@aws-cdk/aws-s3-notifications/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -64,15 +71,16 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", @@ -83,6 +91,7 @@ "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", diff --git a/packages/@aws-cdk/aws-s3-notifications/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-s3-notifications/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..36e2218e03d06 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-notifications/rosetta/default.ts-fixture @@ -0,0 +1,14 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as s3n from '@aws-cdk/aws-s3-notifications'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as path from 'path'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-s3-notifications/test/lambda/lambda.test.ts b/packages/@aws-cdk/aws-s3-notifications/test/lambda/lambda.test.ts index 4d80618ad8ff3..d6e563a4438ca 100644 --- a/packages/@aws-cdk/aws-s3-notifications/test/lambda/lambda.test.ts +++ b/packages/@aws-cdk/aws-s3-notifications/test/lambda/lambda.test.ts @@ -1,6 +1,4 @@ -// import { SynthUtils } from '@aws-cdk/assert-internal'; -import { ResourcePart } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; import { Stack, App } from '@aws-cdk/core'; @@ -29,20 +27,20 @@ test('add notifications to multiple functions', () => { bucket.addEventNotification(s3.EventType.OBJECT_CREATED, lambdaDestination2, { prefix: 'v2/' }); // expecting notification configuration to have both events - expect(stack).toHaveResourceLike('Custom::S3BucketNotifications', { - NotificationConfiguration: { + Template.fromStack(stack).hasResourceProperties('Custom::S3BucketNotifications', { + NotificationConfiguration: Match.objectLike({ LambdaFunctionConfigurations: [ - { Filter: { Key: { FilterRules: [{ Name: 'prefix', Value: 'v1/' }] } } }, - { Filter: { Key: { FilterRules: [{ Name: 'prefix', Value: 'v2/' }] } } }, + Match.objectLike({ Filter: { Key: { FilterRules: [{ Name: 'prefix', Value: 'v1/' }] } } }), + Match.objectLike({ Filter: { Key: { FilterRules: [{ Name: 'prefix', Value: 'v2/' }] } } }), ], - }, + }), }); // expecting one permission for each function - expect(stack).toCountResources('AWS::Lambda::Permission', 2); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 2); // make sure each permission points to the correct function - expect(stack).toHaveResourceLike('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { FunctionName: { 'Fn::GetAtt': [ 'MyFunction12A744C2E', @@ -56,7 +54,7 @@ test('add notifications to multiple functions', () => { ], }, }); - expect(stack).toHaveResourceLike('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { FunctionName: { 'Fn::GetAtt': [ 'MyFunction2F2A964CA', @@ -89,7 +87,7 @@ test('lambda in a different stack as notification target', () => { bucket.addObjectCreatedNotification(new s3n.LambdaDestination(lambdaFunction)); // permission should be in the bucket stack - expect(bucketStack).toHaveResourceLike('AWS::Lambda::Permission', { + Template.fromStack(bucketStack).hasResourceProperties('AWS::Lambda::Permission', { FunctionName: { 'Fn::ImportValue': 'stack1:ExportsOutputFnGetAttlambdaFunction940E68ADArn6B2878AF', }, @@ -115,7 +113,7 @@ test('imported lambda in a different account as notification target', () => { bucket.addObjectCreatedNotification(new s3n.LambdaDestination(lambdaFunction)); // no permissions created - expect(stack).not.toHaveResourceLike('AWS::Lambda::Permission'); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 0); }); test('lambda as notification target', () => { @@ -132,7 +130,7 @@ test('lambda as notification target', () => { bucketA.addObjectCreatedNotification(new s3n.LambdaDestination(fn), { suffix: '.png' }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', FunctionName: { 'Fn::GetAtt': ['MyFunction3BAA72D1', 'Arn'] }, Principal: 's3.amazonaws.com', @@ -140,7 +138,7 @@ test('lambda as notification target', () => { SourceArn: { 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, }); - expect(stack).toHaveResource('Custom::S3BucketNotifications', { + Template.fromStack(stack).hasResourceProperties('Custom::S3BucketNotifications', { NotificationConfiguration: { LambdaFunctionConfigurations: [ { @@ -167,7 +165,7 @@ test('lambda as notification target specified by function arn', () => { bucketA.addObjectCreatedNotification(new s3n.LambdaDestination(fn), { suffix: '.png' }); // THEN - expect(stack).toHaveResource('Custom::S3BucketNotifications', { + Template.fromStack(stack).hasResourceProperties('Custom::S3BucketNotifications', { NotificationConfiguration: { LambdaFunctionConfigurations: [ { @@ -199,9 +197,9 @@ test('permissions are added as a dependency to the notifications resource when u bucket.addEventNotification(s3.EventType.OBJECT_CREATED, lambdaDestination, { prefix: 'v1/' }); - expect(stack).toHaveResource('Custom::S3BucketNotifications', { + Template.fromStack(stack).hasResource('Custom::S3BucketNotifications', { DependsOn: ['MyBucketAllowBucketNotificationsToSingletonLambdauuid28C96883'], - }, ResourcePart.CompleteDefinition); + }); }); test('add multiple event notifications using a singleton function', () => { @@ -220,13 +218,13 @@ test('add multiple event notifications using a singleton function', () => { bucket.addEventNotification(s3.EventType.OBJECT_CREATED, lambdaDestination, { prefix: 'v1/' }); bucket.addEventNotification(s3.EventType.OBJECT_CREATED, lambdaDestination, { prefix: 'v2/' }); - expect(stack).toHaveResourceLike('Custom::S3BucketNotifications', { - NotificationConfiguration: { + Template.fromStack(stack).hasResourceProperties('Custom::S3BucketNotifications', { + NotificationConfiguration: Match.objectLike({ LambdaFunctionConfigurations: [ - { Filter: { Key: { FilterRules: [{ Name: 'prefix', Value: 'v1/' }] } } }, - { Filter: { Key: { FilterRules: [{ Name: 'prefix', Value: 'v2/' }] } } }, + Match.objectLike({ Filter: { Key: { FilterRules: [{ Name: 'prefix', Value: 'v1/' }] } } }), + Match.objectLike({ Filter: { Key: { FilterRules: [{ Name: 'prefix', Value: 'v2/' }] } } }), ], - }, + }), }); }); diff --git a/packages/@aws-cdk/aws-s3-notifications/test/notifications.test.ts b/packages/@aws-cdk/aws-s3-notifications/test/notifications.test.ts index 2bfe968f99279..ee4cbc9c5c5d0 100644 --- a/packages/@aws-cdk/aws-s3-notifications/test/notifications.test.ts +++ b/packages/@aws-cdk/aws-s3-notifications/test/notifications.test.ts @@ -1,5 +1,4 @@ -import { SynthUtils } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as s3 from '@aws-cdk/aws-s3'; import * as sns from '@aws-cdk/aws-sns'; import * as cdk from '@aws-cdk/core'; @@ -13,7 +12,7 @@ test('bucket without notifications', () => { new s3.Bucket(stack, 'MyBucket'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -33,7 +32,7 @@ test('notifications can be added to imported buckets', () => { bucket.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.SnsDestination(topic)); - expect(stack).toHaveResource('Custom::S3BucketNotifications', { + Template.fromStack(stack).hasResourceProperties('Custom::S3BucketNotifications', { ServiceToken: { 'Fn::GetAtt': ['BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691', 'Arn'] }, BucketName: 'mybucket', Managed: false, @@ -61,9 +60,9 @@ test('when notification are added, a custom resource is provisioned + a lambda h bucket.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.SnsDestination(topic)); - expect(stack).toHaveResource('AWS::S3::Bucket'); - expect(stack).toHaveResource('AWS::Lambda::Function', { Description: 'AWS CloudFormation handler for "Custom::S3BucketNotifications" resources (@aws-cdk/aws-s3)' }); - expect(stack).toHaveResource('Custom::S3BucketNotifications'); + Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 1); + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Description: 'AWS CloudFormation handler for "Custom::S3BucketNotifications" resources (@aws-cdk/aws-s3)' }); + Template.fromStack(stack).resourceCountIs('Custom::S3BucketNotifications', 1); }); test('when notification are added, you can tag the lambda', () => { @@ -76,12 +75,12 @@ test('when notification are added, you can tag the lambda', () => { bucket.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.SnsDestination(topic)); - expect(stack).toHaveResource('AWS::S3::Bucket'); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 1); + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Tags: [{ Key: 'Lambda', Value: 'AreTagged' }], Description: 'AWS CloudFormation handler for "Custom::S3BucketNotifications" resources (@aws-cdk/aws-s3)', }); - expect(stack).toHaveResource('Custom::S3BucketNotifications'); + Template.fromStack(stack).resourceCountIs('Custom::S3BucketNotifications', 1); }); test('bucketNotificationTarget is not called during synthesis', () => { @@ -95,7 +94,7 @@ test('bucketNotificationTarget is not called during synthesis', () => { bucket.addObjectCreatedNotification(new s3n.SnsDestination(topic)); - expect(stack).toHaveResourceLike('AWS::SNS::TopicPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::SNS::TopicPolicy', { 'Topics': [ { 'Ref': 'TopicBFC7AF6E', @@ -122,6 +121,7 @@ test('bucketNotificationTarget is not called during synthesis', () => { 'Resource': { 'Ref': 'TopicBFC7AF6E', }, + 'Sid': '0', }, ], 'Version': '2012-10-17', @@ -159,7 +159,7 @@ test('subscription types', () => { bucket.addEventNotification(s3.EventType.OBJECT_CREATED, lambdaTarget); bucket.addObjectRemovedNotification(topicTarget, { prefix: 'prefix' }); - expect(stack).toHaveResource('Custom::S3BucketNotifications', { + Template.fromStack(stack).hasResourceProperties('Custom::S3BucketNotifications', { 'ServiceToken': { 'Fn::GetAtt': [ 'BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691', @@ -227,7 +227,7 @@ test('multiple subscriptions of the same type', () => { }), }); - expect(stack).toHaveResource('Custom::S3BucketNotifications', { + Template.fromStack(stack).hasResourceProperties('Custom::S3BucketNotifications', { 'ServiceToken': { 'Fn::GetAtt': [ 'BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691', @@ -268,7 +268,7 @@ test('prefix/suffix filters', () => { bucket.addEventNotification(s3.EventType.OBJECT_REMOVED_DELETE, { bind: _ => bucketNotificationTarget }, { prefix: 'images/', suffix: '.jpg' }); - expect(stack).toHaveResource('Custom::S3BucketNotifications', { + Template.fromStack(stack).hasResourceProperties('Custom::S3BucketNotifications', { 'ServiceToken': { 'Fn::GetAtt': [ 'BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691', @@ -320,7 +320,7 @@ test('a notification destination can specify a set of dependencies that must be bucket.addObjectCreatedNotification(dest); - expect(SynthUtils.synthesize(stack).template.Resources.BucketNotifications8F2E257D).toEqual({ + expect(Template.fromStack(stack).findResources('Custom::S3BucketNotifications').BucketNotifications8F2E257D).toEqual({ Type: 'Custom::S3BucketNotifications', Properties: { ServiceToken: { 'Fn::GetAtt': ['BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691', 'Arn'] }, @@ -344,7 +344,7 @@ describe('CloudWatch Events', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.s3', @@ -387,7 +387,7 @@ describe('CloudWatch Events', () => { paths: ['my/path.zip'], }); - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.s3', @@ -429,7 +429,7 @@ describe('CloudWatch Events', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.s3', @@ -457,7 +457,7 @@ describe('CloudWatch Events', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.s3', @@ -485,7 +485,7 @@ describe('CloudWatch Events', () => { paths: ['my/path.zip'], }); - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.s3', diff --git a/packages/@aws-cdk/aws-s3-notifications/test/queue.test.ts b/packages/@aws-cdk/aws-s3-notifications/test/queue.test.ts index 22c2edac592c8..44404de1b1d79 100644 --- a/packages/@aws-cdk/aws-s3-notifications/test/queue.test.ts +++ b/packages/@aws-cdk/aws-s3-notifications/test/queue.test.ts @@ -1,5 +1,5 @@ -import { arrayWith, SynthUtils } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template, Annotations } from '@aws-cdk/assertions'; +import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; import * as sqs from '@aws-cdk/aws-sqs'; import { Stack } from '@aws-cdk/core'; @@ -13,7 +13,7 @@ test('queues can be used as destinations', () => { bucket.addObjectRemovedNotification(new notif.SqsDestination(queue)); - expect(stack).toHaveResource('AWS::SQS::QueuePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::QueuePolicy', { PolicyDocument: { Statement: [ { @@ -44,7 +44,7 @@ test('queues can be used as destinations', () => { ], }); - expect(stack).toHaveResource('Custom::S3BucketNotifications', { + Template.fromStack(stack).hasResourceProperties('Custom::S3BucketNotifications', { BucketName: { Ref: 'Bucket83908E77', }, @@ -67,7 +67,11 @@ test('queues can be used as destinations', () => { // make sure the queue policy is added as a dependency to the bucket // notifications resource so it will be created first. - expect(SynthUtils.synthesize(stack).template.Resources.BucketNotifications8F2E257D.DependsOn).toEqual(['QueuePolicy25439813', 'Queue4A7E3555']); + + const resources = Template.fromStack(stack).findResources('Custom::S3BucketNotifications'); + + expect(resources.BucketNotifications8F2E257D.DependsOn) + .toEqual(['QueuePolicy25439813', 'Queue4A7E3555']); }); test('if the queue is encrypted with a custom kms key, the key resource policy is updated to allow s3 to read messages', () => { @@ -77,9 +81,9 @@ test('if the queue is encrypted with a custom kms key, the key resource policy i bucket.addObjectCreatedNotification(new notif.SqsDestination(queue)); - expect(stack).toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Action: [ 'kms:GenerateDataKey*', 'kms:Decrypt', @@ -89,7 +93,31 @@ test('if the queue is encrypted with a custom kms key, the key resource policy i Service: 's3.amazonaws.com', }, Resource: '*', - }), + }]), }, }); }); + +test('if the queue is encrypted with a imported kms key, printout warning', () => { + const stack = new Stack(); + const bucket = new s3.Bucket(stack, 'Bucket'); + const key = kms.Key.fromKeyArn(stack, 'ImportedKey', 'arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab'); + const queue = new sqs.Queue(stack, 'Queue', { + encryption: sqs.QueueEncryption.KMS, + encryptionMasterKey: key, + }); + + bucket.addObjectCreatedNotification(new notif.SqsDestination(queue)); + + Annotations.fromStack(stack).hasWarning('/Default/ImportedKey', `Can not change key policy of imported kms key. Ensure that your key policy contains the following permissions: \n${JSON.stringify({ + Action: [ + 'kms:GenerateDataKey*', + 'kms:Decrypt', + ], + Effect: 'Allow', + Principal: { + Service: 's3.amazonaws.com', + }, + Resource: '*', + }, null, 2)}`); +}); diff --git a/packages/@aws-cdk/aws-s3-notifications/test/sns.test.ts b/packages/@aws-cdk/aws-s3-notifications/test/sns.test.ts index 2c91becaae188..8a1d607113059 100644 --- a/packages/@aws-cdk/aws-s3-notifications/test/sns.test.ts +++ b/packages/@aws-cdk/aws-s3-notifications/test/sns.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as s3 from '@aws-cdk/aws-s3'; import * as sns from '@aws-cdk/aws-sns'; import * as cdk from '@aws-cdk/core'; @@ -16,7 +16,7 @@ test('asBucketNotificationDestination adds bucket permissions only once for each // another bucket will be added to the topic policy new notif.SnsDestination(topic).bind(bucket2, bucket2); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { Bucket83908E77: { Type: 'AWS::S3::Bucket', diff --git a/packages/@aws-cdk/aws-s3/README.md b/packages/@aws-cdk/aws-s3/README.md index 4f2592b9c633a..47138a3d30ec6 100644 --- a/packages/@aws-cdk/aws-s3/README.md +++ b/packages/@aws-cdk/aws-s3/README.md @@ -249,6 +249,33 @@ const bucket = s3.Bucket.fromBucketAttributes(this, 'ImportedBucket', { bucket.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.SnsDestination(topic)); ``` +When you add an event notification to a bucket, a custom resource is created to +manage the notifications. By default, a new role is created for the Lambda +function that implements this feature. If you want to use your own role instead, +you should provide it in the `Bucket` constructor: + +```ts +declare const myRole: iam.IRole; +const bucket = new s3.Bucket(this, 'MyBucket', { + notificationsHandlerRole: myRole, +}); +``` + +Whatever role you provide, the CDK will try to modify it by adding the +permissions from `AWSLambdaBasicExecutionRole` (an AWS managed policy) as well +as the permissions `s3:PutBucketNotification` and `s3:GetBucketNotification`. +If you’re passing an imported role, and you don’t want this to happen, configure +it to be immutable: + +```ts +const importedRole = iam.Role.fromRoleArn(this, 'role', 'arn:aws:iam::123456789012:role/RoleName', { + mutable: false, +}); +``` + +> If you provide an imported immutable role, make sure that it has at least all +> the permissions mentioned above. Otherwise, the deployment will fail! + [S3 Bucket Notifications]: https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index bb00aac5adde3..42d6a84bdc18b 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -356,9 +356,7 @@ export interface IBucket extends IResource { } /** - * A reference to a bucket. The easiest way to instantiate is to call - * `bucket.export()`. Then, the consumer can use `Bucket.import(this, ref)` and - * get a `Bucket`. + * A reference to a bucket outside this stack */ export interface BucketAttributes { /** @@ -429,6 +427,13 @@ export interface BucketAttributes { * @default - it's assumed the bucket is in the same region as the scope it's being imported into */ readonly region?: string; + + /** + * The role to be used by the notifications handler + * + * @default - a new role will be created. + */ + readonly notificationsHandlerRole?: iam.IRole; } /** @@ -486,14 +491,12 @@ export abstract class BucketBase extends Resource implements IBucket { */ protected abstract disallowPublicAccess?: boolean; - private readonly notifications: BucketNotifications; + private notifications?: BucketNotifications; + + protected notificationsHandlerRole?: iam.IRole; constructor(scope: Construct, id: string, props: ResourceProps = {}) { super(scope, id, props); - - // defines a BucketNotifications construct. Notice that an actual resource will only - // be added if there are notifications added, so we don't need to condition this. - this.notifications = new BucketNotifications(this, 'Notifications', { bucket: this }); } /** @@ -838,7 +841,17 @@ export abstract class BucketBase extends Resource implements IBucket { * https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html */ public addEventNotification(event: EventType, dest: IBucketNotificationDestination, ...filters: NotificationKeyFilter[]) { - this.notifications.addNotification(event, dest, ...filters); + this.withNotifications(notifications => notifications.addNotification(event, dest, ...filters)); + } + + private withNotifications(cb: (notifications: BucketNotifications) => void) { + if (!this.notifications) { + this.notifications = new BucketNotifications(this, 'Notifications', { + bucket: this, + handlerRole: this.notificationsHandlerRole, + }); + } + cb(this.notifications); } /** @@ -1089,7 +1102,7 @@ export enum InventoryFormat { */ PARQUET = 'Parquet', /** - * Generate the inventory list as Parquet. + * Generate the inventory list as ORC. */ ORC = 'ORC', } @@ -1461,6 +1474,13 @@ export interface BucketProps { */ readonly transferAcceleration?: boolean; + /** + * The role to be used by the notifications handler + * + * @default - a new role will be created. + */ + readonly notificationsHandlerRole?: iam.IRole; + /** * Inteligent Tiering Configurations * @@ -1544,6 +1564,7 @@ export class Bucket extends BucketBase { public policy?: BucketPolicy = undefined; protected autoCreatePolicy = false; protected disallowPublicAccess = false; + protected notificationsHandlerRole = attrs.notificationsHandlerRole; /** * Exports this bucket from the stack. @@ -1631,6 +1652,8 @@ export class Bucket extends BucketBase { physicalName: props.bucketName, }); + this.notificationsHandlerRole = props.notificationsHandlerRole; + const { bucketEncryption, encryptionKey } = this.parseEncryption(props); Bucket.validateBucketName(this.physicalName); @@ -1869,6 +1892,7 @@ export class Bucket extends BucketBase { noncurrentVersionTransitions: mapOrUndefined(rule.noncurrentVersionTransitions, t => ({ storageClass: t.storageClass.value, transitionInDays: t.transitionAfter.toDays(), + newerNoncurrentVersions: t.noncurrentVersionsToRetain, })), prefix: rule.prefix, status: enabled ? 'Enabled' : 'Disabled', diff --git a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts index c093487a8f105..76edb141a3cd0 100644 --- a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts +++ b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts @@ -7,6 +7,10 @@ import * as cdk from '@aws-cdk/core'; // eslint-disable-next-line no-duplicate-imports, import/order import { Construct } from '@aws-cdk/core'; +export class NotificationsResourceHandlerProps { + role?: iam.IRole; +} + /** * A Lambda-based custom resource handler that provisions S3 bucket * notifications for a bucket. @@ -31,14 +35,14 @@ export class NotificationsResourceHandler extends Construct { * * @returns The ARN of the custom resource lambda function. */ - public static singleton(context: Construct) { + public static singleton(context: Construct, props: NotificationsResourceHandlerProps = {}) { const root = cdk.Stack.of(context); // well-known logical id to ensure stack singletonity const logicalId = 'BucketNotificationsHandler050a0587b7544547bf325f094a3db834'; let lambda = root.node.tryFindChild(logicalId) as NotificationsResourceHandler; if (!lambda) { - lambda = new NotificationsResourceHandler(root, logicalId); + lambda = new NotificationsResourceHandler(root, logicalId, props); } return lambda; @@ -53,19 +57,19 @@ export class NotificationsResourceHandler extends Construct { /** * The role of the handler's lambda function. */ - public readonly role: iam.Role; + public readonly role: iam.IRole; - constructor(scope: Construct, id: string) { + constructor(scope: Construct, id: string, props: NotificationsResourceHandlerProps = {}) { super(scope, id); - this.role = new iam.Role(this, 'Role', { + this.role = props.role ?? new iam.Role(this, 'Role', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), - managedPolicies: [ - iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'), - ], }); - this.role.addToPolicy(new iam.PolicyStatement({ + this.role.addManagedPolicy( + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'), + ); + this.role.addToPrincipalPolicy(new iam.PolicyStatement({ actions: ['s3:PutBucketNotification'], resources: ['*'], })); @@ -80,11 +84,17 @@ export class NotificationsResourceHandler extends Construct { return properties; } } + + const handlerSource = fs.readFileSync(path.join(__dirname, 'lambda/index.py'), 'utf8'); + if (handlerSource.length > 4096) { + throw new Error(`Source of Notifications Resource Handler is too large (${handlerSource.length} > 4096)`); + } + const resource = new InLineLambda(this, 'Resource', { type: resourceType, properties: { Description: 'AWS CloudFormation handler for "Custom::S3BucketNotifications" resources (@aws-cdk/aws-s3)', - Code: { ZipFile: fs.readFileSync(path.join(__dirname, 'lambda/index.py'), 'utf8') }, + Code: { ZipFile: handlerSource }, Handler: 'index.handler', Role: this.role.roleArn, Runtime: 'python3.7', @@ -95,4 +105,8 @@ export class NotificationsResourceHandler extends Construct { this.functionArn = resource.getAtt('Arn').toString(); } + + public addToRolePolicy(statement: iam.PolicyStatement) { + this.role.addToPrincipalPolicy(statement); + } } diff --git a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource.ts b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource.ts index d5190f1a6a913..6bc50ec5b6064 100644 --- a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource.ts +++ b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource.ts @@ -13,6 +13,11 @@ interface NotificationsProps { * The bucket to manage notifications for. */ bucket: IBucket; + + /** + * The role to be used by the lambda handler + */ + handlerRole?: iam.IRole; } /** @@ -36,10 +41,12 @@ export class BucketNotifications extends Construct { private readonly topicNotifications = new Array(); private resource?: cdk.CfnResource; private readonly bucket: IBucket; + private readonly handlerRole?: iam.IRole; constructor(scope: Construct, id: string, props: NotificationsProps) { super(scope, id); this.bucket = props.bucket; + this.handlerRole = props.handlerRole; } /** @@ -102,12 +109,14 @@ export class BucketNotifications extends Construct { */ private createResourceOnce() { if (!this.resource) { - const handler = NotificationsResourceHandler.singleton(this); + const handler = NotificationsResourceHandler.singleton(this, { + role: this.handlerRole, + }); const managed = this.bucket instanceof Bucket; if (!managed) { - handler.role.addToPolicy(new iam.PolicyStatement({ + handler.addToRolePolicy(new iam.PolicyStatement({ actions: ['s3:GetBucketNotification'], resources: ['*'], })); diff --git a/packages/@aws-cdk/aws-s3/lib/perms.ts b/packages/@aws-cdk/aws-s3/lib/perms.ts index f57b97153f27c..abfb00d852f6e 100644 --- a/packages/@aws-cdk/aws-s3/lib/perms.ts +++ b/packages/@aws-cdk/aws-s3/lib/perms.ts @@ -16,11 +16,16 @@ export const LEGACY_BUCKET_PUT_ACTIONS = [ export const BUCKET_PUT_ACTIONS = [ 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', 's3:Abort*', ]; export const BUCKET_PUT_ACL_ACTIONS = [ 's3:PutObjectAcl', + 's3:PutObjectVersionAcl', ]; export const BUCKET_DELETE_ACTIONS = [ diff --git a/packages/@aws-cdk/aws-s3/lib/rule.ts b/packages/@aws-cdk/aws-s3/lib/rule.ts index bd9492c44ab84..5ce32ba7ed798 100644 --- a/packages/@aws-cdk/aws-s3/lib/rule.ts +++ b/packages/@aws-cdk/aws-s3/lib/rule.ts @@ -151,6 +151,13 @@ export interface NoncurrentVersionTransition { * @default No transition count. */ readonly transitionAfter: Duration; + + /** + * Indicates the number of noncurrent version objects to be retained. Can be up to 100 noncurrent versions retained. + * + * @default No noncurrent version retained. + */ + readonly noncurrentVersionsToRetain?: number; } /** diff --git a/packages/@aws-cdk/aws-s3/package.json b/packages/@aws-cdk/aws-s3/package.json index 86bbb57e431e8..13f96096a7018 100644 --- a/packages/@aws-cdk/aws-s3/package.json +++ b/packages/@aws-cdk/aws-s3/package.json @@ -84,9 +84,9 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/aws-lambda": "^8.10.89", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/aws-lambda": "^8.10.92", + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-events": "0.0.0", diff --git a/packages/@aws-cdk/aws-s3/test/bucket.test.ts b/packages/@aws-cdk/aws-s3/test/bucket.test.ts index 574d0d38e4755..dbbd49420c493 100644 --- a/packages/@aws-cdk/aws-s3/test/bucket.test.ts +++ b/packages/@aws-cdk/aws-s3/test/bucket.test.ts @@ -809,6 +809,10 @@ describe('bucket', () => { 's3:List*', 's3:DeleteObject*', 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', 's3:Abort*', ], 'Effect': 'Allow', @@ -1084,6 +1088,10 @@ describe('bucket', () => { 's3:List*', 's3:DeleteObject*', 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', 's3:Abort*', ], 'Effect': 'Allow', @@ -1117,6 +1125,10 @@ describe('bucket', () => { 'Action': [ 's3:DeleteObject*', 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', 's3:Abort*', ], 'Effect': 'Allow', @@ -1184,6 +1196,10 @@ describe('bucket', () => { 'Action': [ 's3:DeleteObject*', 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', 's3:Abort*', ], 'Effect': 'Allow', @@ -1217,6 +1233,10 @@ describe('bucket', () => { { 'Action': [ 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', 's3:Abort*', ], 'Effect': 'Allow', @@ -1247,8 +1267,23 @@ describe('bucket', () => { const resources = Template.fromStack(stack).toJSON().Resources; const actions = (id: string) => resources[id].Properties.PolicyDocument.Statement[0].Action; - expect(actions('WriterDefaultPolicyDC585BCE')).toEqual(['s3:DeleteObject*', 's3:PutObject', 's3:Abort*']); - expect(actions('PutterDefaultPolicyAB138DD3')).toEqual(['s3:PutObject', 's3:Abort*']); + expect(actions('WriterDefaultPolicyDC585BCE')).toEqual([ + 's3:DeleteObject*', + 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', + 's3:Abort*', + ]); + expect(actions('PutterDefaultPolicyAB138DD3')).toEqual([ + 's3:PutObject', + 's3:PutObjectLegalHold', + 's3:PutObjectRetention', + 's3:PutObjectTagging', + 's3:PutObjectVersionTagging', + 's3:Abort*', + ]); expect(actions('DeleterDefaultPolicyCD33B8A0')).toEqual('s3:DeleteObject*'); }); 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 f5cf756e8a75d..da2c8cf503fe6 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 @@ -110,7 +110,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3Bucket2C6C817C" + "Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3Bucket09A62232" }, "S3Key": { "Fn::Join": [ @@ -123,7 +123,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6" + "Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE" } ] } @@ -136,7 +136,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6" + "Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE" } ] } @@ -228,7 +228,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfS3BucketE1985B35" + "Ref": "AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6S3BucketB51EC107" }, "S3Key": { "Fn::Join": [ @@ -241,7 +241,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfS3VersionKey610C6DE2" + "Ref": "AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6S3VersionKey2B267DB5" } ] } @@ -254,7 +254,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfS3VersionKey610C6DE2" + "Ref": "AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6S3VersionKey2B267DB5" } ] } @@ -297,29 +297,29 @@ } }, "Parameters": { - "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3Bucket2C6C817C": { + "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3Bucket09A62232": { "Type": "String", - "Description": "S3 bucket for asset \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\"" + "Description": "S3 bucket for asset \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\"" }, - "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6": { + "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE": { "Type": "String", - "Description": "S3 key for asset version \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\"" + "Description": "S3 key for asset version \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\"" }, - "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709ArtifactHash17D48178": { + "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824ArtifactHash76F8FCF2": { "Type": "String", - "Description": "Artifact hash for asset \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\"" + "Description": "Artifact hash for asset \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\"" }, - "AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfS3BucketE1985B35": { + "AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6S3BucketB51EC107": { "Type": "String", - "Description": "S3 bucket for asset \"618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abf\"" + "Description": "S3 bucket for asset \"31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6\"" }, - "AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfS3VersionKey610C6DE2": { + "AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6S3VersionKey2B267DB5": { "Type": "String", - "Description": "S3 key for asset version \"618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abf\"" + "Description": "S3 key for asset version \"31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6\"" }, - "AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfArtifactHash467DFC33": { + "AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6ArtifactHashEE982197": { "Type": "String", - "Description": "Artifact hash for asset \"618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abf\"" + "Description": "Artifact hash for asset \"31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-inventory.expected.json b/packages/@aws-cdk/aws-s3/test/integ.bucket-inventory.expected.json index f5610756ad71e..a142de99be8b0 100644 --- a/packages/@aws-cdk/aws-s3/test/integ.bucket-inventory.expected.json +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-inventory.expected.json @@ -155,4 +155,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-sharing.lit.expected.json b/packages/@aws-cdk/aws-s3/test/integ.bucket-sharing.lit.expected.json index 4197e9179b4ff..d3f68abaf02b5 100644 --- a/packages/@aws-cdk/aws-s3/test/integ.bucket-sharing.lit.expected.json +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-sharing.lit.expected.json @@ -38,6 +38,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -71,4 +75,4 @@ } } } -] +] \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket.expected.json b/packages/@aws-cdk/aws-s3/test/integ.bucket.expected.json index 58cd3c5760961..4352395e831c2 100644 --- a/packages/@aws-cdk/aws-s3/test/integ.bucket.expected.json +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket.expected.json @@ -89,6 +89,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -173,4 +177,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket.url.lit.expected.json b/packages/@aws-cdk/aws-s3/test/integ.bucket.url.lit.expected.json index 37a7d24a40029..6ab5dcbfef26e 100644 --- a/packages/@aws-cdk/aws-s3/test/integ.bucket.url.lit.expected.json +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket.url.lit.expected.json @@ -44,7 +44,10 @@ [ "https://", { - "Fn::GetAtt": ["MyBucketF68F3FF0", "RegionalDomainName"] + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "RegionalDomainName" + ] }, "/myfolder/myfile.txt" ] @@ -58,7 +61,10 @@ [ "https://", { - "Fn::GetAtt": ["MyBucketF68F3FF0", "DomainName"] + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "DomainName" + ] }, "/myfolder/myfile.txt" ] @@ -80,4 +86,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/notification.test.ts b/packages/@aws-cdk/aws-s3/test/notification.test.ts index fbc8e1aa45a49..411852018d081 100644 --- a/packages/@aws-cdk/aws-s3/test/notification.test.ts +++ b/packages/@aws-cdk/aws-s3/test/notification.test.ts @@ -1,4 +1,5 @@ import { Template } from '@aws-cdk/assertions'; +import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as s3 from '../lib'; @@ -30,6 +31,29 @@ describe('notification', () => { }); }); + test('can specify a custom role for the notifications handler of imported buckets', () => { + const stack = new cdk.Stack(); + + const importedRole = iam.Role.fromRoleArn(stack, 'role', 'arn:aws:iam::111111111111:role/DevsNotAllowedToTouch'); + + const bucket = s3.Bucket.fromBucketAttributes(stack, 'MyBucket', { + bucketName: 'foo-bar', + notificationsHandlerRole: importedRole, + }); + + bucket.addEventNotification(s3.EventType.OBJECT_CREATED, { + bind: () => ({ + arn: 'ARN', + type: s3.BucketNotificationDestinationType.TOPIC, + }), + }); + + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { + Description: 'AWS CloudFormation handler for "Custom::S3BucketNotifications" resources (@aws-cdk/aws-s3)', + Role: 'arn:aws:iam::111111111111:role/DevsNotAllowedToTouch', + }); + }); + test('can specify prefix and suffix filter rules', () => { const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-s3/test/rules.test.ts b/packages/@aws-cdk/aws-s3/test/rules.test.ts index 8f2a39cb4fa99..4adf00e4b3441 100644 --- a/packages/@aws-cdk/aws-s3/test/rules.test.ts +++ b/packages/@aws-cdk/aws-s3/test/rules.test.ts @@ -139,4 +139,76 @@ describe('rules', () => { }, }); }); + + test('Noncurrent transistion rule with versions to retain', () => { + // GIVEN + const stack = new Stack(); + + // WHEN: Noncurrent version to retain available + new Bucket(stack, 'Bucket1', { + versioned: true, + lifecycleRules: [{ + noncurrentVersionExpiration: Duration.days(10), + noncurrentVersionTransitions: [ + { + storageClass: StorageClass.GLACIER_INSTANT_RETRIEVAL, + transitionAfter: Duration.days(10), + noncurrentVersionsToRetain: 1, + }, + ], + }], + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { + LifecycleConfiguration: { + Rules: [{ + NoncurrentVersionExpirationInDays: 10, + NoncurrentVersionTransitions: [ + { + NewerNoncurrentVersions: 1, + StorageClass: 'GLACIER_IR', + TransitionInDays: 10, + }, + ], + Status: 'Enabled', + }], + }, + }); + }); + + test('Noncurrent transistion rule without versions to retain', () => { + // GIVEN + const stack = new Stack(); + + // WHEN: Noncurrent version to retain not set + new Bucket(stack, 'Bucket1', { + versioned: true, + lifecycleRules: [{ + noncurrentVersionExpiration: Duration.days(10), + noncurrentVersionTransitions: [ + { + storageClass: StorageClass.GLACIER_INSTANT_RETRIEVAL, + transitionAfter: Duration.days(10), + }, + ], + }], + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { + LifecycleConfiguration: { + Rules: [{ + NoncurrentVersionExpirationInDays: 10, + NoncurrentVersionTransitions: [ + { + StorageClass: 'GLACIER_IR', + TransitionInDays: 10, + }, + ], + Status: 'Enabled', + }], + }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-s3objectlambda/package.json b/packages/@aws-cdk/aws-s3objectlambda/package.json index 54db2b52dc4fe..23f511c76cc9e 100644 --- a/packages/@aws-cdk/aws-s3objectlambda/package.json +++ b/packages/@aws-cdk/aws-s3objectlambda/package.json @@ -30,6 +30,13 @@ "distName": "aws-cdk.aws-s3objectlambda", "module": "aws_cdk.aws_s3objectlambda" } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } } }, "repository": { @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-s3outposts/package.json b/packages/@aws-cdk/aws-s3outposts/package.json index 2e04d5bd82659..0ad8877061133 100644 --- a/packages/@aws-cdk/aws-s3outposts/package.json +++ b/packages/@aws-cdk/aws-s3outposts/package.json @@ -7,6 +7,13 @@ "jsii": { "outdir": "dist", "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + }, "targets": { "dotnet": { "namespace": "Amazon.CDK.AWS.S3Outposts", @@ -80,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-sagemaker/package.json b/packages/@aws-cdk/aws-sagemaker/package.json index 5d7a3ad04f8cb..cb5332a56fc47 100644 --- a/packages/@aws-cdk/aws-sagemaker/package.json +++ b/packages/@aws-cdk/aws-sagemaker/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -77,7 +84,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-sam/package.json b/packages/@aws-cdk/aws-sam/package.json index 3c70be0bcbeb7..bb86f7d0fceca 100644 --- a/packages/@aws-cdk/aws-sam/package.json +++ b/packages/@aws-cdk/aws-sam/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -77,9 +84,9 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5", - "ts-jest": "^27.1.2" + "@types/jest": "^27.4.1", + "jest": "^27.5.1", + "ts-jest": "^27.1.3" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-sdb/package.json b/packages/@aws-cdk/aws-sdb/package.json index 786e120914404..a4072de0aa01c 100644 --- a/packages/@aws-cdk/aws-sdb/package.json +++ b/packages/@aws-cdk/aws-sdb/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -76,7 +83,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts b/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts index 3656f0d55ba57..7322148e2a245 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts @@ -92,7 +92,7 @@ export class RotationSchedule extends Resource { 'secretsmanager:PutSecretValue', 'secretsmanager:UpdateSecretVersionStage', ], - resources: [props.secret.secretArn], + resources: [props.secret.secretFullArn ? props.secret.secretFullArn : `${props.secret.secretArn}-??????`], }), ); props.rotationLambda.addToRolePolicy( diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts index 5485a812f9120..81e8fa2f4ca81 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts @@ -1,6 +1,6 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; -import { ArnFormat, FeatureFlags, Fn, IResource, Lazy, RemovalPolicy, Resource, SecretValue, Stack, Token } from '@aws-cdk/core'; +import { ArnFormat, FeatureFlags, Fn, IResource, Lazy, RemovalPolicy, Resource, SecretValue, Stack, Token, TokenComparison } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { IConstruct, Construct } from 'constructs'; import { ResourcePolicy } from './policy'; @@ -205,8 +205,8 @@ export class SecretStringValueBeta1 { * ```ts * // Creates a new IAM user, access and secret keys, and stores the secret access key in a Secret. * const user = new iam.User(this, 'User'); - * const accessKey = new iam.CfnAccessKey(this, 'AccessKey', { userName: user.userName }); - * const secretValue = secretsmanager.SecretStringValueBeta1.fromToken(accessKey.attrSecretAccessKey); + * const accessKey = new iam.AccessKey(this, 'AccessKey', { user }); + * const secretValue = secretsmanager.SecretStringValueBeta1.fromToken(accessKey.secretAccessKey.toString()); * new secretsmanager.Secret(this, 'Secret', { * secretStringBeta1: secretValue, * }); @@ -216,7 +216,7 @@ export class SecretStringValueBeta1 { * const secretValue = secretsmanager.SecretStringValueBeta1.fromToken(JSON.stringify({ * username: user.userName, * database: 'foo', - * password: accessKey.attrSecretAccessKey + * password: accessKey.secretAccessKey.toString(), * })); * * Note that the value being a Token does *not* guarantee safety. For example, a Lazy-evaluated string @@ -306,8 +306,10 @@ abstract class SecretBase extends Resource implements ISecret { ); } + const crossAccount = Token.compareStrings(Stack.of(this).account, grantee.grantPrincipal.principalAccount || ''); + // Throw if secret is not imported and it's shared cross account and no KMS key is provided - if (this instanceof Secret && result.resourceStatement && !this.encryptionKey) { + if (this instanceof Secret && result.resourceStatement && (!this.encryptionKey && crossAccount === TokenComparison.DIFFERENT)) { throw new Error('KMS Key must be provided for cross account access to Secret'); } diff --git a/packages/@aws-cdk/aws-secretsmanager/package.json b/packages/@aws-cdk/aws-secretsmanager/package.json index 2b8a496efdffc..abf808fed6a01 100644 --- a/packages/@aws-cdk/aws-secretsmanager/package.json +++ b/packages/@aws-cdk/aws-secretsmanager/package.json @@ -80,12 +80,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-ec2": "0.0.0", diff --git a/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.expected.json b/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.expected.json index 1ae60ce0e2437..e72f363ac687f 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.expected.json +++ b/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.expected.json @@ -127,7 +127,7 @@ } } }, - "AccessKey": { + "AccessKeyE6B25659": { "Type": "AWS::IAM::AccessKey", "Properties": { "UserName": { @@ -140,7 +140,7 @@ "Properties": { "SecretString": { "Fn::GetAtt": [ - "AccessKey", + "AccessKeyE6B25659", "SecretAccessKey" ] } diff --git a/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.ts b/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.ts index f86acbabc12e2..7d63f61dbb34a 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.ts @@ -31,9 +31,9 @@ class SecretsManagerStack extends cdk.Stack { }); // Secret with predefined value - const accessKey = new iam.CfnAccessKey(this, 'AccessKey', { userName: user.userName }); + const accessKey = new iam.AccessKey(this, 'AccessKey', { user }); new secretsmanager.Secret(this, 'PredefinedSecret', { - secretStringBeta1: secretsmanager.SecretStringValueBeta1.fromToken(accessKey.attrSecretAccessKey), + secretStringBeta1: secretsmanager.SecretStringValueBeta1.fromToken(accessKey.secretAccessKey.toString()), }); /// !hide } diff --git a/packages/@aws-cdk/aws-secretsmanager/test/rotation-schedule.test.ts b/packages/@aws-cdk/aws-secretsmanager/test/rotation-schedule.test.ts index e38ffde417b18..caa6543ec42f1 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/rotation-schedule.test.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/rotation-schedule.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as kms from '@aws-cdk/aws-kms'; import * as lambda from '@aws-cdk/aws-lambda'; @@ -26,7 +26,7 @@ test('create a rotation schedule with a rotation Lambda', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { SecretId: { Ref: 'SecretA720EF05', }, @@ -58,7 +58,7 @@ test('assign permissions for rotation schedule with a rotation Lambda', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', FunctionName: { 'Fn::GetAtt': [ @@ -69,7 +69,7 @@ test('assign permissions for rotation schedule with a rotation Lambda', () => { Principal: 'secretsmanager.amazonaws.com', }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -101,6 +101,57 @@ test('assign permissions for rotation schedule with a rotation Lambda', () => { }); }); +test('grants correct permissions for secret imported by name', () => { + // GIVEN + const secret = secretsmanager.Secret.fromSecretNameV2(stack, 'Secret', 'mySecretName'); + const rotationLambda = new lambda.Function(stack, 'Lambda', { + runtime: lambda.Runtime.NODEJS_10_X, + code: lambda.Code.fromInline('export.handler = event => event;'), + handler: 'index.handler', + }); + + // WHEN + new secretsmanager.RotationSchedule(stack, 'RotationSchedule', { + secret, + rotationLambda, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: Match.arrayWith([ + { + Action: [ + 'secretsmanager:DescribeSecret', + 'secretsmanager:GetSecretValue', + 'secretsmanager:PutSecretValue', + 'secretsmanager:UpdateSecretVersionStage', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':secretsmanager:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':secret:mySecretName-??????', + ]], + }, + }, + ]), + Version: '2012-10-17', + }, + PolicyName: 'LambdaServiceRoleDefaultPolicyDAE46E21', + Roles: [ + { + Ref: 'LambdaServiceRoleA8ED4D3B', + }, + ], + }); +}); + test('assign kms permissions for rotation schedule with a rotation Lambda', () => { // GIVEN const encryptionKey = new kms.Key(stack, 'Key'); @@ -118,9 +169,9 @@ test('assign kms permissions for rotation schedule with a rotation Lambda', () = }); // THEN - expect(stack).toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { - Statement: [{}, {}, {}, + Statement: [Match.anyValue(), Match.anyValue(), Match.anyValue(), { Action: [ 'kms:Decrypt', @@ -172,7 +223,7 @@ describe('hosted rotation', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { SecretId: { Ref: 'SecretA720EF05', }, @@ -188,7 +239,7 @@ describe('hosted rotation', () => { Transform: 'AWS::SecretsManager-2020-07-23', })); - expect(stack).toHaveResource('AWS::SecretsManager::ResourcePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::ResourcePolicy', { ResourcePolicy: { Statement: [ { @@ -236,7 +287,7 @@ describe('hosted rotation', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { SecretId: { Ref: 'SecretA720EF05', }, @@ -251,7 +302,7 @@ describe('hosted rotation', () => { }, }); - expect(stack).toHaveResource('AWS::SecretsManager::ResourcePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::ResourcePolicy', { ResourcePolicy: { Statement: [ { @@ -302,7 +353,7 @@ describe('hosted rotation', () => { dbConnections.allowDefaultPortFrom(hostedRotation); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { SecretId: { Ref: 'SecretA720EF05', }, @@ -334,7 +385,7 @@ describe('hosted rotation', () => { }, }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { FromPort: 3306, GroupId: { 'Fn::GetAtt': [ @@ -374,7 +425,7 @@ describe('hosted rotation', () => { dbConnections.allowDefaultPortFrom(hostedRotation); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { SecretId: { Ref: 'SecretA720EF05', }, @@ -420,7 +471,7 @@ describe('hosted rotation', () => { }, }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { FromPort: 3306, GroupId: { 'Fn::GetAtt': [ diff --git a/packages/@aws-cdk/aws-secretsmanager/test/secret-rotation.test.ts b/packages/@aws-cdk/aws-secretsmanager/test/secret-rotation.test.ts index 974aa0c611c4d..c6c153ce83ae6 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/secret-rotation.test.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/secret-rotation.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as secretsmanager from '../lib'; @@ -34,7 +34,7 @@ test('secret rotation single user', () => { }); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { IpProtocol: 'tcp', Description: 'from SecretRotationSecurityGroupAEC520AB:3306', FromPort: 3306, @@ -53,7 +53,7 @@ test('secret rotation single user', () => { ToPort: 3306, }); - expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { SecretId: { Ref: 'SecretA720EF05', }, @@ -68,11 +68,11 @@ test('secret rotation single user', () => { }, }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/SecretRotation/SecurityGroup', }); - expect(stack).toHaveResource('AWS::Serverless::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::Serverless::Application', { Location: { ApplicationId: { 'Fn::FindInMap': ['SecretRotationSARMappingC10A2F5D', { Ref: 'AWS::Partition' }, 'applicationId'], @@ -122,7 +122,7 @@ test('secret rotation single user', () => { }, }); - expect(stack).toHaveResource('AWS::SecretsManager::ResourcePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::ResourcePolicy', { ResourcePolicy: { Statement: [ { @@ -171,7 +171,7 @@ test('secret rotation multi user', () => { }); // THEN - expect(stack).toHaveResource('AWS::Serverless::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::Serverless::Application', { Parameters: { endpoint: { 'Fn::Join': [ @@ -215,7 +215,7 @@ test('secret rotation multi user', () => { }, }); - expect(stack).toHaveResource('AWS::SecretsManager::ResourcePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::ResourcePolicy', { ResourcePolicy: { Statement: [ { @@ -261,7 +261,7 @@ test('secret rotation allows passing an empty string for excludeCharacters', () }); // THEN - expect(stack).toHaveResourceLike('AWS::Serverless::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::Serverless::Application', { Parameters: { excludeCharacters: '', }, @@ -304,7 +304,7 @@ test('rotation function name does not exceed 64 chars', () => { }); // THEN - expect(stack).toHaveResource('AWS::Serverless::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::Serverless::Application', { Parameters: { endpoint: { 'Fn::Join': [ @@ -364,7 +364,7 @@ test('with interface vpc endpoint', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::Serverless::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::Serverless::Application', { Parameters: { endpoint: { 'Fn::Join': ['', [ diff --git a/packages/@aws-cdk/aws-secretsmanager/test/secret.test.ts b/packages/@aws-cdk/aws-secretsmanager/test/secret.test.ts index 365374e84bdd4..cd90bc9c715df 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/secret.test.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/secret.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT, expect as assertExpect, ResourcePart } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as lambda from '@aws-cdk/aws-lambda'; @@ -19,7 +18,7 @@ test('default secret', () => { new secretsmanager.Secret(stack, 'Secret'); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { GenerateSecretString: {}, }); }); @@ -31,11 +30,9 @@ test('set removalPolicy to secret', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::SecretsManager::Secret', - { - DeletionPolicy: 'Retain', - }, ResourcePart.CompleteDefinition, - ); + Template.fromStack(stack).hasResource('AWS::SecretsManager::Secret', { + DeletionPolicy: 'Retain', + }); }); test('secret with kms', () => { @@ -46,10 +43,9 @@ test('secret with kms', () => { new secretsmanager.Secret(stack, 'Secret', { encryptionKey: key }); // THEN - expect(stack).toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { - Statement: [ - {}, + Statement: Match.arrayWith([ { Effect: 'Allow', Resource: '*', @@ -136,7 +132,7 @@ test('secret with kms', () => { }, }, }, - ], + ]), Version: '2012-10-17', }, }); @@ -152,7 +148,7 @@ test('secret with generate secret string options', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { GenerateSecretString: { ExcludeUppercase: true, PasswordLength: 20, @@ -170,7 +166,7 @@ test('templated secret string', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { GenerateSecretString: { SecretStringTemplate: '{"username":"username"}', GenerateStringKey: 'password', @@ -180,11 +176,11 @@ test('templated secret string', () => { describe('secretStringBeta1', () => { let user: iam.User; - let accessKey: iam.CfnAccessKey; + let accessKey: iam.AccessKey; beforeEach(() => { user = new iam.User(stack, 'User'); - accessKey = new iam.CfnAccessKey(stack, 'MyKey', { userName: user.userName }); + accessKey = new iam.AccessKey(stack, 'MyKey', { user }); }); test('fromUnsafePlaintext allows specifying a plaintext string', () => { @@ -192,8 +188,8 @@ describe('secretStringBeta1', () => { secretStringBeta1: secretsmanager.SecretStringValueBeta1.fromUnsafePlaintext('unsafeP@$$'), }); - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { - GenerateSecretString: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { + GenerateSecretString: Match.absent(), SecretString: 'unsafeP@$$', }); }); @@ -206,26 +202,26 @@ describe('secretStringBeta1', () => { test('toToken allows referencing a construct attribute', () => { new secretsmanager.Secret(stack, 'Secret', { - secretStringBeta1: secretsmanager.SecretStringValueBeta1.fromToken(accessKey.attrSecretAccessKey), + secretStringBeta1: secretsmanager.SecretStringValueBeta1.fromToken(accessKey.secretAccessKey.toString()), }); - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { - GenerateSecretString: ABSENT, - SecretString: { 'Fn::GetAtt': ['MyKey', 'SecretAccessKey'] }, + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { + GenerateSecretString: Match.absent(), + SecretString: { 'Fn::GetAtt': ['MyKey6AB29FA6', 'SecretAccessKey'] }, }); }); test('toToken allows referencing a construct attribute in nested JSON', () => { const secretString = secretsmanager.SecretStringValueBeta1.fromToken(JSON.stringify({ - key: accessKey.attrSecretAccessKey, + key: accessKey.secretAccessKey.toString(), username: 'myUser', })); new secretsmanager.Secret(stack, 'Secret', { secretStringBeta1: secretString, }); - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { - GenerateSecretString: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { + GenerateSecretString: Match.absent(), SecretString: { 'Fn::Join': [ '', @@ -233,7 +229,7 @@ describe('secretStringBeta1', () => { '{"key":"', { 'Fn::GetAtt': [ - 'MyKey', + 'MyKey6AB29FA6', 'SecretAccessKey', ], }, @@ -248,7 +244,7 @@ describe('secretStringBeta1', () => { // NOTE - This is actually not desired behavior, but the simple `!Token.isUnresolved` // check is the simplest and most consistent to implement. Covering this edge case of // a resolved Token representing a Ref/Fn::GetAtt is out of scope for this initial pass. - const secretKey = stack.resolve(accessKey.attrSecretAccessKey); + const secretKey = stack.resolve(accessKey.secretAccessKey); expect(() => new secretsmanager.Secret(stack, 'Secret', { secretStringBeta1: secretsmanager.SecretStringValueBeta1.fromToken(secretKey), })).toThrow(/appears to be plaintext/); @@ -260,13 +256,53 @@ describe('secretStringBeta1', () => { generateStringKey: 'username', secretStringTemplate: JSON.stringify({ username: 'username' }), }, - secretStringBeta1: secretsmanager.SecretStringValueBeta1.fromToken(accessKey.attrSecretAccessKey), + secretStringBeta1: secretsmanager.SecretStringValueBeta1.fromToken(accessKey.secretAccessKey.toString()), })).toThrow(/Cannot specify both `generateSecretString` and `secretStringBeta1`./); }); }); test('grantRead', () => { + // GIVEN + const secret = new secretsmanager.Secret(stack, 'Secret'); + const role = new iam.Role(stack, 'Role', { assumedBy: new iam.AccountRootPrincipal() }); + + // WHEN + secret.grantRead(role); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [{ + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], + Effect: 'Allow', + Resource: { Ref: 'SecretA720EF05' }, + }], + }, + }); +}); + +test('Error when grantRead with different role and no KMS', () => { + // GIVEN + const testStack = new cdk.Stack(app, 'TestStack', { + env: { + account: '123456789012', + }, + }); + const secret = new secretsmanager.Secret(testStack, 'Secret'); + const role = iam.Role.fromRoleArn(testStack, 'RoleFromArn', 'arn:aws:iam::111111111111:role/SomeRole'); + + // THEN + expect(() => { + secret.grantRead(role); + }).toThrowError('KMS Key must be provided for cross account access to Secret'); +}); + +test('grantRead with KMS Key', () => { // GIVEN const key = new kms.Key(stack, 'KMS'); const secret = new secretsmanager.Secret(stack, 'Secret', { encryptionKey: key }); @@ -276,7 +312,7 @@ test('grantRead', () => { secret.grantRead(role); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [{ @@ -289,12 +325,9 @@ test('grantRead', () => { }], }, }); - expect(stack).toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { - Statement: [ - {}, - {}, - {}, + Statement: Match.arrayWith([ { Action: 'kms:Decrypt', Condition: { @@ -324,7 +357,7 @@ test('grantRead', () => { }, Resource: '*', }, - ], + ]), Version: '2012-10-17', }, }); @@ -340,7 +373,7 @@ test('grantRead cross account', () => { secret.grantRead(principal, ['FOO', 'bar']).assertSuccess(); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::ResourcePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::ResourcePolicy', { ResourcePolicy: { Statement: [ { @@ -383,48 +416,43 @@ test('grantRead cross account', () => { }, }); - expect(stack).toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { - Statement: [ - {}, - {}, - {}, - { - Action: 'kms:Decrypt', - Condition: { - StringEquals: { - 'kms:ViaService': { - 'Fn::Join': [ - '', - [ - 'secretsmanager.', - { - Ref: 'AWS::Region', - }, - '.amazonaws.com', - ], - ], - }, - }, - }, - Effect: 'Allow', - Principal: { - AWS: { + Statement: Match.arrayWith([{ + Action: 'kms:Decrypt', + Condition: { + StringEquals: { + 'kms:ViaService': { 'Fn::Join': [ '', [ - 'arn:', + 'secretsmanager.', { - Ref: 'AWS::Partition', + Ref: 'AWS::Region', }, - ':iam::1234:root', + '.amazonaws.com', ], ], }, }, - Resource: '*', }, - ], + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::1234:root', + ], + ], + }, + }, + Resource: '*', + }]), Version: '2012-10-17', }, }); @@ -440,7 +468,7 @@ test('grantRead with version label constraint', () => { secret.grantRead(role, ['FOO', 'bar']); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [{ @@ -458,42 +486,37 @@ test('grantRead with version label constraint', () => { }], }, }); - expect(stack).toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { - Statement: [ - {}, - {}, - {}, - { - Action: 'kms:Decrypt', - Condition: { - StringEquals: { - 'kms:ViaService': { - 'Fn::Join': [ - '', - [ - 'secretsmanager.', - { - Ref: 'AWS::Region', - }, - '.amazonaws.com', - ], + Statement: Match.arrayWith([{ + Action: 'kms:Decrypt', + Condition: { + StringEquals: { + 'kms:ViaService': { + 'Fn::Join': [ + '', + [ + 'secretsmanager.', + { + Ref: 'AWS::Region', + }, + '.amazonaws.com', ], - }, - }, - }, - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::GetAtt': [ - 'Role1ABCC5F0', - 'Arn', ], }, }, - Resource: '*', }, - ], + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::GetAtt': [ + 'Role1ABCC5F0', + 'Arn', + ], + }, + }, + Resource: '*', + }]), Version: '2012-10-17', }, }); @@ -508,7 +531,7 @@ test('grantWrite', () => { secret.grantWrite(role); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [{ @@ -533,8 +556,7 @@ test('grantWrite with kms', () => { secret.grantWrite(role); // THEN - const expectStack = expect(stack); - expectStack.toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [{ @@ -547,46 +569,41 @@ test('grantWrite with kms', () => { }], }, }); - expectStack.toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { - Statement: [ - {}, - {}, - {}, - { - Action: [ - 'kms:Encrypt', - 'kms:ReEncrypt*', - 'kms:GenerateDataKey*', - ], - Condition: { - StringEquals: { - 'kms:ViaService': { - 'Fn::Join': [ - '', - [ - 'secretsmanager.', - { - Ref: 'AWS::Region', - }, - '.amazonaws.com', - ], + Statement: Match.arrayWith([{ + Action: [ + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', + ], + Condition: { + StringEquals: { + 'kms:ViaService': { + 'Fn::Join': [ + '', + [ + 'secretsmanager.', + { + Ref: 'AWS::Region', + }, + '.amazonaws.com', ], - }, - }, - }, - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::GetAtt': [ - 'Role1ABCC5F0', - 'Arn', ], }, }, - Resource: '*', }, - ], + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::GetAtt': [ + 'Role1ABCC5F0', + 'Arn', + ], + }, + }, + Resource: '*', + }]), }, }); }); @@ -605,7 +622,7 @@ test('secretValue', () => { }); // THEN - expect(stack).toHaveResource('CDK::Phony::Resource', { + Template.fromStack(stack).hasResourceProperties('CDK::Phony::Resource', { value: { 'Fn::Join': ['', [ '{{resolve:secretsmanager:', @@ -624,9 +641,8 @@ describe('secretName', () => { }); // Creates secret name by parsing ARN. - expect(stack).toHaveOutput({ - outputName: 'MySecretName', - outputValue: { 'Fn::Select': [6, { 'Fn::Split': [':', { Ref: 'SecretA720EF05' }] }] }, + Template.fromStack(stack).hasOutput('*', { + Value: { 'Fn::Select': [6, { 'Fn::Split': [':', { Ref: 'SecretA720EF05' }] }] }, }); } @@ -659,9 +675,8 @@ describe('secretName', () => { const resourceName = { 'Fn::Select': [6, { 'Fn::Split': [':', { Ref: 'SecretA720EF05' }] }] }; - expect(stack).toHaveOutput({ - outputName: 'MySecretName', - outputValue: { + Template.fromStack(stack).hasOutput('MySecretName', { + Value: { 'Fn::Join': ['-', [ { 'Fn::Select': [0, { 'Fn::Split': ['-', resourceName] }] }, { 'Fn::Select': [1, { 'Fn::Split': ['-', resourceName] }] }, @@ -680,11 +695,8 @@ describe('secretName', () => { const resourceName = { 'Fn::Select': [6, { 'Fn::Split': [':', { Ref: 'SecretA720EF05' }] }] }; - expect(stack).toHaveOutput({ - outputName: 'MySecretName', - outputValue: { - 'Fn::Select': [0, { 'Fn::Split': ['-', resourceName] }], - }, + Template.fromStack(stack).hasOutput('MySecretName', { + Value: { 'Fn::Select': [0, { 'Fn::Split': ['-', resourceName] }] }, }); }); @@ -699,11 +711,8 @@ describe('secretName', () => { secretNameSegments.push({ 'Fn::Select': [i, { 'Fn::Split': ['-', resourceName] }] }); } - expect(stack).toHaveOutput({ - outputName: 'MySecretName', - outputValue: { - 'Fn::Join': ['-', secretNameSegments], - }, + Template.fromStack(stack).hasOutput('MySecretName', { + Value: { 'Fn::Join': ['-', secretNameSegments] }, }); } @@ -739,7 +748,7 @@ describe('secretName', () => { value: secret2.secretName, }); - const outputs = assertExpect(stack).value.Outputs; + const outputs = Template.fromStack(stack).findOutputs('*'); expect(outputs.MySecretName1).toEqual(outputs.MySecretName2); }); }); @@ -807,9 +816,8 @@ test('import by secretArn supports tokens for ARNs', () => { // THEN expect(secretB.secretArn).toBe(secretA.secretArn); - expect(stackB).toHaveOutput({ - outputName: 'secretBSecretName', - outputValue: { 'Fn::Select': [6, { 'Fn::Split': [':', { 'Fn::ImportValue': 'StackA:ExportsOutputRefSecretA188F281703FC8A52' }] }] }, + Template.fromStack(stackB).hasOutput('secretBSecretName', { + Value: { 'Fn::Select': [6, { 'Fn::Split': [':', { 'Fn::ImportValue': 'StackA:ExportsOutputRefSecretA188F281703FC8A52' }] }] }, }); }); @@ -854,7 +862,7 @@ test('fromSecretCompleteArn - grants', () => { secret.grantWrite(role); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [{ @@ -891,7 +899,7 @@ test('fromSecretCompleteArn - can be assigned to a property with type number', ( }); // THEN - expect(stack).toHaveResourceLike('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { MemorySize: `{{resolve:secretsmanager:${secretArn}:SecretString:LambdaFunctionMemorySize::}}`, }); }); @@ -923,7 +931,7 @@ test('fromSecretPartialArn - grants', () => { secret.grantWrite(role); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [{ @@ -1048,7 +1056,7 @@ testDeprecated('import by secret name with grants', () => { ':secret:MySecret*', ]], }; - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [{ @@ -1116,7 +1124,7 @@ test('import by secret name v2 with grants', () => { ':secret:MySecret-??????', ]], }; - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [{ @@ -1152,7 +1160,7 @@ test('can attach a secret with attach()', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::SecretTargetAttachment', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::SecretTargetAttachment', { SecretId: { Ref: 'SecretA720EF05', }, @@ -1197,7 +1205,7 @@ test('add a rotation schedule to an attached secret', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { SecretId: { Ref: 'SecretAttachment2E1B7C3B', // The secret returned by the attachment, not the secret itself. }, @@ -1244,7 +1252,7 @@ test('can add to the resource policy of a secret', () => { })); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::ResourcePolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::ResourcePolicy', { ResourcePolicy: { Statement: [ { @@ -1297,20 +1305,14 @@ test('fails if secret policy has no IAM principals', () => { test('with replication regions', () => { // WHEN const secret = new secretsmanager.Secret(stack, 'Secret', { - replicaRegions: [ - { - region: 'eu-west-1', - }, - ], + replicaRegions: [{ region: 'eu-west-1' }], }); secret.addReplicaRegion('eu-central-1', kms.Key.fromKeyArn(stack, 'Key', 'arn:aws:kms:eu-central-1:123456789012:key/my-key-id')); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { ReplicaRegions: [ - { - Region: 'eu-west-1', - }, + { Region: 'eu-west-1' }, { KmsKeyId: 'arn:aws:kms:eu-central-1:123456789012:key/my-key-id', Region: 'eu-central-1', diff --git a/packages/@aws-cdk/aws-securityhub/package.json b/packages/@aws-cdk/aws-securityhub/package.json index 3e29438e9e791..2be17327b22a1 100644 --- a/packages/@aws-cdk/aws-securityhub/package.json +++ b/packages/@aws-cdk/aws-securityhub/package.json @@ -85,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-servicecatalog/README.md b/packages/@aws-cdk/aws-servicecatalog/README.md index 436fe376f9624..12e57cbc200ee 100644 --- a/packages/@aws-cdk/aws-servicecatalog/README.md +++ b/packages/@aws-cdk/aws-servicecatalog/README.md @@ -201,21 +201,24 @@ portfolio.addProduct(product); ## Tag Options TagOptions allow administrators to easily manage tags on provisioned products by creating a selection of tags for end users to choose from. -For example, an end user can choose an `ec2` for the instance type size. -TagOptions are created by specifying a key with a selection of values and can be associated with both portfolios and products. +TagOptions are created by specifying a tag key with a selection of allowed values and can be associated with both portfolios and products. When launching a product, both the TagOptions associated with the product and the containing portfolio are made available. At the moment, TagOptions can only be disabled in the console. ```ts fixture=portfolio-product -const tagOptionsForPortfolio = new servicecatalog.TagOptions({ - costCenter: ['Data Insights', 'Marketing'], +const tagOptionsForPortfolio = new servicecatalog.TagOptions(this, 'OrgTagOptions', { + allowedValuesForTags: { + Group: ['finance', 'engineering', 'marketing', 'research'], + CostCenter: ['01', '02','03'], + }, }); portfolio.associateTagOptions(tagOptionsForPortfolio); -const tagOptionsForProduct = new servicecatalog.TagOptions({ - ec2InstanceType: ['A1', 'M4'], - ec2InstanceSize: ['medium', 'large'], +const tagOptionsForProduct = new servicecatalog.TagOptions(this, 'ProductTagOptions', { + allowedValuesForTags: { + Environment: ['dev', 'alpha', 'prod'], + }, }); product.associateTagOptions(tagOptionsForProduct); ``` diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts b/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts index e1e4ee8de38da..dd44ef0f022fc 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts @@ -9,7 +9,7 @@ import { IPortfolio } from '../portfolio'; import { IProduct } from '../product'; import { CfnLaunchNotificationConstraint, CfnLaunchRoleConstraint, CfnLaunchTemplateConstraint, CfnPortfolioProductAssociation, - CfnResourceUpdateConstraint, CfnStackSetConstraint, CfnTagOption, CfnTagOptionAssociation, + CfnResourceUpdateConstraint, CfnStackSetConstraint, CfnTagOptionAssociation, } from '../servicecatalog.generated'; import { TagOptions } from '../tag-options'; import { hashValues } from './util'; @@ -139,33 +139,16 @@ export class AssociationManager { } } - public static associateTagOptions(resource: cdk.IResource, resourceId: string, tagOptions: TagOptions): void { - const resourceStack = cdk.Stack.of(resource); - for (const [key, tagOptionsList] of Object.entries(tagOptions.tagOptionsMap)) { - InputValidator.validateLength(resource.node.addr, 'TagOption key', 1, 128, key); - tagOptionsList.forEach((value: string) => { - InputValidator.validateLength(resource.node.addr, 'TagOption value', 1, 256, value); - const tagOptionKey = hashValues(key, value, resourceStack.node.addr); - const tagOptionConstructId = `TagOption${tagOptionKey}`; - let cfnTagOption = resourceStack.node.tryFindChild(tagOptionConstructId) as CfnTagOption; - if (!cfnTagOption) { - cfnTagOption = new CfnTagOption(resourceStack, tagOptionConstructId, { - key: key, - value: value, - active: true, - }); - } - const tagAssocationKey = hashValues(key, value, resource.node.addr); - const tagAssocationConstructId = `TagOptionAssociation${tagAssocationKey}`; - if (!resource.node.tryFindChild(tagAssocationConstructId)) { - new CfnTagOptionAssociation(resource as cdk.Resource, tagAssocationConstructId, { - resourceId: resourceId, - tagOptionId: cfnTagOption.ref, - }); - } - }); - }; + for (const cfnTagOption of tagOptions._cfnTagOptions) { + const tagAssocationConstructId = `TagOptionAssociation${hashValues(cfnTagOption.key, cfnTagOption.value, resource.node.addr)}`; + if (!resource.node.tryFindChild(tagAssocationConstructId)) { + new CfnTagOptionAssociation(resource as cdk.Resource, tagAssocationConstructId, { + resourceId: resourceId, + tagOptionId: cfnTagOption.ref, + }); + } + } } private static setLaunchRoleConstraint( diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/product.ts b/packages/@aws-cdk/aws-servicecatalog/lib/product.ts index 29a47fc6932a9..3c4e8bd9fb59f 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/product.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/product.ts @@ -1,11 +1,11 @@ import { ArnFormat, IResource, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { TagOptions } from '.'; import { CloudFormationTemplate } from './cloudformation-template'; import { MessageLanguage } from './common'; import { AssociationManager } from './private/association-manager'; import { InputValidator } from './private/validation'; import { CfnCloudFormationProduct } from './servicecatalog.generated'; +import { TagOptions } from './tag-options'; /** * A Service Catalog product, currently only supports type CloudFormationProduct @@ -137,7 +137,7 @@ export interface CloudFormationProductProps { * * @default - No tagOptions provided */ - readonly tagOptions?: TagOptions + readonly tagOptions?: TagOptions; } /** diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/tag-options.ts b/packages/@aws-cdk/aws-servicecatalog/lib/tag-options.ts index 808ea78add4a3..b0342c6336bd3 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/tag-options.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/tag-options.ts @@ -1,14 +1,70 @@ +import * as cdk from '@aws-cdk/core'; +import { hashValues } from './private/util'; +import { InputValidator } from './private/validation'; +import { CfnTagOption } from './servicecatalog.generated'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from 'constructs'; + +/** + * Properties for TagOptions. + */ +export interface TagOptionsProps { + /** + * The values that are allowed to be set for specific tags. + * The keys of the map represent the tag keys, + * and the values of the map are a list of allowed values for that particular tag key. + */ + readonly allowedValuesForTags: { [tagKey: string]: string[] }; +} + /** - * Defines a Tag Option, which are similar to tags - * but have multiple values per key. + * Defines a set of TagOptions, which are a list of key-value pairs managed in AWS Service Catalog. + * It is not an AWS tag, but serves as a template for creating an AWS tag based on the TagOption. + * See https://docs.aws.amazon.com/servicecatalog/latest/adminguide/tagoptions.html + * + * @resource AWS::ServiceCatalog::TagOption */ -export class TagOptions { +export class TagOptions extends cdk.Resource { /** - * List of CfnTagOption - */ - public readonly tagOptionsMap: { [key: string]: string[] }; + * List of underlying CfnTagOption resources. + * + * @internal + */ + public _cfnTagOptions: CfnTagOption[]; - constructor(tagOptionsMap: { [key: string]: string[]} ) { - this.tagOptionsMap = { ...tagOptionsMap }; + constructor(scope: Construct, id: string, props: TagOptionsProps) { + super(scope, id); + + this._cfnTagOptions = this.createUnderlyingTagOptions(props.allowedValuesForTags); + } + + private createUnderlyingTagOptions(allowedValuesForTags: { [tagKey: string]: string[] }): CfnTagOption[] { + if (Object.keys(allowedValuesForTags).length === 0) { + throw new Error(`No tag option keys or values were provided for resource ${this.node.path}`); + } + var tagOptions: CfnTagOption[] = []; + + for (const [tagKey, tagValues] of Object.entries(allowedValuesForTags)) { + InputValidator.validateLength(this.node.addr, 'TagOption key', 1, 128, tagKey); + + const uniqueTagValues = new Set(tagValues); + if (uniqueTagValues.size === 0) { + throw new Error(`No tag option values were provided for tag option key ${tagKey} for resource ${this.node.path}`); + } + uniqueTagValues.forEach((tagValue: string) => { + InputValidator.validateLength(this.node.addr, 'TagOption value', 1, 256, tagValue); + const tagOptionIdentifier = hashValues(tagKey, tagValue); + const tagOption = new CfnTagOption(this, tagOptionIdentifier, { + key: tagKey, + value: tagValue, + active: true, + }); + tagOptions.push(tagOption); + }); + } + return tagOptions; } } + diff --git a/packages/@aws-cdk/aws-servicecatalog/package.json b/packages/@aws-cdk/aws-servicecatalog/package.json index bd597180a56cd..9a4c6500da1f8 100644 --- a/packages/@aws-cdk/aws-servicecatalog/package.json +++ b/packages/@aws-cdk/aws-servicecatalog/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -77,7 +84,7 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-iam": "0.0.0", @@ -105,7 +112,13 @@ "props-physical-name:@aws-cdk/aws-servicecatalog.CloudFormationProductProps", "resource-attribute:@aws-cdk/aws-servicecatalog.Portfolio.portfolioName", "props-physical-name:@aws-cdk/aws-servicecatalog.PortfolioProps", - "props-physical-name:@aws-cdk/aws-servicecatalog.ProductStack" + "props-physical-name:@aws-cdk/aws-servicecatalog.ProductStack", + "props-struct-name:@aws-cdk/aws-servicecatalog.ITagOptions", + "props-physical-name:@aws-cdk/aws-servicecatalog.TagOptionsProps", + "ref-via-interface:@aws-cdk/aws-servicecatalog.CloudFormationProductProps.tagOptions", + "ref-via-interface:@aws-cdk/aws-servicecatalog.IProduct.associateTagOptions.tagOptions", + "ref-via-interface:@aws-cdk/aws-servicecatalog.IPortfolio.associateTagOptions.tagOptions", + "ref-via-interface:@aws-cdk/aws-servicecatalog.PortfolioProps.tagOptions" ] }, "maturity": "experimental", diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json index c298f292d039d..c25d867209591 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json @@ -81,7 +81,7 @@ "Ref": "TestPortfolio4AC794EB" }, "TagOptionId": { - "Ref": "TagOptionc0d88a3c4b8b" + "Ref": "TagOptions5f31c54ba705F110F743" } } }, @@ -92,7 +92,7 @@ "Ref": "TestPortfolio4AC794EB" }, "TagOptionId": { - "Ref": "TagOption9b16df08f83d" + "Ref": "TagOptions8d263919cebb6764AC10" } } }, @@ -103,7 +103,7 @@ "Ref": "TestPortfolio4AC794EB" }, "TagOptionId": { - "Ref": "TagOptiondf34c1c83580" + "Ref": "TagOptionsa260cbbd99c416C40F73" } } }, @@ -217,7 +217,7 @@ "TestPortfolioPortfolioProductAssociationa0185761d231B0D998A7" ] }, - "TagOptionc0d88a3c4b8b": { + "TagOptions5f31c54ba705F110F743": { "Type": "AWS::ServiceCatalog::TagOption", "Properties": { "Key": "key1", @@ -225,7 +225,7 @@ "Active": true } }, - "TagOption9b16df08f83d": { + "TagOptions8d263919cebb6764AC10": { "Type": "AWS::ServiceCatalog::TagOption", "Properties": { "Key": "key1", @@ -233,7 +233,7 @@ "Active": true } }, - "TagOptiondf34c1c83580": { + "TagOptionsa260cbbd99c416C40F73": { "Type": "AWS::ServiceCatalog::TagOption", "Properties": { "Key": "key2", @@ -263,7 +263,7 @@ "Ref": "TestProduct7606930B" }, "TagOptionId": { - "Ref": "TagOptionc0d88a3c4b8b" + "Ref": "TagOptions5f31c54ba705F110F743" } } }, @@ -274,7 +274,7 @@ "Ref": "TestProduct7606930B" }, "TagOptionId": { - "Ref": "TagOption9b16df08f83d" + "Ref": "TagOptions8d263919cebb6764AC10" } } }, @@ -285,7 +285,7 @@ "Ref": "TestProduct7606930B" }, "TagOptionId": { - "Ref": "TagOptiondf34c1c83580" + "Ref": "TagOptionsa260cbbd99c416C40F73" } } }, diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts index 669016f35be2a..c5941dec42062 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts @@ -22,9 +22,11 @@ const portfolio = new servicecatalog.Portfolio(stack, 'TestPortfolio', { portfolio.giveAccessToRole(role); portfolio.giveAccessToGroup(group); -const tagOptions = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], - key2: ['value1'], +const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1'], + }, }); portfolio.associateTagOptions(tagOptions); diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.product.expected.json b/packages/@aws-cdk/aws-servicecatalog/test/integ.product.expected.json index fb51ec2ad0df4..05ea621ec10fd 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.product.expected.json +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.product.expected.json @@ -226,7 +226,7 @@ "Ref": "TestProduct7606930B" }, "TagOptionId": { - "Ref": "TagOptionab501c9aef99" + "Ref": "TagOptions5f31c54ba705F110F743" } } }, @@ -237,7 +237,7 @@ "Ref": "TestProduct7606930B" }, "TagOptionId": { - "Ref": "TagOptiona453ac93ee6f" + "Ref": "TagOptions8d263919cebb6764AC10" } } }, @@ -248,11 +248,11 @@ "Ref": "TestProduct7606930B" }, "TagOptionId": { - "Ref": "TagOptiona006431604cb" + "Ref": "TagOptionsa260cbbd99c416C40F73" } } }, - "TagOptionab501c9aef99": { + "TagOptions5f31c54ba705F110F743": { "Type": "AWS::ServiceCatalog::TagOption", "Properties": { "Key": "key1", @@ -260,7 +260,7 @@ "Active": true } }, - "TagOptiona453ac93ee6f": { + "TagOptions8d263919cebb6764AC10": { "Type": "AWS::ServiceCatalog::TagOption", "Properties": { "Key": "key1", @@ -268,7 +268,7 @@ "Active": true } }, - "TagOptiona006431604cb": { + "TagOptionsa260cbbd99c416C40F73": { "Type": "AWS::ServiceCatalog::TagOption", "Properties": { "Key": "key2", diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.product.ts b/packages/@aws-cdk/aws-servicecatalog/test/integ.product.ts index e1e08105ee3ce..22429b3ddbf83 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.product.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.product.ts @@ -38,9 +38,11 @@ const product = new servicecatalog.CloudFormationProduct(stack, 'TestProduct', { ], }); -const tagOptions = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], - key2: ['value1'], +const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1'], + }, }); product.associateTagOptions(tagOptions); diff --git a/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts b/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts index 43a283c157f80..e53062e0a6752 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts @@ -304,9 +304,11 @@ describe('portfolio associations and product constraints', () => { }), test('add tag options to portfolio', () => { - const tagOptions = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], - key2: ['value1'], + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1'], + }, }); portfolio.associateTagOptions(tagOptions); @@ -316,9 +318,11 @@ describe('portfolio associations and product constraints', () => { }), test('add tag options to portfolio as prop', () => { - const tagOptions = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], - key2: ['value1'], + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1'], + }, }); portfolio = new servicecatalog.Portfolio(stack, 'MyPortfolioWithTag', { @@ -331,53 +335,21 @@ describe('portfolio associations and product constraints', () => { Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOptionAssociation', 3); }), - test('adding identical tag options to portfolio is idempotent', () => { - const tagOptions1 = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], - key2: ['value1'], - }); - - const tagOptions2 = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], + test('adding tag options to portfolio multiple times is idempotent', () => { + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1'], + }, }); - portfolio.associateTagOptions(tagOptions1); - portfolio.associateTagOptions(tagOptions2); // If not idempotent this would fail + portfolio.associateTagOptions(tagOptions); + portfolio.associateTagOptions(tagOptions); // If not idempotent this would fail Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOption', 3); //Generates a resource for each unique key-value pair Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOptionAssociation', 3); }), - test('fails to add tag options with invalid minimum key length', () => { - const tagOptions = new servicecatalog.TagOptions({ - '': ['value1', 'value2'], - 'key2': ['value1'], - }); - expect(() => { - portfolio.associateTagOptions(tagOptions); - }).toThrowError(/Invalid TagOption key for resource/); - }); - - test('fails to add tag options with invalid maxium key length', () => { - const tagOptions = new servicecatalog.TagOptions({ - ['key1'.repeat(1000)]: ['value1', 'value2'], - key2: ['value1'], - }); - expect(() => { - portfolio.associateTagOptions(tagOptions); - }).toThrowError(/Invalid TagOption key for resource/); - }), - - test('fails to add tag options with invalid value length', () => { - const tagOptions = new servicecatalog.TagOptions({ - key1: ['value1'.repeat(1000), 'value2'], - key2: ['value1'], - }); - expect(() => { - portfolio.associateTagOptions(tagOptions); - }).toThrowError(/Invalid TagOption value for resource/); - }), - test('add tag update constraint', () => { portfolio.addProduct(product); portfolio.constrainTagUpdates(product, { diff --git a/packages/@aws-cdk/aws-servicecatalog/test/product.test.ts b/packages/@aws-cdk/aws-servicecatalog/test/product.test.ts index 0afc91ce86153..d27e18a1c3358 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/product.test.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/product.test.ts @@ -271,7 +271,7 @@ describe('Product', () => { productVersions: [], }); }).toThrowError(/Invalid product versions for resource Default\/MyProduct/); - }), + }); describe('adding and associating TagOptions to a product', () => { let product: servicecatalog.IProduct; @@ -286,12 +286,14 @@ describe('Product', () => { }, ], }); - }), + }); test('add tag options to product', () => { - const tagOptions = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], - key2: ['value1'], + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1'], + }, }); product.associateTagOptions(tagOptions); @@ -301,9 +303,11 @@ describe('Product', () => { }), test('add tag options as input to product in props', () => { - const tagOptions = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], - key2: ['value1'], + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1'], + }, }); new servicecatalog.CloudFormationProduct(stack, 'MyProductWithTagOptions', { @@ -321,44 +325,19 @@ describe('Product', () => { Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOptionAssociation', 3); }), - test('adding identical tag options to product is idempotent', () => { - const tagOptions1 = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], - key2: ['value1'], - }); - - const tagOptions2 = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], + test('adding tag options to product multiple times is idempotent', () => { + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1'], + }, }); - product.associateTagOptions(tagOptions1); - product.associateTagOptions(tagOptions2); // If not idempotent this would fail + product.associateTagOptions(tagOptions); + product.associateTagOptions(tagOptions); // If not idempotent this would fail Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOption', 3); //Generates a resource for each unique key-value pair Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOptionAssociation', 3); - }), - - test('adding duplicate tag options to portfolio and product creates unique tag options and enumerated associations', () => { - const tagOptions1 = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], - key2: ['value1'], - }); - - const tagOptions2 = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], - key2: ['value2'], - }); - - const portfolio = new servicecatalog.Portfolio(stack, 'MyPortfolio', { - displayName: 'testPortfolio', - providerName: 'testProvider', - }); - - portfolio.associateTagOptions(tagOptions1); - product.associateTagOptions(tagOptions2); // If not idempotent this would fail - - Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOption', 4); //Generates a resource for each unique key-value pair - Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOptionAssociation', 6); }); }); }); diff --git a/packages/@aws-cdk/aws-servicecatalog/test/tag-option.test.ts b/packages/@aws-cdk/aws-servicecatalog/test/tag-option.test.ts new file mode 100644 index 0000000000000..54e0464da467e --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalog/test/tag-option.test.ts @@ -0,0 +1,194 @@ +import { Template } from '@aws-cdk/assertions'; +import * as cdk from '@aws-cdk/core'; +import * as servicecatalog from '../lib'; + +describe('TagOptions', () => { + let app: cdk.App; + let stack: cdk.Stack; + + beforeEach(() => { + app = new cdk.App(); + stack = new cdk.Stack(app); + }); + + describe('creating tagOption(s)', () => { + test('default tagOptions creation', () => { + new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1', 'value2', 'value3'], + }, + }); + + Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOption', 5); + }), + + test('fails to create tag option with invalid minimum key length', () => { + expect(() => { + new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + '': ['value1', 'value2'], + }, + }); + }).toThrowError(/Invalid TagOption key for resource/); + }), + + test('fails to create tag option with invalid maxium key length', () => { + expect(() => { + new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + ['longKey'.repeat(1000)]: ['value1', 'value2'], + }, + }); + }).toThrowError(/Invalid TagOption key for resource/); + }), + + test('fails to create tag option with invalid value length', () => { + expect(() => { + new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key: ['tagOptionValue'.repeat(1000)], + }, + }); + }).toThrowError(/Invalid TagOption value for resource/); + }), + + test('fails to create tag options with no tag keys or values', () => { + expect(() => { + new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: {}, + }); + }).toThrowError(/No tag option keys or values were provided/); + }), + + test('fails to create tag options for tag key with no values', () => { + expect(() => { + new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: [], + }, + }); + }).toThrowError(/No tag option values were provided for tag option key/); + }), + + test('associate tag options', () => { + const portfolio = new servicecatalog.Portfolio(stack, 'MyPortfolio', { + displayName: 'testPortfolio', + providerName: 'testProvider', + }); + + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1', 'value2', 'value3'], + }, + }); + portfolio.associateTagOptions(tagOptions); + + Template.fromStack(stack).hasResource('AWS::ServiceCatalog::TagOption', 5); + Template.fromStack(stack).hasResource('AWS::ServiceCatalog::TagOptionAssociation', 5); + }), + + test('creating tag options with duplicate values is idempotent', () => { + const portfolio = new servicecatalog.Portfolio(stack, 'MyPortfolio', { + displayName: 'testPortfolio', + providerName: 'testProvider', + }); + + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2', 'value2'], + key2: ['value1', 'value2', 'value3', 'value3'], + }, + }); + portfolio.associateTagOptions(tagOptions); + + Template.fromStack(stack).hasResource('AWS::ServiceCatalog::TagOption', 5); + Template.fromStack(stack).hasResource('AWS::ServiceCatalog::TagOptionAssociation', 5); + }), + + test('create and associate tag options to different resources', () => { + const portfolio1 = new servicecatalog.Portfolio(stack, 'MyPortfolio1', { + displayName: 'testPortfolio1', + providerName: 'testProvider1', + }); + + const portfolio2 = new servicecatalog.Portfolio(stack, 'MyPortfolio2', { + displayName: 'testPortfolio2', + providerName: 'testProvider2', + }); + + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1', 'value2', 'value3'], + }, + }); + + portfolio1.associateTagOptions(tagOptions); + portfolio2.associateTagOptions(tagOptions); + + Template.fromStack(stack).hasResource('AWS::ServiceCatalog::TagOption', 5); + Template.fromStack(stack).hasResource('AWS::ServiceCatalog::TagOptionAssociation', 10); + }), + + test('adding tag options to portfolio and product creates unique tag options and enumerated associations', () => { + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1'], + }, + }); + + const portfolio = new servicecatalog.Portfolio(stack, 'MyPortfolio', { + displayName: 'testPortfolio', + providerName: 'testProvider', + }); + + const product = new servicecatalog.CloudFormationProduct(stack, 'MyProduct', { + productName: 'testProduct', + owner: 'testOwner', + productVersions: [ + { + cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromUrl('https://awsdocs.s3.amazonaws.com/servicecatalog/development-environment.template'), + }, + ], + tagOptions: tagOptions, + }); + + portfolio.associateTagOptions(tagOptions); + product.associateTagOptions(tagOptions); + + Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOption', 3); //Generates a resource for each unique key-value pair + Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOptionAssociation', 6); + }); + + test('create and associate tag options in another stack', () => { + const tagOptionsStack = new cdk.Stack(app, 'TagOptionsStack'); + const productStack = new cdk.Stack(app, 'ProductStack'); + + const tagOptions = new servicecatalog.TagOptions(tagOptionsStack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1', 'value2', 'value3'], + }, + }); + + new servicecatalog.CloudFormationProduct(productStack, 'MyProduct', { + productName: 'testProduct', + owner: 'testOwner', + productVersions: [ + { + cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromUrl('https://awsdocs.s3.amazonaws.com/servicecatalog/development-environment.template'), + }, + ], + tagOptions: tagOptions, + }); + + Template.fromStack(tagOptionsStack).hasResource('AWS::ServiceCatalog::TagOption', 5); + Template.fromStack(productStack).resourceCountIs('AWS::ServiceCatalog::TagOption', 0); + Template.fromStack(productStack).hasResource('AWS::ServiceCatalog::TagOptionAssociation', 5); + }); + }); +}); diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/README.md b/packages/@aws-cdk/aws-servicecatalogappregistry/README.md index e816724f30d3c..6fcee1d94bf9c 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/README.md +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/README.md @@ -54,8 +54,11 @@ An application that has been created outside of the stack can be imported into y Applications can be imported by their ARN via the `Application.fromApplicationArn()` API: ```ts -const importedApplication = appreg.Application.fromApplicationArn(this, 'MyImportedApplication', - 'arn:aws:servicecatalog:us-east-1:012345678910:/applications/0aqmvxvgmry0ecc4mjhwypun6i'); +const importedApplication = appreg.Application.fromApplicationArn( + this, + 'MyImportedApplication', + 'arn:aws:servicecatalog:us-east-1:012345678910:/applications/0aqmvxvgmry0ecc4mjhwypun6i', +); ``` ## Attribute Group @@ -84,8 +87,11 @@ An attribute group that has been created outside of the stack can be imported in Attribute groups can be imported by their ARN via the `AttributeGroup.fromAttributeGroupArn()` API: ```ts -const importedAttributeGroup = appreg.AttributeGroup.fromAttributeGroupArn(this, 'MyImportedAttrGroup', - 'arn:aws:servicecatalog:us-east-1:012345678910:/attribute-groups/0aqmvxvgmry0ecc4mjhwypun6i'); +const importedAttributeGroup = appreg.AttributeGroup.fromAttributeGroupArn( + this, + 'MyImportedAttrGroup', + 'arn:aws:servicecatalog:us-east-1:012345678910:/attribute-groups/0aqmvxvgmry0ecc4mjhwypun6i', +); ``` ## Associations @@ -101,7 +107,9 @@ CDK will fail at deploy time. You can associate an attribute group with an application with the `associateAttributeGroup()` API: -```ts basic-constructs +```ts +declare const application: appreg.Application; +declare const attributeGroup: appreg.AttributeGroup; application.associateAttributeGroup(attributeGroup); ``` @@ -109,8 +117,10 @@ application.associateAttributeGroup(attributeGroup); You can associate a stack with an application with the `associateStack()` API: -```ts basic-constructs -const myStack = new cdk.Stack(app, 'MyStack'); +```ts +const app = new App(); +const myStack = new Stack(app, 'MyStack'); +declare const application: appreg.Application; application.associateStack(myStack); ``` diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/package.json b/packages/@aws-cdk/aws-servicecatalogappregistry/package.json index 0da24cca81ef4..96e9d4dd7967c 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/package.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/package.json @@ -7,6 +7,13 @@ "jsii": { "outdir": "dist", "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + }, "targets": { "dotnet": { "namespace": "Amazon.CDK.AWS.ServiceCatalogAppRegistry", @@ -81,7 +88,7 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/rosetta/basic-constructs.ts-fixture b/packages/@aws-cdk/aws-servicecatalogappregistry/rosetta/basic-constructs.ts-fixture deleted file mode 100644 index 19ffd84abf486..0000000000000 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/rosetta/basic-constructs.ts-fixture +++ /dev/null @@ -1,22 +0,0 @@ -// Fixture with packages imported, but nothing else -import * as cdk from '@aws-cdk/core'; -import * as appreg from '@aws-cdk/aws-servicecatalogappregistry'; - -class Fixture extends cdk.Stack { - constructor(scope: Construct, id: string) { - super(scope, id); - - const application = new appreg.Application(stack, 'MyApplication', { - applicationName: 'MyApplication', - }); - - const attributeGroup = new appreg.AttributeGroup(stack, 'MyAttributeGroup', { - attributeGroupName: 'testAttributeGroup', - attributes: { - key: 'value', - }, - }); - - /// here - } -} diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-servicecatalogappregistry/rosetta/default.ts-fixture index 61dfd75923e1e..45174ea8d63ed 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/rosetta/default.ts-fixture @@ -1,8 +1,9 @@ // Fixture with packages imported, but nothing else -import * as cdk from '@aws-cdk/core'; +import { App, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; import * as appreg from '@aws-cdk/aws-servicecatalogappregistry'; -class Fixture extends cdk.Stack { +class Fixture extends Stack { constructor(scope: Construct, id: string) { super(scope, id); diff --git a/packages/@aws-cdk/aws-servicediscovery/package.json b/packages/@aws-cdk/aws-servicediscovery/package.json index 283783e207c93..98567a421aa13 100644 --- a/packages/@aws-cdk/aws-servicediscovery/package.json +++ b/packages/@aws-cdk/aws-servicediscovery/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -75,13 +82,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-ec2": "0.0.0", @@ -156,7 +163,9 @@ "docs-public-apis:@aws-cdk/aws-servicediscovery.PrivateDnsNamespaceAttributes", "docs-public-apis:@aws-cdk/aws-servicediscovery.PrivateDnsNamespaceProps", "docs-public-apis:@aws-cdk/aws-servicediscovery.PublicDnsNamespaceAttributes", - "docs-public-apis:@aws-cdk/aws-servicediscovery.PublicDnsNamespaceProps" + "docs-public-apis:@aws-cdk/aws-servicediscovery.PublicDnsNamespaceProps", + "resource-attribute:@aws-cdk/aws-servicediscovery.PrivateDnsNamespace.privateDnsNamespaceHostedZoneId", + "resource-attribute:@aws-cdk/aws-servicediscovery.PublicDnsNamespace.publicDnsNamespaceHostedZoneId" ] }, "stability": "stable", diff --git a/packages/@aws-cdk/aws-servicediscovery/test/instance.test.ts b/packages/@aws-cdk/aws-servicediscovery/test/instance.test.ts index 89ee3a81f4bfa..0fb472c1b1130 100644 --- a/packages/@aws-cdk/aws-servicediscovery/test/instance.test.ts +++ b/packages/@aws-cdk/aws-servicediscovery/test/instance.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as cdk from '@aws-cdk/core'; @@ -24,7 +24,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::ServiceDiscovery::Instance', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::Instance', { InstanceAttributes: { AWS_INSTANCE_IPV4: '10.0.0.0', AWS_INSTANCE_IPV6: '0:0:0:0:0:ffff:a00:0', @@ -62,7 +62,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::ServiceDiscovery::Instance', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::Instance', { InstanceAttributes: { AWS_INSTANCE_IPV4: '54.239.25.192', AWS_INSTANCE_IPV6: '0:0:0:0:0:ffff:a00:0', @@ -102,7 +102,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::ServiceDiscovery::Instance', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::Instance', { InstanceAttributes: { AWS_INSTANCE_IPV4: '10.0.0.0', AWS_INSTANCE_IPV6: '0:0:0:0:0:ffff:a00:0', @@ -256,7 +256,7 @@ describe('instance', () => { service.registerLoadBalancer('Loadbalancer', alb, customAttributes); // THEN - expect(stack).toHaveResource('AWS::ServiceDiscovery::Instance', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::Instance', { InstanceAttributes: { AWS_ALIAS_DNS_NAME: { 'Fn::GetAtt': [ @@ -343,7 +343,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::ServiceDiscovery::Instance', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::Instance', { InstanceAttributes: { AWS_INSTANCE_CNAME: 'foo.com', dogs: 'good', @@ -397,7 +397,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::ServiceDiscovery::Instance', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::Instance', { InstanceAttributes: { dogs: 'good', }, @@ -494,7 +494,7 @@ describe('instance', () => { }); // THEN - expect(stack).toCountResources('AWS::ServiceDiscovery::Instance', 2); + Template.fromStack(stack).resourceCountIs('AWS::ServiceDiscovery::Instance', 2); }); diff --git a/packages/@aws-cdk/aws-servicediscovery/test/namespace.test.ts b/packages/@aws-cdk/aws-servicediscovery/test/namespace.test.ts index d0c70b057534b..30ccf20fc85f1 100644 --- a/packages/@aws-cdk/aws-servicediscovery/test/namespace.test.ts +++ b/packages/@aws-cdk/aws-servicediscovery/test/namespace.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as servicediscovery from '../lib'; @@ -11,7 +11,7 @@ describe('namespace', () => { name: 'foobar.com', }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyNamespaceD0BB8558: { Type: 'AWS::ServiceDiscovery::HttpNamespace', @@ -32,7 +32,7 @@ describe('namespace', () => { name: 'foobar.com', }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyNamespaceD0BB8558: { Type: 'AWS::ServiceDiscovery::PublicDnsNamespace', @@ -55,7 +55,7 @@ describe('namespace', () => { vpc, }); - expect(stack).toHaveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::PrivateDnsNamespace', { Name: 'foobar.com', Vpc: { Ref: 'MyVpcF9F0CA6F', diff --git a/packages/@aws-cdk/aws-servicediscovery/test/service.test.ts b/packages/@aws-cdk/aws-servicediscovery/test/service.test.ts index 44c5ba3e97c6b..556bc6c2c7260 100644 --- a/packages/@aws-cdk/aws-servicediscovery/test/service.test.ts +++ b/packages/@aws-cdk/aws-servicediscovery/test/service.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as servicediscovery from '../lib'; @@ -21,7 +21,7 @@ describe('service', () => { }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyNamespaceD0BB8558: { Type: 'AWS::ServiceDiscovery::HttpNamespace', @@ -69,7 +69,7 @@ describe('service', () => { }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyNamespaceD0BB8558: { Type: 'AWS::ServiceDiscovery::HttpNamespace', @@ -118,7 +118,7 @@ describe('service', () => { }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyNamespaceD0BB8558: { Type: 'AWS::ServiceDiscovery::PublicDnsNamespace', @@ -176,7 +176,7 @@ describe('service', () => { }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyNamespaceD0BB8558: { Type: 'AWS::ServiceDiscovery::PublicDnsNamespace', @@ -233,7 +233,7 @@ describe('service', () => { }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyNamespaceD0BB8558: { Type: 'AWS::ServiceDiscovery::PublicDnsNamespace', @@ -417,11 +417,11 @@ describe('service', () => { }); // THEN - expect(stack).toHaveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::PrivateDnsNamespace', { Name: 'private', }); - expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { + Template.fromStack(stack).hasResourceProperties('AWS::ServiceDiscovery::Service', { Description: 'service description', DnsConfig: { DnsRecords: [ diff --git a/packages/@aws-cdk/aws-ses-actions/package.json b/packages/@aws-cdk/aws-ses-actions/package.json index 0a975238c1252..92c00efabcbcb 100644 --- a/packages/@aws-cdk/aws-ses-actions/package.json +++ b/packages/@aws-cdk/aws-ses-actions/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -65,13 +72,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-iam": "0.0.0", diff --git a/packages/@aws-cdk/aws-ses-actions/test/actions.test.ts b/packages/@aws-cdk/aws-ses-actions/test/actions.test.ts index 4a425fd51f76b..845f99c963aa7 100644 --- a/packages/@aws-cdk/aws-ses-actions/test/actions.test.ts +++ b/packages/@aws-cdk/aws-ses-actions/test/actions.test.ts @@ -1,5 +1,4 @@ -import { arrayWith, ResourcePart } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as kms from '@aws-cdk/aws-kms'; import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; @@ -25,7 +24,7 @@ test('add header action', () => { value: 'value', })); - expect(stack).toHaveResource('AWS::SES::ReceiptRule', { + Template.fromStack(stack).hasResourceProperties('AWS::SES::ReceiptRule', { Rule: { Actions: [ { @@ -62,7 +61,7 @@ test('add bounce action', () => { topic, })); - expect(stack).toHaveResource('AWS::SES::ReceiptRule', { + Template.fromStack(stack).hasResourceProperties('AWS::SES::ReceiptRule', { Rule: { Actions: [ { @@ -95,7 +94,7 @@ test('add lambda action', () => { topic, })); - expect(stack).toHaveResource('AWS::SES::ReceiptRule', { + Template.fromStack(stack).hasResource('AWS::SES::ReceiptRule', { Properties: { Rule: { Actions: [ @@ -123,9 +122,9 @@ test('add lambda action', () => { DependsOn: [ 'FunctionAllowSes1829904A', ], - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', FunctionName: { 'Fn::GetAtt': [ @@ -151,7 +150,7 @@ test('add s3 action', () => { topic, })); - expect(stack).toHaveResource('AWS::SES::ReceiptRule', { + Template.fromStack(stack).hasResource('AWS::SES::ReceiptRule', { Properties: { Rule: { Actions: [ @@ -182,9 +181,9 @@ test('add s3 action', () => { DependsOn: [ 'BucketPolicyE9A3008A', ], - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('AWS::S3::BucketPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { Bucket: { Ref: 'Bucket83908E77', }, @@ -223,9 +222,9 @@ test('add s3 action', () => { }, }); - expect(stack).toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Action: [ 'kms:Encrypt', 'kms:GenerateDataKey', @@ -246,7 +245,7 @@ test('add s3 action', () => { Service: 'ses.amazonaws.com', }, Resource: '*', - }), + }]), }, }); }); @@ -257,7 +256,7 @@ test('add sns action', () => { topic, })); - expect(stack).toHaveResource('AWS::SES::ReceiptRule', { + Template.fromStack(stack).hasResourceProperties('AWS::SES::ReceiptRule', { Rule: { Actions: [ { @@ -279,7 +278,7 @@ test('add stop action', () => { topic, })); - expect(stack).toHaveResource('AWS::SES::ReceiptRule', { + Template.fromStack(stack).hasResourceProperties('AWS::SES::ReceiptRule', { Rule: { Actions: [ { diff --git a/packages/@aws-cdk/aws-ses/README.md b/packages/@aws-cdk/aws-ses/README.md index b2d36c9785175..04fc9f2e26202 100644 --- a/packages/@aws-cdk/aws-ses/README.md +++ b/packages/@aws-cdk/aws-ses/README.md @@ -20,56 +20,58 @@ Create a receipt rule set with rules and actions (actions can be found in the ```ts import * as s3 from '@aws-cdk/aws-s3'; -import * as ses from '@aws-cdk/aws-ses'; import * as actions from '@aws-cdk/aws-ses-actions'; -import * as sns from '@aws-cdk/aws-sns'; -const bucket = new s3.Bucket(stack, 'Bucket'); -const topic = new sns.Topic(stack, 'Topic'); +const bucket = new s3.Bucket(this, 'Bucket'); +const topic = new sns.Topic(this, 'Topic'); -new ses.ReceiptRuleSet(stack, 'RuleSet', { +new ses.ReceiptRuleSet(this, 'RuleSet', { rules: [ { recipients: ['hello@aws.com'], actions: [ new actions.AddHeader({ name: 'X-Special-Header', - value: 'aws' + value: 'aws', }), new actions.S3({ bucket, objectKeyPrefix: 'emails/', - topic - }) + topic, + }), ], }, { recipients: ['aws.com'], actions: [ new actions.Sns({ - topic - }) - ] - } - ] + topic, + }), + ], + }, + ], }); ``` Alternatively, rules can be added to a rule set: ```ts -const ruleSet = new ses.ReceiptRuleSet(this, 'RuleSet'): +const ruleSet = new ses.ReceiptRuleSet(this, 'RuleSet'); const awsRule = ruleSet.addRule('Aws', { - recipients: ['aws.com'] + recipients: ['aws.com'], }); ``` And actions to rules: ```ts +import * as actions from '@aws-cdk/aws-ses-actions'; + +declare const awsRule: ses.ReceiptRule; +declare const topic: sns.Topic; awsRule.addAction(new actions.Sns({ - topic + topic, })); ``` @@ -81,7 +83,7 @@ A rule to drop spam can be added by setting `dropSpam` to `true`: ```ts new ses.ReceiptRuleSet(this, 'RuleSet', { - dropSpam: true + dropSpam: true, }); ``` @@ -94,8 +96,8 @@ Create a receipt filter: ```ts new ses.ReceiptFilter(this, 'Filter', { - ip: '1.2.3.4/16' // Will be blocked -}) + ip: '1.2.3.4/16', // Will be blocked +}); ``` An allow list filter is also available: @@ -105,7 +107,7 @@ new ses.AllowListReceiptFilter(this, 'AllowList', { ips: [ '10.0.0.0/16', '1.2.3.4/16', - ] + ], }); ``` diff --git a/packages/@aws-cdk/aws-ses/package.json b/packages/@aws-cdk/aws-ses/package.json index 611d336d3dbfa..45d5852e16554 100644 --- a/packages/@aws-cdk/aws-ses/package.json +++ b/packages/@aws-cdk/aws-ses/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -72,14 +79,14 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/aws-lambda": "^8.10.89", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/aws-lambda": "^8.10.92", + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-iam": "0.0.0", diff --git a/packages/@aws-cdk/aws-ses/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-ses/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..f86757d5fe6f4 --- /dev/null +++ b/packages/@aws-cdk/aws-ses/rosetta/default.ts-fixture @@ -0,0 +1,12 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as ses from '@aws-cdk/aws-ses'; +import * as sns from '@aws-cdk/aws-sns'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ses/test/receipt-filter.test.ts b/packages/@aws-cdk/aws-ses/test/receipt-filter.test.ts index 69e966555d0a9..74e6dc8742340 100644 --- a/packages/@aws-cdk/aws-ses/test/receipt-filter.test.ts +++ b/packages/@aws-cdk/aws-ses/test/receipt-filter.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import { AllowListReceiptFilter, ReceiptFilter, ReceiptFilterPolicy } from '../lib'; @@ -17,7 +17,7 @@ describe('receipt filter', () => { }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'FilterC907D6DA': { 'Type': 'AWS::SES::ReceiptFilter', @@ -50,7 +50,7 @@ describe('receipt filter', () => { }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'AllowListBlockAll094C9B97': { 'Type': 'AWS::SES::ReceiptFilter', diff --git a/packages/@aws-cdk/aws-ses/test/receipt-rule-set.test.ts b/packages/@aws-cdk/aws-ses/test/receipt-rule-set.test.ts index a80886c442144..0386b71f39425 100644 --- a/packages/@aws-cdk/aws-ses/test/receipt-rule-set.test.ts +++ b/packages/@aws-cdk/aws-ses/test/receipt-rule-set.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import { ReceiptRuleSet } from '../lib'; @@ -15,7 +15,7 @@ describe('receipt rule set', () => { }); // THEN - expect(stack).toHaveResource('AWS::SES::ReceiptRuleSet', { + Template.fromStack(stack).hasResourceProperties('AWS::SES::ReceiptRuleSet', { RuleSetName: 'MyRuleSet', }); @@ -32,7 +32,7 @@ describe('receipt rule set', () => { }); // THEN - expect(stack).toHaveResource('AWS::SES::ReceiptRule', { + Template.fromStack(stack).hasResourceProperties('AWS::SES::ReceiptRule', { Rule: { Actions: [ { @@ -52,7 +52,7 @@ describe('receipt rule set', () => { }, }); - expect(stack).toHaveResource('AWS::Lambda::Function'); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); }); @@ -73,7 +73,7 @@ describe('receipt rule set', () => { }); // THEN - expect(stack).toHaveResource('AWS::SES::ReceiptRule', { + Template.fromStack(stack).hasResourceProperties('AWS::SES::ReceiptRule', { Rule: { Enabled: true, Recipients: [ @@ -87,7 +87,7 @@ describe('receipt rule set', () => { }, }); - expect(stack).toHaveResource('AWS::Lambda::Function'); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 1); }); @@ -102,7 +102,7 @@ describe('receipt rule set', () => { receiptRuleSet.addRule('MyRule'); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'ImportedRuleSetMyRule53EE2F7F': { 'Type': 'AWS::SES::ReceiptRule', diff --git a/packages/@aws-cdk/aws-ses/test/receipt-rule.test.ts b/packages/@aws-cdk/aws-ses/test/receipt-rule.test.ts index 4cb86efe88b72..d8a3805cee858 100644 --- a/packages/@aws-cdk/aws-ses/test/receipt-rule.test.ts +++ b/packages/@aws-cdk/aws-ses/test/receipt-rule.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import { ReceiptRule, ReceiptRuleSet, TlsPolicy } from '../lib'; @@ -26,7 +26,7 @@ describe('receipt rule', () => { }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'RuleSetE30C6C48': { 'Type': 'AWS::SES::ReceiptRuleSet', @@ -82,7 +82,7 @@ describe('receipt rule', () => { }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'RuleSetE30C6C48': { 'Type': 'AWS::SES::ReceiptRuleSet', @@ -124,7 +124,7 @@ describe('receipt rule', () => { }); // THEN - expect(stack).toHaveResource('AWS::SES::ReceiptRule', { + Template.fromStack(stack).hasResourceProperties('AWS::SES::ReceiptRule', { 'Rule': { 'Actions': [ { @@ -159,7 +159,7 @@ describe('receipt rule', () => { }); // THEN - expect(stack).toHaveResource('AWS::SES::ReceiptRule', { + Template.fromStack(stack).hasResourceProperties('AWS::SES::ReceiptRule', { 'Rule': { 'Actions': [ { diff --git a/packages/@aws-cdk/aws-signer/README.md b/packages/@aws-cdk/aws-signer/README.md index 2de7797eb344f..6c95da4668e7a 100644 --- a/packages/@aws-cdk/aws-signer/README.md +++ b/packages/@aws-cdk/aws-signer/README.md @@ -27,7 +27,7 @@ to sign a zip file. For more information go to [Signing Platforms in AWS Signer] AWS Signer provides a pre-defined set of signing platforms. They are available in the CDK as - -```ts +```text Platform.AWS_IOT_DEVICE_MANAGEMENT_SHA256_ECDSA Platform.AWS_LAMBDA_SHA384_ECDSA Platform.AMAZON_FREE_RTOS_TI_CC3220SF @@ -43,11 +43,9 @@ For more information, visit [Signing Profiles in AWS Signer](https://docs.aws.am The following code sets up a signing profile for signing lambda code bundles - ```ts -import * as signer from '@aws-cdk/aws-signer'; - const signingProfile = new signer.SigningProfile(this, 'SigningProfile', { platform: signer.Platform.AWS_LAMBDA_SHA384_ECDSA, -} ); +}); ``` A signing profile is valid by default for 135 months. This can be modified by specifying the `signatureValidityPeriod` property. diff --git a/packages/@aws-cdk/aws-signer/package.json b/packages/@aws-cdk/aws-signer/package.json index e0c3a66833452..0bc0f4f9d9522 100644 --- a/packages/@aws-cdk/aws-signer/package.json +++ b/packages/@aws-cdk/aws-signer/package.json @@ -7,6 +7,13 @@ "jsii": { "outdir": "dist", "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + }, "targets": { "dotnet": { "namespace": "Amazon.CDK.AWS.Signer", @@ -74,11 +81,11 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-signer/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-signer/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..443e1e88b241a --- /dev/null +++ b/packages/@aws-cdk/aws-signer/rosetta/default.ts-fixture @@ -0,0 +1,11 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as signer from '@aws-cdk/aws-signer'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} diff --git a/packages/@aws-cdk/aws-signer/test/signing-profile.test.ts b/packages/@aws-cdk/aws-signer/test/signing-profile.test.ts index 49ada5da2f596..44db183b565a5 100644 --- a/packages/@aws-cdk/aws-signer/test/signing-profile.test.ts +++ b/packages/@aws-cdk/aws-signer/test/signing-profile.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as signer from '../lib'; @@ -14,7 +14,7 @@ describe('signing profile', () => { const platform = signer.Platform.AWS_LAMBDA_SHA384_ECDSA; new signer.SigningProfile( stack, 'SigningProfile', { platform } ); - expect(stack).toHaveResource('AWS::Signer::SigningProfile', { + Template.fromStack(stack).hasResourceProperties('AWS::Signer::SigningProfile', { PlatformId: platform.platformId, SignatureValidityPeriod: { Type: 'MONTHS', @@ -30,7 +30,7 @@ describe('signing profile', () => { signatureValidity: cdk.Duration.days( 7 ), } ); - expect(stack).toHaveResource('AWS::Signer::SigningProfile', { + Template.fromStack(stack).hasResourceProperties('AWS::Signer::SigningProfile', { PlatformId: platform.platformId, SignatureValidityPeriod: { Type: 'DAYS', @@ -47,7 +47,7 @@ describe('signing profile', () => { cdk.Tags.of(signing).add('tag2', 'value2'); cdk.Tags.of(signing).add('tag3', ''); - expect(stack).toHaveResource('AWS::Signer::SigningProfile', { + Template.fromStack(stack).hasResourceProperties('AWS::Signer::SigningProfile', { PlatformId: platform.platformId, SignatureValidityPeriod: { Type: 'MONTHS', @@ -109,7 +109,7 @@ describe('signing profile', () => { ], ], }); - expect(stack).toMatchTemplate({}); + Template.fromStack(stack).templateMatches({}); }); } ); }); diff --git a/packages/@aws-cdk/aws-sns-subscriptions/package.json b/packages/@aws-cdk/aws-sns-subscriptions/package.json index 4ebe2e25b6a60..f295805a8b389 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/package.json +++ b/packages/@aws-cdk/aws-sns-subscriptions/package.json @@ -71,13 +71,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-iam": "0.0.0", diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts b/packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts index 671937a3ed01e..546fe9a3c6dae 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as kms from '@aws-cdk/aws-kms'; import * as lambda from '@aws-cdk/aws-lambda'; import * as sns from '@aws-cdk/aws-sns'; @@ -22,7 +22,7 @@ beforeEach(() => { test('url subscription', () => { topic.addSubscription(new subs.UrlSubscription('https://foobar.com/')); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyTopic86869434': { 'Type': 'AWS::SNS::Topic', @@ -54,7 +54,7 @@ test('url subscription with user provided dlq', () => { deadLetterQueue: dlQueue, })); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyTopic86869434': { 'Type': 'AWS::SNS::Topic', @@ -134,7 +134,7 @@ test('url subscription (with raw delivery)', () => { rawMessageDelivery: true, })); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyTopic86869434': { 'Type': 'AWS::SNS::Topic', @@ -160,7 +160,7 @@ test('url subscription (unresolved url with protocol)', () => { const urlToken = Token.asString({ Ref: 'my-url-1' }); topic.addSubscription(new subs.UrlSubscription(urlToken, { protocol: sns.SubscriptionProtocol.HTTPS })); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyTopic86869434': { 'Type': 'AWS::SNS::Topic', @@ -190,7 +190,7 @@ test('url subscription (double unresolved url with protocol)', () => { topic.addSubscription(new subs.UrlSubscription(urlToken1, { protocol: sns.SubscriptionProtocol.HTTPS })); topic.addSubscription(new subs.UrlSubscription(urlToken2, { protocol: sns.SubscriptionProtocol.HTTPS })); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyTopic86869434': { 'Type': 'AWS::SNS::Topic', @@ -240,7 +240,7 @@ test('queue subscription', () => { topic.addSubscription(new subs.SqsSubscription(queue)); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyTopic86869434': { 'Type': 'AWS::SNS::Topic', @@ -332,7 +332,7 @@ test('queue subscription cross region', () => { topic1.addSubscription(new subs.SqsSubscription(queue)); - expect(topicStack).toMatchTemplate({ + Template.fromStack(topicStack).templateMatches({ 'Resources': { 'TopicBFC7AF6E': { 'Type': 'AWS::SNS::Topic', @@ -344,7 +344,7 @@ test('queue subscription cross region', () => { }, }); - expect(queueStack).toMatchTemplate({ + Template.fromStack(queueStack).templateMatches({ 'Resources': { 'MyQueueE6CA6235': { 'Type': 'AWS::SQS::Queue', @@ -438,7 +438,7 @@ test('queue subscription cross region, env agnostic', () => { topic1.addSubscription(new subs.SqsSubscription(queue)); - expect(topicStack).toMatchTemplate({ + Template.fromStack(topicStack).templateMatches({ 'Resources': { 'TopicBFC7AF6E': { 'Type': 'AWS::SNS::Topic', @@ -460,7 +460,7 @@ test('queue subscription cross region, env agnostic', () => { }, }); - expect(queueStack).toMatchTemplate({ + Template.fromStack(queueStack).templateMatches({ 'Resources': { 'MyQueueE6CA6235': { 'Type': 'AWS::SQS::Queue', @@ -540,7 +540,7 @@ test('queue subscription cross region, topic env agnostic', () => { topic1.addSubscription(new subs.SqsSubscription(queue)); - expect(topicStack).toMatchTemplate({ + Template.fromStack(topicStack).templateMatches({ 'Resources': { 'TopicBFC7AF6E': { 'Type': 'AWS::SNS::Topic', @@ -552,7 +552,7 @@ test('queue subscription cross region, topic env agnostic', () => { }, }); - expect(queueStack).toMatchTemplate({ + Template.fromStack(queueStack).templateMatches({ 'Resources': { 'MyQueueE6CA6235': { 'Type': 'AWS::SQS::Queue', @@ -666,7 +666,7 @@ test('queue subscription cross region, queue env agnostic', () => { topic1.addSubscription(new subs.SqsSubscription(queue)); - expect(topicStack).toMatchTemplate({ + Template.fromStack(topicStack).templateMatches({ 'Resources': { 'TopicBFC7AF6E': { 'Type': 'AWS::SNS::Topic', @@ -678,7 +678,7 @@ test('queue subscription cross region, queue env agnostic', () => { }, }); - expect(queueStack).toMatchTemplate({ + Template.fromStack(queueStack).templateMatches({ 'Resources': { 'MyQueueE6CA6235': { 'Type': 'AWS::SQS::Queue', @@ -768,7 +768,7 @@ test('queue subscription with user provided dlq', () => { deadLetterQueue: dlQueue, })); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyTopic86869434': { 'Type': 'AWS::SNS::Topic', @@ -893,7 +893,7 @@ test('queue subscription (with raw delivery)', () => { topic.addSubscription(new subs.SqsSubscription(queue, { rawMessageDelivery: true })); - expect(stack).toHaveResource('AWS::SNS::Subscription', { + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { 'Endpoint': { 'Fn::GetAtt': [ 'MyQueueE6CA6235', @@ -920,7 +920,7 @@ test('encrypted queue subscription', () => { topic.addSubscription(new subs.SqsSubscription(queue)); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyTopic86869434': { 'Type': 'AWS::SNS::Topic', @@ -1067,7 +1067,7 @@ test('lambda subscription', () => { topic.addSubscription(new subs.LambdaSubscription(fction)); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyTopic86869434': { 'Type': 'AWS::SNS::Topic', @@ -1178,7 +1178,7 @@ test('lambda subscription, cross region env agnostic', () => { topic1.addSubscription(new subs.LambdaSubscription(fction)); - expect(lambdaStack).toMatchTemplate({ + Template.fromStack(lambdaStack).templateMatches({ 'Resources': { 'MyFuncServiceRole54065130': { 'Type': 'AWS::IAM::Role', @@ -1292,7 +1292,7 @@ test('lambda subscription, cross region', () => { topic1.addSubscription(new subs.LambdaSubscription(fction)); - expect(lambdaStack).toMatchTemplate({ + Template.fromStack(lambdaStack).templateMatches({ 'Resources': { 'MyFuncServiceRole54065130': { 'Type': 'AWS::IAM::Role', @@ -1401,7 +1401,7 @@ test('lambda subscription, cross region', () => { test('email subscription', () => { topic.addSubscription(new subs.EmailSubscription('foo@bar.com')); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyTopic86869434': { 'Type': 'AWS::SNS::Topic', @@ -1428,7 +1428,7 @@ test('email subscription with unresolved', () => { const emailToken = Token.asString({ Ref: 'my-email-1' }); topic.addSubscription(new subs.EmailSubscription(emailToken)); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyTopic86869434': { 'Type': 'AWS::SNS::Topic', @@ -1459,7 +1459,7 @@ test('email and url subscriptions with unresolved', () => { topic.addSubscription(new subs.EmailSubscription(emailToken)); topic.addSubscription(new subs.UrlSubscription(urlToken, { protocol: sns.SubscriptionProtocol.HTTPS })); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyTopic86869434': { 'Type': 'AWS::SNS::Topic', @@ -1507,7 +1507,7 @@ test('email and url subscriptions with unresolved - four subscriptions', () => { topic.addSubscription(new subs.EmailSubscription(emailToken3)); topic.addSubscription(new subs.EmailSubscription(emailToken4)); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyTopic86869434': { 'Type': 'AWS::SNS::Topic', @@ -1579,7 +1579,7 @@ test('multiple subscriptions', () => { topic.addSubscription(new subs.SqsSubscription(queue)); topic.addSubscription(new subs.LambdaSubscription(func)); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyTopic86869434': { 'Type': 'AWS::SNS::Topic', @@ -1759,7 +1759,7 @@ test('with filter policy', () => { }, })); - expect(stack).toHaveResource('AWS::SNS::Subscription', { + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { 'FilterPolicy': { 'color': [ 'red', @@ -1797,7 +1797,7 @@ test('region property is present on an imported topic - sqs', () => { const queue = new sqs.Queue(stack, 'myqueue'); imported.addSubscription(new subs.SqsSubscription(queue)); - expect(stack).toHaveResource('AWS::SNS::Subscription', { + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { Region: 'us-east-1', }); }); @@ -1808,7 +1808,7 @@ test('region property on an imported topic as a parameter - sqs', () => { const queue = new sqs.Queue(stack, 'myqueue'); imported.addSubscription(new subs.SqsSubscription(queue)); - expect(stack).toHaveResource('AWS::SNS::Subscription', { + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { Region: { 'Fn::Select': [3, { 'Fn::Split': [':', { 'Ref': 'topicArn' }] }], }, @@ -1824,7 +1824,7 @@ test('region property is present on an imported topic - lambda', () => { }); imported.addSubscription(new subs.LambdaSubscription(func)); - expect(stack).toHaveResource('AWS::SNS::Subscription', { + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { Region: 'us-east-1', }); }); @@ -1839,7 +1839,7 @@ test('region property on an imported topic as a parameter - lambda', () => { }); imported.addSubscription(new subs.LambdaSubscription(func)); - expect(stack).toHaveResource('AWS::SNS::Subscription', { + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { Region: { 'Fn::Select': [3, { 'Fn::Split': [':', { 'Ref': 'topicArn' }] }], }, @@ -1849,7 +1849,7 @@ test('region property on an imported topic as a parameter - lambda', () => { test('sms subscription', () => { topic.addSubscription(new subs.SmsSubscription('+15551231234')); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyTopic86869434': { 'Type': 'AWS::SNS::Topic', @@ -1876,7 +1876,7 @@ test('sms subscription with unresolved', () => { const smsToken = Token.asString({ Ref: 'my-sms-1' }); topic.addSubscription(new subs.SmsSubscription(smsToken)); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyTopic86869434': { 'Type': 'AWS::SNS::Topic', diff --git a/packages/@aws-cdk/aws-sns/lib/topic-base.ts b/packages/@aws-cdk/aws-sns/lib/topic-base.ts index b867670474177..70dff8b8572df 100644 --- a/packages/@aws-cdk/aws-sns/lib/topic-base.ts +++ b/packages/@aws-cdk/aws-sns/lib/topic-base.ts @@ -28,6 +28,13 @@ export interface ITopic extends IResource, notifications.INotificationRuleTarget */ readonly topicName: string; + /** + * Whether this topic is an Amazon SNS FIFO queue. If false, this is a standard topic. + * + * @attribute + */ + readonly fifo: boolean; + /** * Subscribe some endpoint to this topic */ @@ -56,6 +63,8 @@ export abstract class TopicBase extends Resource implements ITopic { public abstract readonly topicName: string; + public abstract readonly fifo: boolean; + /** * Controls automatic creation of policy objects. * diff --git a/packages/@aws-cdk/aws-sns/lib/topic.ts b/packages/@aws-cdk/aws-sns/lib/topic.ts index aa78dcfee6b80..0525e832181a2 100644 --- a/packages/@aws-cdk/aws-sns/lib/topic.ts +++ b/packages/@aws-cdk/aws-sns/lib/topic.ts @@ -64,6 +64,7 @@ export class Topic extends TopicBase { class Import extends TopicBase { public readonly topicArn = topicArn; public readonly topicName = Stack.of(scope).splitArn(topicArn, ArnFormat.NO_RESOURCE_NAME).resource; + public readonly fifo = this.topicName.endsWith('.fifo'); protected autoCreatePolicy: boolean = false; } @@ -72,6 +73,7 @@ export class Topic extends TopicBase { public readonly topicArn: string; public readonly topicName: string; + public readonly fifo: boolean; protected readonly autoCreatePolicy: boolean = true; @@ -110,5 +112,6 @@ export class Topic extends TopicBase { resource: this.physicalName, }); this.topicName = this.getResourceNameAttribute(resource.attrTopicName); + this.fifo = props.fifo || false; } } diff --git a/packages/@aws-cdk/aws-sns/package.json b/packages/@aws-cdk/aws-sns/package.json index c95bb45ab42a5..b1664ed712223 100644 --- a/packages/@aws-cdk/aws-sns/package.json +++ b/packages/@aws-cdk/aws-sns/package.json @@ -88,8 +88,8 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-cloudwatch": "0.0.0", diff --git a/packages/@aws-cdk/aws-sns/test/sns.test.ts b/packages/@aws-cdk/aws-sns/test/sns.test.ts index 950aef6cbcef1..c39f6d709587b 100644 --- a/packages/@aws-cdk/aws-sns/test/sns.test.ts +++ b/packages/@aws-cdk/aws-sns/test/sns.test.ts @@ -343,9 +343,23 @@ describe('Topic', () => { // THEN expect(imported.topicName).toEqual('my_corporate_topic'); expect(imported.topicArn).toEqual('arn:aws:sns:*:123456789012:my_corporate_topic'); + expect(imported.fifo).toEqual(false); }); + test('fromTopicArn fifo', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const imported = sns.Topic.fromTopicArn(stack, 'Imported', 'arn:aws:sns:*:123456789012:mytopic.fifo'); + + // THEN + expect(imported.topicName).toEqual('mytopic.fifo'); + expect(imported.topicArn).toEqual('arn:aws:sns:*:123456789012:mytopic.fifo'); + expect(imported.fifo).toEqual(true); + }); + test('test metrics', () => { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-sqs/package.json b/packages/@aws-cdk/aws-sqs/package.json index 8292ff02fbbcc..a6b12eaf10285 100644 --- a/packages/@aws-cdk/aws-sqs/package.json +++ b/packages/@aws-cdk/aws-sqs/package.json @@ -79,15 +79,15 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "aws-sdk": "^2.848.0", - "jest": "^27.4.5" + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-cloudwatch": "0.0.0", diff --git a/packages/@aws-cdk/aws-sqs/test/sqs.test.ts b/packages/@aws-cdk/aws-sqs/test/sqs.test.ts index b444220caf198..6a4d5fda6c02c 100644 --- a/packages/@aws-cdk/aws-sqs/test/sqs.test.ts +++ b/packages/@aws-cdk/aws-sqs/test/sqs.test.ts @@ -1,5 +1,4 @@ -import { ResourcePart } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import { CfnParameter, Duration, Stack, App, Token } from '@aws-cdk/core'; @@ -13,7 +12,7 @@ test('default properties', () => { expect(q.fifo).toEqual(false); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'Queue4A7E3555': { 'Type': 'AWS::SQS::Queue', @@ -23,9 +22,9 @@ test('default properties', () => { }, }); - expect(stack).toHaveResource('AWS::SQS::Queue', { + Template.fromStack(stack).hasResource('AWS::SQS::Queue', { DeletionPolicy: 'Delete', - }, ResourcePart.CompleteDefinition); + }); }); test('with a dead letter queue', () => { @@ -34,7 +33,7 @@ test('with a dead letter queue', () => { const dlqProps = { queue: dlq, maxReceiveCount: 3 }; const queue = new sqs.Queue(stack, 'Queue', { deadLetterQueue: dlqProps }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'DLQ581697C4': { 'Type': 'AWS::SQS::Queue', @@ -91,7 +90,7 @@ test('message retention period can be provided as a parameter', () => { }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Parameters': { 'myretentionperiod': { 'Type': 'Number', @@ -122,7 +121,7 @@ test('addToPolicy will automatically create a policy for this queue', () => { principals: [new iam.ArnPrincipal('arn')], })); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyQueueE6CA6235': { 'Type': 'AWS::SQS::Queue', @@ -323,7 +322,7 @@ describe('grants', () => { queue.grantPurge(user); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -350,7 +349,7 @@ describe('queue encryption', () => { const queue = new sqs.Queue(stack, 'Queue', { encryptionMasterKey: key }); expect(queue.encryptionMasterKey).toEqual(key); - expect(stack).toHaveResource('AWS::SQS::Queue', { + Template.fromStack(stack).hasResourceProperties('AWS::SQS::Queue', { 'KmsMasterKeyId': { 'Fn::GetAtt': ['CustomKey1E6D0D07', 'Arn'] }, }); }); @@ -360,8 +359,8 @@ describe('queue encryption', () => { new sqs.Queue(stack, 'Queue', { encryption: sqs.QueueEncryption.KMS }); - expect(stack).toHaveResource('AWS::KMS::Key'); - expect(stack).toHaveResource('AWS::SQS::Queue', { + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', Match.anyValue()); + Template.fromStack(stack).hasResourceProperties('AWS::SQS::Queue', { 'KmsMasterKeyId': { 'Fn::GetAtt': [ 'QueueKey39FCBAE6', @@ -375,7 +374,7 @@ describe('queue encryption', () => { const stack = new Stack(); new sqs.Queue(stack, 'Queue', { encryption: sqs.QueueEncryption.KMS_MANAGED }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'Queue4A7E3555': { 'Type': 'AWS::SQS::Queue', @@ -403,7 +402,7 @@ describe('queue encryption', () => { queue.grantSendMessages(role); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -440,7 +439,7 @@ test('test ".fifo" suffixed queues register as fifo', () => { expect(queue.fifo).toEqual(true); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'Queue4A7E3555': { 'Type': 'AWS::SQS::Queue', @@ -463,7 +462,7 @@ test('test a fifo queue is observed when the "fifo" property is specified', () = expect(queue.fifo).toEqual(true); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'Queue4A7E3555': { 'Type': 'AWS::SQS::Queue', @@ -486,7 +485,7 @@ test('test a fifo queue is observed when high throughput properties are specifie }); expect(queue.fifo).toEqual(true); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'Queue4A7E3555': { 'Type': 'AWS::SQS::Queue', @@ -584,7 +583,7 @@ function testGrant(action: (q: sqs.Queue, principal: iam.IPrincipal) => void, .. action(queue, principal); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { diff --git a/packages/@aws-cdk/aws-ssm/package.json b/packages/@aws-cdk/aws-ssm/package.json index 3f3d6df07852c..65caf4f4c5873 100644 --- a/packages/@aws-cdk/aws-ssm/package.json +++ b/packages/@aws-cdk/aws-ssm/package.json @@ -79,13 +79,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-iam": "0.0.0", diff --git a/packages/@aws-cdk/aws-ssm/test/parameter-store-string.test.ts b/packages/@aws-cdk/aws-ssm/test/parameter-store-string.test.ts index a8ce58f22fded..d9da8a0697535 100644 --- a/packages/@aws-cdk/aws-ssm/test/parameter-store-string.test.ts +++ b/packages/@aws-cdk/aws-ssm/test/parameter-store-string.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as ssm from '../lib'; @@ -26,7 +26,7 @@ test('can reference SSMPS string - latest version', () => { }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Parameters: { RefParameter: { Type: 'AWS::SSM::Parameter::Value', diff --git a/packages/@aws-cdk/aws-ssm/test/parameter.test.ts b/packages/@aws-cdk/aws-ssm/test/parameter.test.ts index 272a477523623..48594d1e3ee41 100644 --- a/packages/@aws-cdk/aws-ssm/test/parameter.test.ts +++ b/packages/@aws-cdk/aws-ssm/test/parameter.test.ts @@ -1,6 +1,6 @@ /* eslint-disable max-len */ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as cdk from '@aws-cdk/core'; @@ -19,7 +19,7 @@ test('creating a String SSM Parameter', () => { }); // THEN - expect(stack).toHaveResource('AWS::SSM::Parameter', { + Template.fromStack(stack).hasResourceProperties('AWS::SSM::Parameter', { AllowedPattern: '.*', Description: 'The value Foo', Name: 'FooParameter', @@ -50,7 +50,7 @@ test('dataType can be specified', () => { }); // THEN - expect(stack).toHaveResource('AWS::SSM::Parameter', { + Template.fromStack(stack).hasResourceProperties('AWS::SSM::Parameter', { Value: 'myValue', DataType: 'aws:ec2:image', }); @@ -70,7 +70,7 @@ test('expect String SSM Parameter to have tier properly set', () => { }); // THEN - expect(stack).toHaveResource('AWS::SSM::Parameter', { + Template.fromStack(stack).hasResourceProperties('AWS::SSM::Parameter', { Tier: 'Advanced', }); }); @@ -110,7 +110,7 @@ test('creating a StringList SSM Parameter', () => { }); // THEN - expect(stack).toHaveResource('AWS::SSM::Parameter', { + Template.fromStack(stack).hasResourceProperties('AWS::SSM::Parameter', { AllowedPattern: '(Foo|Bar)', Description: 'The values Foo and Bar', Name: 'FooParameter', @@ -340,7 +340,7 @@ test('StringParameter.fromStringParameterName', () => { expect(stack.resolve(param.parameterName)).toEqual('MyParamName'); expect(stack.resolve(param.parameterType)).toEqual('String'); expect(stack.resolve(param.stringValue)).toEqual({ Ref: 'MyParamNameParameter' }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Parameters: { MyParamNameParameter: { Type: 'AWS::SSM::Parameter::Value', @@ -487,7 +487,7 @@ test('StringParameter.fromSecureStringParameterAttributes with encryption key cr param.grantRead(role); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -547,7 +547,7 @@ test('StringParameter.fromSecureStringParameterAttributes with encryption key cr param.grantWrite(role); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -658,7 +658,7 @@ describe('valueForStringParameter', () => { const value = ssm.StringParameter.valueForStringParameter(stack, 'my-param-name'); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Parameters: { SsmParameterValuemyparamnameC96584B6F00A464EAD1953AFF4B05118Parameter: { Type: 'AWS::SSM::Parameter::Value', @@ -680,7 +680,7 @@ describe('valueForStringParameter', () => { ssm.StringParameter.valueForStringParameter(stack, 'my-param-name'); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Parameters: { SsmParameterValuemyparamnameC96584B6F00A464EAD1953AFF4B05118Parameter: { Type: 'AWS::SSM::Parameter::Value', diff --git a/packages/@aws-cdk/aws-ssm/test/ssm-document.test.ts b/packages/@aws-cdk/aws-ssm/test/ssm-document.test.ts index 36f6f54c566e6..ba57c2ed0e4a3 100644 --- a/packages/@aws-cdk/aws-ssm/test/ssm-document.test.ts +++ b/packages/@aws-cdk/aws-ssm/test/ssm-document.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as ssm from '../lib'; @@ -16,11 +16,11 @@ test('association name is rendered properly in L1 construct', () => { }); // THEN - expect(stack).to(haveResource('AWS::SSM::Association', { + Template.fromStack(stack).hasResourceProperties('AWS::SSM::Association', { Name: 'document', Parameters: { a: ['a'], B: [], }, - })); + }); }); diff --git a/packages/@aws-cdk/aws-ssmcontacts/package.json b/packages/@aws-cdk/aws-ssmcontacts/package.json index a886fc2903395..bfce82c5f9d0a 100644 --- a/packages/@aws-cdk/aws-ssmcontacts/package.json +++ b/packages/@aws-cdk/aws-ssmcontacts/package.json @@ -87,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-ssmincidents/package.json b/packages/@aws-cdk/aws-ssmincidents/package.json index 537f57269969d..f014c6fc4ef38 100644 --- a/packages/@aws-cdk/aws-ssmincidents/package.json +++ b/packages/@aws-cdk/aws-ssmincidents/package.json @@ -87,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-sso/package.json b/packages/@aws-cdk/aws-sso/package.json index 2f24ecd575f36..349b3f7ef235a 100644 --- a/packages/@aws-cdk/aws-sso/package.json +++ b/packages/@aws-cdk/aws-sso/package.json @@ -85,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 59adc3517e704..ad4c2a365c38b 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -208,6 +208,7 @@ and invokes it asynchronously. ```ts declare const fn: lambda.Function; + const submitJob = new tasks.LambdaInvoke(this, 'Invoke Handler', { lambdaFunction: fn, payload: sfn.TaskInput.fromJsonPathAt('$.input'), @@ -215,12 +216,12 @@ const submitJob = new tasks.LambdaInvoke(this, 'Invoke Handler', { }); ``` -You can also use [intrinsic functions](https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html) with `JsonPath.stringAt()`. +You can also use [intrinsic functions](https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html) available on `JsonPath`, for example `JsonPath.format()`. Here is an example of starting an Athena query that is dynamically created using the task input: ```ts const startQueryExecutionJob = new tasks.AthenaStartQueryExecution(this, 'Athena Start Query', { - queryString: sfn.JsonPath.stringAt("States.Format('select contacts where year={};', $.year)"), + queryString: sfn.JsonPath.format('select contacts where year={};', sfn.JsonPath.stringAt('$.year')), queryExecutionContext: { databaseName: 'interactions', }, @@ -305,6 +306,25 @@ const invokeTask = new tasks.CallApiGatewayRestApiEndpoint(this, 'Call REST API' }); ``` +Be aware that the header values must be arrays. When passing the Task Token +in the headers field `WAIT_FOR_TASK_TOKEN` integration, use +`JsonPath.array()` to wrap the token in an array: + +```ts +import * as apigateway from '@aws-cdk/aws-apigateway'; +declare const api: apigateway.RestApi; + +new tasks.CallApiGatewayRestApiEndpoint(this, 'Endpoint', { + api, + stageName: 'Stage', + method: tasks.HttpMethod.PUT, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + headers: sfn.TaskInput.fromObject({ + TaskToken: sfn.JsonPath.array(sfn.JsonPath.taskToken), + }), +}); +``` + ### Call HTTP API Endpoint The `CallApiGatewayHttpApiEndpoint` calls the HTTP API endpoint. @@ -798,7 +818,7 @@ The service integration APIs correspond to Amazon EMR on EKS APIs, but differ in ### Create Virtual Cluster -The [CreateVirtualCluster](https://docs.aws.amazon.com/emr-on-eks/latest/APIReference/API_CreateVirtualCluster.html) API creates a single virtual cluster that's mapped to a single Kubernetes namespace. +The [CreateVirtualCluster](https://docs.aws.amazon.com/emr-on-eks/latest/APIReference/API_CreateVirtualCluster.html) API creates a single virtual cluster that's mapped to a single Kubernetes namespace. The EKS cluster containing the Kubernetes namespace where the virtual cluster will be mapped can be passed in from the task input. @@ -1147,6 +1167,8 @@ If your training job or model uses resources from AWS Marketplace, [network isolation is required](https://docs.aws.amazon.com/sagemaker/latest/dg/mkt-algo-model-internet-free.html). To do so, set the `enableNetworkIsolation` property to `true` for `SageMakerCreateModel` or `SageMakerCreateTrainingJob`. +To set environment variables for the Docker container use the `environment` property. + ### Create Training Job You can call the [`CreateTrainingJob`](https://docs.aws.amazon.com/sagemaker/latest/dg/API_CreateTrainingJob.html) API from a `Task` state. diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-rest-api.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-rest-api.ts index 0352777e9c06a..153331cb6dafb 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-rest-api.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-rest-api.ts @@ -24,6 +24,25 @@ export interface CallApiGatewayRestApiEndpointProps extends CallApiGatewayEndpoi /** * Call REST API endpoint as a Task * + * Be aware that the header values must be arrays. When passing the Task Token + * in the headers field `WAIT_FOR_TASK_TOKEN` integration, use + * `JsonPath.array()` to wrap the token in an array: + * + * ```ts + * import * as apigateway from '@aws-cdk/aws-apigateway'; + * declare const api: apigateway.RestApi; + * + * new tasks.CallApiGatewayRestApiEndpoint(this, 'Endpoint', { + * api, + * stageName: 'Stage', + * method: tasks.HttpMethod.PUT, + * integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + * headers: sfn.TaskInput.fromObject({ + * TaskToken: sfn.JsonPath.array(sfn.JsonPath.taskToken), + * }), + * }); + * ``` + * * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html */ export class CallApiGatewayRestApiEndpoint extends CallApiGatewayEndpointBase { 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 4cac7c7180bde..e223988059c52 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 @@ -202,11 +202,11 @@ export class EmrCreateCluster extends sfn.TaskStateBase { this.taskPolicies = this.createPolicyStatements(this._serviceRole, this._clusterRole, this._autoScalingRole); - if (this.props.releaseLabel !== undefined) { + if (this.props.releaseLabel !== undefined && !cdk.Token.isUnresolved(this.props.releaseLabel)) { this.validateReleaseLabel(this.props.releaseLabel); } - if (this.props.stepConcurrencyLevel !== undefined) { + if (this.props.stepConcurrencyLevel !== undefined && !cdk.Token.isUnresolved(this.props.stepConcurrencyLevel)) { if (this.props.stepConcurrencyLevel < 1 || this.props.stepConcurrencyLevel > 256) { throw new Error(`Step concurrency level must be in range [1, 256], but got ${this.props.stepConcurrencyLevel}.`); } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts index 64680dc357747..ba3274579eb37 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts @@ -5,7 +5,7 @@ import { Duration, Lazy, Size, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; import { AlgorithmSpecification, Channel, InputMode, OutputDataConfig, ResourceConfig, S3DataType, StoppingCondition, VpcConfig } from './base-types'; -import { renderTags } from './private/utils'; +import { renderEnvironment, renderTags } from './private/utils'; /** * Properties for creating an Amazon SageMaker training job @@ -85,6 +85,13 @@ export interface SageMakerCreateTrainingJobProps extends sfn.TaskStateBaseProps * @default - No VPC */ readonly vpcConfig?: VpcConfig; + + /** + * Environment variables to set in the Docker container. + * + * @default - No environment variables + */ + readonly environment?: { [key: string]: string }; } /** @@ -234,6 +241,7 @@ export class SageMakerCreateTrainingJob extends sfn.TaskStateBase implements iam ...this.renderHyperparameters(this.props.hyperparameters), ...renderTags(this.props.tags), ...this.renderVpcConfig(this.props.vpcConfig), + ...renderEnvironment(this.props.environment), }; } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-transform-job.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-transform-job.ts index cc108c45e4439..84c08fa22529d 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-transform-job.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-transform-job.ts @@ -5,7 +5,7 @@ import { Size, Stack, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; import { BatchStrategy, ModelClientOptions, S3DataType, TransformInput, TransformOutput, TransformResources } from './base-types'; -import { renderTags } from './private/utils'; +import { renderEnvironment, renderTags } from './private/utils'; /** * Properties for creating an Amazon SageMaker transform job task @@ -166,7 +166,7 @@ export class SageMakerCreateTransformJob extends sfn.TaskStateBase { private renderParameters(): { [key: string]: any } { return { ...(this.props.batchStrategy ? { BatchStrategy: this.props.batchStrategy } : {}), - ...this.renderEnvironment(this.props.environment), + ...renderEnvironment(this.props.environment), ...(this.props.maxConcurrentTransforms ? { MaxConcurrentTransforms: this.props.maxConcurrentTransforms } : {}), ...(this.props.maxPayload ? { MaxPayloadInMB: this.props.maxPayload.toMebibytes() } : {}), ...this.props.modelClientOptions ? this.renderModelClientOptions(this.props.modelClientOptions) : {}, @@ -234,10 +234,6 @@ export class SageMakerCreateTransformJob extends sfn.TaskStateBase { }; } - private renderEnvironment(environment: { [key: string]: any } | undefined): { [key: string]: any } { - return environment ? { Environment: environment } : {}; - } - private makePolicyStatements(): iam.PolicyStatement[] { const stack = Stack.of(this); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/private/utils.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/private/utils.ts index e308fd890864c..bbcaed118a083 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/private/utils.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/private/utils.ts @@ -1,4 +1,7 @@ - export function renderTags(tags: { [key: string]: any } | undefined): { [key: string]: any } { return tags ? { Tags: Object.keys(tags).map((key) => ({ Key: key, Value: tags[key] })) } : {}; -} \ No newline at end of file +} + +export function renderEnvironment(environment: { [key: string]: any } | undefined): { [key: string]: any } { + return environment ? { Environment: environment } : {}; +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json index fb4c8d45dd7fc..0bbda5a9263c7 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json @@ -90,8 +90,8 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-apigateway": "0.0.0", diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-rest-api.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-rest-api.test.ts index 37a083fb2cc95..f47ca69e0dc0a 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-rest-api.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-rest-api.test.ts @@ -69,7 +69,7 @@ describe('CallApiGatewayRestApiEndpoint', () => { method: HttpMethod.GET, stageName: 'dev', integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, - headers: sfn.TaskInput.fromObject({ TaskToken: sfn.JsonPath.taskToken }), + headers: sfn.TaskInput.fromObject({ TaskToken: sfn.JsonPath.array(sfn.JsonPath.taskToken) }), }); // THEN @@ -97,7 +97,7 @@ describe('CallApiGatewayRestApiEndpoint', () => { }, AuthType: 'NO_AUTH', Headers: { - 'TaskToken.$': '$$.Task.Token', + 'TaskToken.$': 'States.Array($$.Task.Token)', }, Method: 'GET', Stage: 'dev', 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 3d846ec1d06d2..67a745bd16e2a 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 @@ -34,6 +34,14 @@ beforeEach(() => { ); }); +test('Create Cluster with an unresolved release label', () => { + new EmrCreateCluster(stack, 'Task', { + instances: {}, + name: 'Cluster', + releaseLabel: cdk.Token.asString({}), + }); +}); + test('Create Cluster with FIRE_AND_FORGET integrationPattern', () => { // WHEN const task = new EmrCreateCluster(stack, 'Task', { @@ -194,6 +202,27 @@ describe('Cluster with StepConcurrencyLevel', () => { }); }); + test('can be set dynamically through JsonPath', async () => { + // WHEN + const task = new EmrCreateCluster(stack, 'Task', { + instances: {}, + clusterRole, + name: 'Cluster', + serviceRole, + autoScalingRole, + stepConcurrencyLevel: sfn.JsonPath.numberAt('$.foo.bar'), + integrationPattern: sfn.IntegrationPattern.REQUEST_RESPONSE, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toMatchObject({ + Parameters: { + 'Name': 'Cluster', + 'StepConcurrencyLevel.$': '$.foo.bar', + }, + }); + }); + test('throws if < 1 or > 256', async () => { expect(() => new EmrCreateCluster(stack, 'Task1', { instances: {}, diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emrcontainers/integ.job-submission-workflow.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emrcontainers/integ.job-submission-workflow.expected.json index ec1551086d626..ad853185040b4 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emrcontainers/integ.job-submission-workflow.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emrcontainers/integ.job-submission-workflow.expected.json @@ -1342,6 +1342,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts index 7e7a5eff26348..9d5ebb1f9711a 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts @@ -192,6 +192,9 @@ test('create complex training job', () => { vpcConfig: { vpc, }, + environment: { + SOMEVAR: 'myvalue', + }, }); trainTask.addSecurityGroup(securityGroup); @@ -285,6 +288,9 @@ test('create complex training job', () => { { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, ], }, + Environment: { + SOMEVAR: 'myvalue', + }, }, }); }); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.call-sagemaker.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.call-sagemaker.expected.json index 107fd16780144..942f72ba3d41b 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.call-sagemaker.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.call-sagemaker.expected.json @@ -148,6 +148,10 @@ "Action": [ "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.expected.json index 0cd3166d73138..3e069a953f03a 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.expected.json @@ -148,6 +148,10 @@ "Action": [ "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index e74367df84676..94129f56a38f9 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -95,6 +95,51 @@ properly (for example, permissions to invoke any Lambda functions you add to your workflow). A role will be created by default, but you can supply an existing one as well. +## Accessing State (the JsonPath class) + +Every State Machine execution has [State Machine +Data](https://docs.aws.amazon.com/step-functions/latest/dg/concepts-state-machine-data.html): +a JSON document containing keys and values that is fed into the state machine, +gets modified as the state machine progresses, and finally is produced as output. + +You can pass fragments of this State Machine Data into Tasks of the state machine. +To do so, use the static methods on the `JsonPath` class. For example, to pass +the value that's in the data key of `OrderId` to a Lambda function as you invoke +it, use `JsonPath.stringAt('$.OrderId')`, like so: + +```ts +import * as lambda from '@aws-cdk/aws-lambda'; + +declare const orderFn: lambda.Function; + +const submitJob = new tasks.LambdaInvoke(this, 'InvokeOrderProcessor', { + lambdaFunction: orderFn, + payload: sfn.TaskInput.fromObject({ + OrderId: sfn.JsonPath.stringAt('$.OrderId'), + }), +}); +``` + +The following methods are available: + +| Method | Purpose | +|--------|---------| +| `JsonPath.stringAt('$.Field')` | reference a field, return the type as a `string`. | +| `JsonPath.listAt('$.Field')` | reference a field, return the type as a list of strings. | +| `JsonPath.numberAt('$.Field')` | reference a field, return the type as a number. Use this for functions that expect a number argument. | +| `JsonPath.objectAt('$.Field')` | reference a field, return the type as an `IResolvable`. Use this for functions that expect an object argument. | +| `JsonPath.entirePayload` | reference the entire data object (equivalent to a path of `$`). | +| `JsonPath.taskToken` | reference the [Task Token](https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html#connect-wait-token), used for integration patterns that need to run for a long time. | + +You can also call [intrinsic functions](https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html) using the methods on `JsonPath`: + +| Method | Purpose | +|--------|---------| +| `JsonPath.array(JsonPath.stringAt('$.Field'), ...)` | make an array from other elements. | +| `JsonPath.format('The value is {}.', JsonPath.stringAt('$.Value'))` | insert elements into a format string. | +| `JsonPath.stringToJson(JsonPath.stringAt('$.ObjStr'))` | parse a JSON string to an object | +| `JsonPath.jsonToString(JsonPath.objectAt('$.Obj'))` | stringify an object to a JSON string | + ## Amazon States Language This library comes with a set of classes that model the [Amazon States @@ -485,9 +530,9 @@ The class `StateMachineFragment` contains some helper functions (like machine as a subclass of this, it will be convenient to use: ```ts nofixture -import { Construct, Stack } from '@aws-cdk/core'; +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; interface MyJobProps { jobFlavor: string; @@ -515,10 +560,14 @@ class MyStack extends Stack { constructor(scope: Construct, id: string) { super(scope, id); // Do 3 different variants of MyJob in parallel - new sfn.Parallel(this, 'All jobs') + const parallel = new sfn.Parallel(this, 'All jobs') .branch(new MyJob(this, 'Quick', { jobFlavor: 'quick' }).prefixStates()) .branch(new MyJob(this, 'Medium', { jobFlavor: 'medium' }).prefixStates()) .branch(new MyJob(this, 'Slow', { jobFlavor: 'slow' }).prefixStates()); + + new sfn.StateMachine(this, 'MyStateMachine', { + definition: parallel, + }); } } ``` @@ -599,8 +648,8 @@ new cloudwatch.Alarm(this, 'ThrottledAlarm', { ## Error names -Step Functions identifies errors in the Amazon States Language using case-sensitive strings, known as error names. -The Amazon States Language defines a set of built-in strings that name well-known errors, all beginning with the `States.` prefix. +Step Functions identifies errors in the Amazon States Language using case-sensitive strings, known as error names. +The Amazon States Language defines a set of built-in strings that name well-known errors, all beginning with the `States.` prefix. * `States.ALL` - A wildcard that matches any known error name. * `States.Runtime` - An execution failed due to some exception that could not be processed. Often these are caused by errors at runtime, such as attempting to apply InputPath or OutputPath on a null JSON payload. A `States.Runtime` error is not retriable, and will always cause the execution to fail. A retry or catch on `States.ALL` will NOT catch States.Runtime errors. diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/fields.ts b/packages/@aws-cdk/aws-stepfunctions/lib/fields.ts index 990a2542d4fea..e78c026293798 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/fields.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/fields.ts @@ -1,5 +1,5 @@ -import { Token } from '@aws-cdk/core'; -import { findReferencedPaths, jsonPathString, JsonPathToken, renderObject } from './json-path'; +import { Token, IResolvable } from '@aws-cdk/core'; +import { findReferencedPaths, jsonPathString, JsonPathToken, renderObject, renderInExpression, jsonPathFromAny } from './private/json-path'; /** * Extract a field from the State Machine data or context @@ -38,6 +38,14 @@ export class JsonPath { return Token.asNumber(new JsonPathToken(path)); } + /** + * Reference a complete (complex) object in a JSON path location + */ + public static objectAt(path: string): IResolvable { + validateJsonPath(path); + return new JsonPathToken(path); + } + /** * Use the entire data structure * @@ -78,6 +86,82 @@ export class JsonPath { return new JsonPathToken('$$').toString(); } + /** + * Make an intrinsic States.Array expression + * + * Combine any number of string literals or JsonPath expressions into an array. + * + * Use this function if the value of an array element directly has to come + * from a JSON Path expression (either the State object or the Context object). + * + * If the array contains object literals whose values come from a JSON path + * expression, you do not need to use this function. + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html + */ + public static array(...values: string[]): string { + return new JsonPathToken(`States.Array(${values.map(renderInExpression).join(', ')})`).toString(); + } + + /** + * Make an intrinsic States.Format expression + * + * This can be used to embed JSON Path variables inside a format string. + * + * For example: + * + * ```ts + * sfn.JsonPath.format('Hello, my name is {}.', sfn.JsonPath.stringAt('$.name')) + * ``` + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html + */ + public static format(formatString: string, ...values: string[]): string { + const allArgs = [formatString, ...values]; + return new JsonPathToken(`States.Format(${allArgs.map(renderInExpression).join(', ')})`).toString(); + } + + /** + * Make an intrinsic States.StringToJson expression + * + * During the execution of the Step Functions state machine, parse the given + * argument as JSON into its object form. + * + * For example: + * + * ```ts + * sfn.JsonPath.stringToJson(sfn.JsonPath.stringAt('$.someJsonBody')) + * ``` + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html + */ + public static stringToJson(jsonString: string): IResolvable { + return new JsonPathToken(`States.StringToJson(${renderInExpression(jsonString)})`); + } + + /** + * Make an intrinsic States.JsonToString expression + * + * During the execution of the Step Functions state machine, encode the + * given object into a JSON string. + * + * For example: + * + * ```ts + * sfn.JsonPath.jsonToString(sfn.JsonPath.objectAt('$.someObject')) + * ``` + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html + */ + public static jsonToString(value: any): string { + const path = jsonPathFromAny(value); + if (!path) { + throw new Error('Argument to JsonPath.jsonToString() must be a JsonPath object'); + } + + return new JsonPathToken(`States.JsonToString(${path})`).toString(); + } + private constructor() {} } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/private/intrinstics.ts b/packages/@aws-cdk/aws-stepfunctions/lib/private/intrinstics.ts new file mode 100644 index 0000000000000..f10c81ffb115b --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/private/intrinstics.ts @@ -0,0 +1,262 @@ +export type IntrinsicExpression = StringLiteralExpression | PathExpression | FnCallExpression; +export type TopLevelIntrinsic = PathExpression | FnCallExpression; + +export interface StringLiteralExpression { + readonly type: 'string-literal'; + readonly literal: string; +} + +export interface PathExpression { + readonly type: 'path'; + readonly path: string; +} + +export interface FnCallExpression { + readonly type: 'fncall'; + readonly functionName: string; + readonly arguments: IntrinsicExpression[]; +} + + +/** + * LL(1) parser for StepFunctions intrinsics + * + * The parser implements a state machine over a cursor into an expression + * string. The cusor gets moved, the character at the cursor gets inspected + * and based on the character we accumulate some value and potentially move + * to a different state. + * + * Literal strings are not allowed at the top level, but are allowed inside + * function calls. + */ +export class IntrinsicParser { + private i: number = 0; + + constructor(private readonly expression: string) { + } + + public parseTopLevelIntrinsic(): TopLevelIntrinsic { + this.ws(); + + let ret; + if (this.char() === '$') { + ret = this.parsePath(); + } else if (isAlphaNum(this.char())) { + ret = this.parseFnCall(); + } else { + this.raiseError("expected '$' or a function call"); + } + + this.ws(); + + if (!this.eof) { + this.raiseError('unexpected trailing characters'); + } + + return ret; + } + + private parseIntrinsic(): IntrinsicExpression { + this.ws(); + + if (this.char() === '$') { + return this.parsePath(); + } + + if (isAlphaNum(this.char())) { + return this.parseFnCall(); + } + + if (this.char() === "'") { + return this.parseStringLiteral(); + } + + this.raiseError('expected $, function or single-quoted string'); + } + + /** + * Simplified path parsing + * + * JSON path can actually be quite complicated, but we don't need to validate + * it precisely. We just need to know how far it extends. + * + * Therefore, we only care about: + * + * - Starts with a $ + * - Accept ., $ and alphanums + * - Accept single-quoted strings ('...') + * - Accept anything between matched square brackets ([...]) + */ + private parsePath(): PathExpression { + const pathString = new Array(); + if (this.char() !== '$') { + this.raiseError('expected \'$\''); + } + pathString.push(this.consume()); + + let done = false; + while (!done && !this.eof) { + switch (this.char()) { + case '.': + case '$': + pathString.push(this.consume()); + break; + case "'": + const { quoted } = this.consumeQuotedString(); + pathString.push(quoted); + break; + + case '[': + pathString.push(this.consumeBracketedExpression(']')); + break; + + default: + if (isAlphaNum(this.char())) { + pathString.push(this.consume()); + break; + } + + // Not alphanum, end of path expression + done = true; + } + } + + return { type: 'path', path: pathString.join('') }; + } + + /** + * Parse a fncall + * + * Cursor should be on call identifier. Afterwards, cursor will be on closing + * quote. + */ + private parseFnCall(): FnCallExpression { + const name = new Array(); + while (this.char() !== '(') { + name.push(this.consume()); + } + + this.next(); // Consume the '(' + this.ws(); + + const args = []; + while (this.char() !== ')') { + args.push(this.parseIntrinsic()); + this.ws(); + + if (this.char() === ',') { + this.next(); + continue; + } else if (this.char() === ')') { + continue; + } else { + this.raiseError('expected , or )'); + } + } + this.next(); // Consume ')' + + return { + type: 'fncall', + arguments: args, + functionName: name.join(''), + }; + } + + /** + * Parse a string literal + * + * Cursor is expected to be on the first opening quote. Afterwards, + * cursor will be after the closing quote. + */ + private parseStringLiteral(): StringLiteralExpression { + const { unquoted } = this.consumeQuotedString(); + return { type: 'string-literal', literal: unquoted }; + } + + /** + * Parse a bracketed expression + * + * Cursor is expected to be on the opening brace. Afterwards, + * the cursor will be after the closing brace. + */ + private consumeBracketedExpression(closingBrace: string): string { + const ret = new Array(); + ret.push(this.consume()); + while (this.char() !== closingBrace) { + if (this.char() === '[') { + ret.push(this.consumeBracketedExpression(']')); + } else if (this.char() === '{') { + ret.push(this.consumeBracketedExpression('}')); + } else { + ret.push(this.consume()); + } + } + ret.push(this.consume()); + return ret.join(''); + } + + /** + * Parse a string literal + * + * Cursor is expected to be on the first opening quote. Afterwards, + * cursor will be after the closing quote. + */ + private consumeQuotedString(): { readonly quoted: string; unquoted: string } { + const quoted = new Array(); + const unquoted = new Array(); + + quoted.push(this.consume()); + while (this.char() !== "'") { + if (this.char() === '\\') { + // Advance and add next character literally, whatever it is + quoted.push(this.consume()); + } + quoted.push(this.char()); + unquoted.push(this.char()); + this.next(); + } + quoted.push(this.consume()); + return { quoted: quoted.join(''), unquoted: unquoted.join('') }; + } + + /** + * Consume whitespace if it exists + * + * Move the cursor to the next non-whitespace character. + */ + private ws() { + while (!this.eof && [' ', '\t', '\n'].includes(this.char())) { + this.next(); + } + } + + private get eof() { + return this.i >= this.expression.length; + } + + private char(): string { + if (this.eof) { + this.raiseError('unexpected end of string'); + } + + return this.expression[this.i]; + } + + private next() { + this.i++; + } + + private consume() { + const ret = this.char(); + this.next(); + return ret; + } + + private raiseError(message: string): never { + throw new Error(`Invalid JSONPath expression: ${message} at index ${this.i} in ${JSON.stringify(this.expression)}`); + } +} + +function isAlphaNum(x: string) { + return x.match(/^[a-zA-Z0-9]$/); +} diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/json-path.ts b/packages/@aws-cdk/aws-stepfunctions/lib/private/json-path.ts similarity index 70% rename from packages/@aws-cdk/aws-stepfunctions/lib/json-path.ts rename to packages/@aws-cdk/aws-stepfunctions/lib/private/json-path.ts index b4602b5e887d0..7263da227261f 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/json-path.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/private/json-path.ts @@ -1,4 +1,5 @@ import { captureStackTrace, IResolvable, IResolveContext, Token, Tokenization } from '@aws-cdk/core'; +import { IntrinsicParser, IntrinsicExpression } from './intrinstics'; const JSON_PATH_TOKEN_SYMBOL = Symbol.for('@aws-cdk/aws-stepfunctions.JsonPathToken'); @@ -38,6 +39,7 @@ export function renderObject(obj: object | undefined): object | undefined { handleList: renderStringList, handleNumber: renderNumber, handleBoolean: renderBoolean, + handleResolvable: renderResolvable, }); } @@ -49,36 +51,79 @@ export function findReferencedPaths(obj: object | undefined): Set { recurseObject(obj, { handleString(_key: string, x: string) { - const path = jsonPathString(x); - if (path !== undefined) { found.add(path); } + for (const p of findPathsInIntrinsicFunctions(jsonPathString(x))) { + found.add(p); + } return {}; }, handleList(_key: string, x: string[]) { - const path = jsonPathStringList(x); - if (path !== undefined) { found.add(path); } + for (const p of findPathsInIntrinsicFunctions(jsonPathStringList(x))) { + found.add(p); + } return {}; }, handleNumber(_key: string, x: number) { - const path = jsonPathNumber(x); - if (path !== undefined) { found.add(path); } + for (const p of findPathsInIntrinsicFunctions(jsonPathNumber(x))) { + found.add(p); + } return {}; }, handleBoolean(_key: string, _x: boolean) { return {}; }, + + handleResolvable(_key: string, x: IResolvable) { + for (const p of findPathsInIntrinsicFunctions(jsonPathFromAny(x))) { + found.add(p); + } + return {}; + }, }); return found; } +/** + * From an expression, return the list of JSON paths referenced in it + */ +function findPathsInIntrinsicFunctions(expression?: string): string[] { + if (!expression) { return []; } + + const ret = new Array(); + + try { + const parsed = new IntrinsicParser(expression).parseTopLevelIntrinsic(); + recurse(parsed); + return ret; + } catch (e) { + // Not sure that our parsing is 100% correct. We don't want to break anyone, so + // fall back to legacy behavior if we can't parse this string. + return [expression]; + } + + function recurse(p: IntrinsicExpression) { + switch (p.type) { + case 'path': + ret.push(p.path); + break; + + case 'fncall': + for (const arg of p.arguments) { + recurse(arg); + } + } + } +} + interface FieldHandlers { handleString(key: string, x: string): {[key: string]: string}; handleList(key: string, x: string[]): {[key: string]: string[] | string }; handleNumber(key: string, x: number): {[key: string]: number | string}; handleBoolean(key: string, x: boolean): {[key: string]: boolean}; + handleResolvable(key: string, x: IResolvable): {[key: string]: any}; } export function recurseObject(obj: object | undefined, handlers: FieldHandlers, visited: object[] = []): object | undefined { @@ -108,7 +153,11 @@ export function recurseObject(obj: object | undefined, handlers: FieldHandlers, } else if (value === null || value === undefined) { // Nothing } else if (typeof value === 'object') { - ret[key] = recurseObject(value, handlers, visited); + if (Tokenization.isResolvable(value)) { + Object.assign(ret, handlers.handleResolvable(key, value)); + } else { + ret[key] = recurseObject(value, handlers, visited); + } } } @@ -166,6 +215,21 @@ function renderString(key: string, value: string): {[key: string]: string} { } } +/** + * Render a resolvable + * + * If we can extract a Path from it, render as a path string, otherwise as itself (will + * be resolved later + */ +function renderResolvable(key: string, value: IResolvable): {[key: string]: any} { + const path = jsonPathFromAny(value); + if (path !== undefined) { + return { [key + '.$']: path }; + } else { + return { [key]: value }; + } +} + /** * Render a parameter string list * @@ -219,6 +283,12 @@ export function jsonPathString(x: string): string | undefined { return undefined; } +export function jsonPathFromAny(x: any) { + if (!x) { return undefined; } + if (typeof x === 'string') { return jsonPathString(x); } + return pathFromToken(Tokenization.reverse(x)); +} + /** * If the indicated string list is an encoded JSON path, return the path * @@ -240,3 +310,35 @@ function jsonPathNumber(x: number): string | undefined { function pathFromToken(token: IResolvable | undefined) { return token && (JsonPathToken.isJsonPathToken(token) ? token.path : undefined); } + +/** + * Render the string in a valid JSON Path expression. + * + * If the string is a Tokenized JSON path reference -- return the JSON path reference inside it. + * Otherwise, single-quote it. + * + * Call this function whenever you're building compound JSONPath expressions, in + * order to avoid having tokens-in-tokens-in-tokens which become very hard to parse. + */ +export function renderInExpression(x: string) { + const path = jsonPathString(x); + return path ?? singleQuotestring(x); +} + +function singleQuotestring(x: string) { + const ret = new Array(); + ret.push("'"); + for (const c of x) { + if (c === "'") { + ret.push("\\'"); + } else if (c === '\\') { + ret.push('\\\\'); + } else if (c === '\n') { + ret.push('\\n'); + } else { + ret.push(c); + } + } + ret.push("'"); + return ret.join(''); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index 4143e4bba6601..7fe4423b8a310 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -142,7 +142,9 @@ abstract class StateMachineBase extends Resource implements IStateMachine { public readonly stateMachineArn = stateMachineArn; public readonly grantPrincipal = new iam.UnknownPrincipal({ resource: this }); } - return new Import(scope, id); + return new Import(scope, id, { + environmentFromArn: stateMachineArn, + }); } public abstract readonly stateMachineArn: string; diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts index fb5e52d2831b2..bff92d431cbab 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts @@ -116,7 +116,7 @@ export interface PassProps { /** * Define a Pass in the state machine * - * A Pass state can be used to transform the current exeuction's state. + * A Pass state can be used to transform the current execution's state. */ export class Pass extends State implements INextable { public readonly endStates: INextable[]; diff --git a/packages/@aws-cdk/aws-stepfunctions/package.json b/packages/@aws-cdk/aws-stepfunctions/package.json index f8bd77b981dc2..c901ae4980ad4 100644 --- a/packages/@aws-cdk/aws-stepfunctions/package.json +++ b/packages/@aws-cdk/aws-stepfunctions/package.json @@ -84,7 +84,7 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-cloudwatch": "0.0.0", diff --git a/packages/@aws-cdk/aws-stepfunctions/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-stepfunctions/rosetta/default.ts-fixture index 91085b6c8933c..01f59d49ec36d 100644 --- a/packages/@aws-cdk/aws-stepfunctions/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/aws-stepfunctions/rosetta/default.ts-fixture @@ -1,10 +1,10 @@ // Fixture with packages imported, but nothing else import { Construct } from 'constructs'; import { App, CfnOutput, Duration, Stack } from '@aws-cdk/core'; -import sfn = require('@aws-cdk/aws-stepfunctions'); -import tasks = require('@aws-cdk/aws-stepfunctions-tasks'); -import cloudwatch = require('@aws-cdk/aws-cloudwatch'); -import iam = require('@aws-cdk/aws-iam'); +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import * as iam from '@aws-cdk/aws-iam'; class Fixture extends Stack { constructor(scope: Construct, id: string) { diff --git a/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts index 0e6e3e9c25175..eec3864f938e6 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts @@ -212,3 +212,52 @@ describe('Fields', () => { }); }); }); + +describe('intrinsics constructors', () => { + test('array', () => { + expect(FieldUtils.renderObject({ + Field: JsonPath.array('asdf', JsonPath.stringAt('$.Id')), + })).toEqual({ + 'Field.$': "States.Array('asdf', $.Id)", + }); + }); + + test('format', () => { + expect(FieldUtils.renderObject({ + Field: JsonPath.format('Hi my name is {}.', JsonPath.stringAt('$.Name')), + })).toEqual({ + 'Field.$': "States.Format('Hi my name is {}.', $.Name)", + }); + + expect(FieldUtils.renderObject({ + Field: JsonPath.format(JsonPath.stringAt('$.Format'), JsonPath.stringAt('$.Name')), + })).toEqual({ + 'Field.$': 'States.Format($.Format, $.Name)', + }); + }); + + test('stringToJson', () => { + expect(FieldUtils.renderObject({ + Field: JsonPath.stringToJson(JsonPath.stringAt('$.Str')), + })).toEqual({ + 'Field.$': 'States.StringToJson($.Str)', + }); + }); + + test('jsonToString', () => { + expect(FieldUtils.renderObject({ + Field: JsonPath.jsonToString(JsonPath.objectAt('$.Obj')), + })).toEqual({ + 'Field.$': 'States.JsonToString($.Obj)', + }); + }); +}); + +test('find task token even if nested in intrinsic functions', () => { + expect(FieldUtils.containsTaskToken({ x: JsonPath.array(JsonPath.taskToken) })).toEqual(true); + + expect(FieldUtils.containsTaskToken({ x: JsonPath.array('nope') })).toEqual(false); + + // Even if it's a hand-written literal and doesn't use our constructors + expect(FieldUtils.containsTaskToken({ x: JsonPath.stringAt('States.Array($$.Task.Token)') })).toEqual(true); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/private/intrinsics.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/private/intrinsics.test.ts new file mode 100644 index 0000000000000..5534ad5509b20 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/private/intrinsics.test.ts @@ -0,0 +1,91 @@ +import { IntrinsicParser } from '../../lib/private/intrinstics'; + + +test('parse JSON path', () => { + expect(parse('$.Path')).toEqual({ type: 'path', path: '$.Path' }); + expect(parse('$[\'complex\'].Path')).toEqual({ type: 'path', path: '$[\'complex\'].Path' }); + + // Can add whitespace + expect(parse(' $.Path')).toEqual({ type: 'path', path: '$.Path' }); + expect(parse('$.Path ')).toEqual({ type: 'path', path: '$.Path' }); +}); + +test('JSON path with quoted literal', () => { + expect(parse("$['I\\'m'].Path")).toEqual({ type: 'path', path: "$['I\\'m'].Path" }); +}); + +test('Complex JSON path between square brackets', () => { + expect(parse("$[?('Eva Green' in @['starring'])]")).toEqual({ type: 'path', path: "$[?('Eva Green' in @['starring'])]" }); +}); + +test('JSON path must be contiguous', () => { + expect(() => parse('$.Path AndThen')).toThrow(/Invalid JSONPath expression/); +}); + +test('parse fncall with path', () => { + expect(parse('States.Array($$.Context.Token)')).toEqual({ + type: 'fncall', + functionName: 'States.Array', + arguments: [{ + type: 'path', + path: '$$.Context.Token', + }], + }); +}); + +test('parse fncall with string and path', () => { + expect(parse("States.Format('Hi my name is {}.', $.Name)")).toEqual({ + type: 'fncall', + functionName: 'States.Format', + arguments: [ + { + type: 'string-literal', + literal: 'Hi my name is {}.', + }, + { + type: 'path', + path: '$.Name', + }, + ], + }); +}); + +test('parse string literal with escaped quotes', () => { + expect(parse("States.Format('Hi I\\'m cool')")).toEqual({ + type: 'fncall', + functionName: 'States.Format', + arguments: [ + { + type: 'string-literal', + literal: "Hi I'm cool", + }, + ], + }); +}); + +test('nested function calls', () => { + expect(parse("States.Format('{}', States.JsonToString($.Obj))")).toEqual({ + type: 'fncall', + functionName: 'States.Format', + arguments: [ + { + type: 'string-literal', + literal: '{}', + }, + { + type: 'fncall', + functionName: 'States.JsonToString', + arguments: [ + { + type: 'path', + path: '$.Obj', + }, + ], + }, + ], + }); +}); + +function parse(x: string) { + return new IntrinsicParser(x).parseTopLevelIntrinsic(); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts index f60c0b756e66a..f99cc479c9975 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts @@ -277,4 +277,35 @@ describe('State Machine', () => { ], }); }); + + describe('StateMachine.fromStateMachineArn()', () => { + let stack: cdk.Stack; + + beforeEach(() => { + const app = new cdk.App(); + stack = new cdk.Stack(app, 'Base', { + env: { account: '111111111111', region: 'stack-region' }, + }); + }); + + describe('for a state machine in a different account and region', () => { + let mach: stepfunctions.IStateMachine; + + beforeEach(() => { + mach = stepfunctions.StateMachine.fromStateMachineArn( + stack, + 'iMach', + 'arn:aws:states:machine-region:222222222222:stateMachine:machine-name', + ); + }); + + test("the state machine's region is taken from the ARN", () => { + expect(mach.env.region).toBe('machine-region'); + }); + + test("the state machine's account is taken from the ARN", () => { + expect(mach.env.account).toBe('222222222222'); + }); + }); + }); }); diff --git a/packages/@aws-cdk/aws-synthetics/lib/canary.ts b/packages/@aws-cdk/aws-synthetics/lib/canary.ts index 31d7c0d2a2967..fab47a960289a 100644 --- a/packages/@aws-cdk/aws-synthetics/lib/canary.ts +++ b/packages/@aws-cdk/aws-synthetics/lib/canary.ts @@ -226,6 +226,7 @@ export class Canary extends cdk.Resource { this.artifactsBucket = props.artifactsBucketLocation?.bucket ?? new s3.Bucket(this, 'ArtifactsBucket', { encryption: s3.BucketEncryption.KMS_MANAGED, + enforceSSL: true, }); this.role = props.role ?? this.createDefaultRole(props.artifactsBucketLocation?.prefix); @@ -289,7 +290,6 @@ export class Canary extends cdk.Resource { * Returns a default role for the canary */ private createDefaultRole(prefix?: string): iam.IRole { - const { partition } = cdk.Stack.of(this); // Created role will need these policies to run the Canary. // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-synthetics-canary.html#cfn-synthetics-canary-executionrolearn const policy = new iam.PolicyDocument({ @@ -298,9 +298,13 @@ export class Canary extends cdk.Resource { resources: ['*'], actions: ['s3:ListAllMyBuckets'], }), + new iam.PolicyStatement({ + resources: [this.artifactsBucket.bucketArn], + actions: ['s3:GetBucketLocation'], + }), new iam.PolicyStatement({ resources: [this.artifactsBucket.arnForObjects(`${prefix ? prefix+'/*' : '*'}`)], - actions: ['s3:PutObject', 's3:GetBucketLocation'], + actions: ['s3:PutObject'], }), new iam.PolicyStatement({ resources: ['*'], @@ -308,7 +312,7 @@ export class Canary extends cdk.Resource { conditions: { StringEquals: { 'cloudwatch:namespace': 'CloudWatchSynthetics' } }, }), new iam.PolicyStatement({ - resources: [`arn:${partition}:logs:::*`], + resources: [this.logGroupArn()], actions: ['logs:CreateLogStream', 'logs:CreateLogGroup', 'logs:PutLogEvents'], }), ], @@ -322,6 +326,15 @@ export class Canary extends cdk.Resource { }); } + private logGroupArn() { + return cdk.Stack.of(this).formatArn({ + service: 'logs', + resource: 'log-group', + arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME, + resourceName: '/aws/lambda/cwsyn-*', + }); + } + /** * Returns the code object taken in by the canary resource. */ diff --git a/packages/@aws-cdk/aws-synthetics/package.json b/packages/@aws-cdk/aws-synthetics/package.json index 95ea3307e1823..ed5b4eb9f4534 100644 --- a/packages/@aws-cdk/aws-synthetics/package.json +++ b/packages/@aws-cdk/aws-synthetics/package.json @@ -86,7 +86,7 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/aws-cloudwatch": "0.0.0", diff --git a/packages/@aws-cdk/aws-synthetics/test/canary.test.ts b/packages/@aws-cdk/aws-synthetics/test/canary.test.ts index 83ba173562834..873bf36d61d6d 100644 --- a/packages/@aws-cdk/aws-synthetics/test/canary.test.ts +++ b/packages/@aws-cdk/aws-synthetics/test/canary.test.ts @@ -440,3 +440,98 @@ test('can specify custom test', () => { }, }); }); + +test('Role policy generated as expected', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new synthetics.Canary(stack, 'Canary', { + test: synthetics.Test.custom({ + handler: 'index.handler', + code: synthetics.Code.fromInline('/* Synthetics handler code */'), + }), + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_3_3, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + Policies: [{ + PolicyDocument: { + Statement: [ + { + Action: 's3:ListAllMyBuckets', + Effect: 'Allow', + Resource: '*', + }, + { + Action: 's3:GetBucketLocation', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'CanaryArtifactsBucket4A60D32B', + 'Arn', + ], + }, + }, + { + Action: 's3:PutObject', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'CanaryArtifactsBucket4A60D32B', + '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:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':log-group:/aws/lambda/cwsyn-*', + ], + ], + }, + }, + ], + }, + }], + }); +}); diff --git a/packages/@aws-cdk/aws-synthetics/test/integ.asset.expected.json b/packages/@aws-cdk/aws-synthetics/test/integ.asset.expected.json index 7d614f08201b7..256de95e7be25 100644 --- a/packages/@aws-cdk/aws-synthetics/test/integ.asset.expected.json +++ b/packages/@aws-cdk/aws-synthetics/test/integ.asset.expected.json @@ -41,10 +41,17 @@ "Resource": "*" }, { - "Action": [ - "s3:PutObject", - "s3:GetBucketLocation" - ], + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyCanaryArtifactsBucket89975E6D", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", "Effect": "Allow", "Resource": { "Fn::Join": [ @@ -197,10 +204,17 @@ "Resource": "*" }, { - "Action": [ - "s3:PutObject", - "s3:GetBucketLocation" - ], + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyCanaryArtifactsBucket89975E6D", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", "Effect": "Allow", "Resource": { "Fn::Join": [ 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 988973d8bfe78..aa88a65ad353a 100644 --- a/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json +++ b/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json @@ -30,10 +30,17 @@ "Resource": "*" }, { - "Action": [ - "s3:PutObject", - "s3:GetBucketLocation" - ], + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "mytestbucket8DC16178", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", "Effect": "Allow", "Resource": { "Fn::Join": [ @@ -75,7 +82,15 @@ { "Ref": "AWS::Partition" }, - ":logs:::*" + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/cwsyn-*" ] ] } @@ -138,6 +153,53 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, + "MyCanaryOneArtifactsBucketPolicyA2B99545": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "MyCanaryOneArtifactsBucketDF4A487D" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "MyCanaryOneArtifactsBucketDF4A487D", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyCanaryOneArtifactsBucketDF4A487D", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, "MyCanaryOneServiceRole41995561": { "Type": "AWS::IAM::Role", "Properties": { @@ -163,10 +225,17 @@ "Resource": "*" }, { - "Action": [ - "s3:PutObject", - "s3:GetBucketLocation" - ], + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyCanaryOneArtifactsBucketDF4A487D", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", "Effect": "Allow", "Resource": { "Fn::Join": [ @@ -208,7 +277,15 @@ { "Ref": "AWS::Partition" }, - ":logs:::*" + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/cwsyn-*" ] ] } @@ -305,6 +382,53 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, + "MyCanaryTwoArtifactsBucketPolicy4719E279": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "MyCanaryTwoArtifactsBucket79B179B6" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "MyCanaryTwoArtifactsBucket79B179B6", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyCanaryTwoArtifactsBucket79B179B6", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, "MyCanaryTwoServiceRole041E85D4": { "Type": "AWS::IAM::Role", "Properties": { @@ -330,10 +454,17 @@ "Resource": "*" }, { - "Action": [ - "s3:PutObject", - "s3:GetBucketLocation" - ], + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyCanaryTwoArtifactsBucket79B179B6", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", "Effect": "Allow", "Resource": { "Fn::Join": [ @@ -375,7 +506,15 @@ { "Ref": "AWS::Partition" }, - ":logs:::*" + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/cwsyn-*" ] ] } @@ -472,6 +611,53 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, + "MyCanaryThreeArtifactsBucketPolicy568A97F7": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "MyCanaryThreeArtifactsBucket894E857E" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "MyCanaryThreeArtifactsBucket894E857E", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyCanaryThreeArtifactsBucket894E857E", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, "MyCanaryThreeServiceRole68117E65": { "Type": "AWS::IAM::Role", "Properties": { @@ -497,10 +683,17 @@ "Resource": "*" }, { - "Action": [ - "s3:PutObject", - "s3:GetBucketLocation" - ], + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyCanaryThreeArtifactsBucket894E857E", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", "Effect": "Allow", "Resource": { "Fn::Join": [ @@ -542,7 +735,15 @@ { "Ref": "AWS::Partition" }, - ":logs:::*" + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/cwsyn-*" ] ] } @@ -639,6 +840,53 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, + "MyPythonCanaryArtifactsBucketPolicy7E13B7C5": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "MyPythonCanaryArtifactsBucket7AE88133" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "MyPythonCanaryArtifactsBucket7AE88133", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyPythonCanaryArtifactsBucket7AE88133", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, "MyPythonCanaryServiceRole41A363E1": { "Type": "AWS::IAM::Role", "Properties": { @@ -664,10 +912,17 @@ "Resource": "*" }, { - "Action": [ - "s3:PutObject", - "s3:GetBucketLocation" - ], + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyPythonCanaryArtifactsBucket7AE88133", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", "Effect": "Allow", "Resource": { "Fn::Join": [ @@ -709,7 +964,15 @@ { "Ref": "AWS::Partition" }, - ":logs:::*" + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/cwsyn-*" ] ] } diff --git a/packages/@aws-cdk/aws-timestream/package.json b/packages/@aws-cdk/aws-timestream/package.json index 252484a673f49..d630d3f2f818f 100644 --- a/packages/@aws-cdk/aws-timestream/package.json +++ b/packages/@aws-cdk/aws-timestream/package.json @@ -85,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/aws-transfer/package.json b/packages/@aws-cdk/aws-transfer/package.json index e213292d647b1..efeddaba3fff8 100644 --- a/packages/@aws-cdk/aws-transfer/package.json +++ b/packages/@aws-cdk/aws-transfer/package.json @@ -85,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-waf/package.json b/packages/@aws-cdk/aws-waf/package.json index 1f8abd7413335..20c6e3c1a8896 100644 --- a/packages/@aws-cdk/aws-waf/package.json +++ b/packages/@aws-cdk/aws-waf/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -76,7 +83,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-wafregional/package.json b/packages/@aws-cdk/aws-wafregional/package.json index 92f5f997825db..aa4997b25435f 100644 --- a/packages/@aws-cdk/aws-wafregional/package.json +++ b/packages/@aws-cdk/aws-wafregional/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -76,7 +83,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-wafv2/package.json b/packages/@aws-cdk/aws-wafv2/package.json index f1077a51389fd..3f6fa8ac9d3ff 100644 --- a/packages/@aws-cdk/aws-wafv2/package.json +++ b/packages/@aws-cdk/aws-wafv2/package.json @@ -85,7 +85,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-wisdom/package.json b/packages/@aws-cdk/aws-wisdom/package.json index ab4a5c4f1aa37..f641345d8e7ea 100644 --- a/packages/@aws-cdk/aws-wisdom/package.json +++ b/packages/@aws-cdk/aws-wisdom/package.json @@ -88,7 +88,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-workspaces/package.json b/packages/@aws-cdk/aws-workspaces/package.json index a19a5749ac5f7..8f0da7bbbfc03 100644 --- a/packages/@aws-cdk/aws-workspaces/package.json +++ b/packages/@aws-cdk/aws-workspaces/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -76,7 +83,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-xray/package.json b/packages/@aws-cdk/aws-xray/package.json index 19980bffb77f4..83f4d9e17dfed 100644 --- a/packages/@aws-cdk/aws-xray/package.json +++ b/packages/@aws-cdk/aws-xray/package.json @@ -87,7 +87,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3" + "@types/jest": "^27.4.1" }, "dependencies": { "@aws-cdk/core": "0.0.0" diff --git a/packages/@aws-cdk/cdk-assets-schema/package.json b/packages/@aws-cdk/cdk-assets-schema/package.json index 108cfe23af0dd..abec52ee08000 100644 --- a/packages/@aws-cdk/cdk-assets-schema/package.json +++ b/packages/@aws-cdk/cdk-assets-schema/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "scripts": { "build": "cdk-build", @@ -54,8 +61,8 @@ "devDependencies": { "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "repository": { "url": "https://github.com/aws/aws-cdk.git", diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index 00bc8552e8367..e324b3ca575f9 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,854 @@ +# CloudFormation Resource Specification v57.0.0 + +## New Resource Types + +* AWS::ECR::PullThroughCacheRule + +## Attribute Changes + +* AWS::Batch::ComputeEnvironment ComputeEnvironmentArn (__added__) +* AWS::Batch::JobQueue JobQueueArn (__added__) +* AWS::SES::ConfigurationSetEventDestination Id (__added__) + +## Property Changes + +* AWS::Batch::ComputeEnvironment Tags.PrimitiveType (__deleted__) +* AWS::Batch::ComputeEnvironment Tags.PrimitiveItemType (__added__) +* AWS::Batch::ComputeEnvironment Tags.Type (__added__) +* AWS::Batch::JobQueue ComputeEnvironmentOrder.DuplicatesAllowed (__added__) +* AWS::Batch::JobQueue Tags.PrimitiveType (__deleted__) +* AWS::Batch::JobQueue Tags.PrimitiveItemType (__added__) +* AWS::Batch::JobQueue Tags.Type (__added__) +* AWS::DocDB::DBCluster MasterUserPassword.Required (__changed__) + * Old: true + * New: false +* AWS::DocDB::DBCluster MasterUsername.Required (__changed__) + * Old: true + * New: false +* AWS::EC2::Instance PrivateDnsNameOptions (__added__) +* AWS::IoT::Authorizer EnableCachingForHttp (__added__) +* AWS::Kendra::DataSource CustomDocumentEnrichmentConfiguration (__added__) +* AWS::Lambda::EventSourceMapping FilterCriteria.PrimitiveType (__deleted__) +* AWS::Lambda::EventSourceMapping FilterCriteria.Type (__added__) +* AWS::Lambda::EventSourceMapping StartingPositionTimestamp.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::StepFunctions::StateMachine StateMachineType.UpdateType (__changed__) + * Old: Mutable + * New: Immutable + +## Property Type Changes + +* AWS::EC2::Instance.PrivateDnsNameOptions (__added__) +* AWS::Kendra::DataSource.CustomDocumentEnrichmentConfiguration (__added__) +* AWS::Kendra::DataSource.DocumentAttributeCondition (__added__) +* AWS::Kendra::DataSource.DocumentAttributeTarget (__added__) +* AWS::Kendra::DataSource.DocumentAttributeValue (__added__) +* AWS::Kendra::DataSource.HookConfiguration (__added__) +* AWS::Kendra::DataSource.InlineCustomDocumentEnrichmentConfiguration (__added__) +* AWS::Lambda::EventSourceMapping.Filter (__added__) +* AWS::Lambda::EventSourceMapping.FilterCriteria (__added__) +* AWS::WAFv2::WebACL.FieldIdentifier (__added__) +* AWS::WAFv2::WebACL.ManagedRuleGroupConfig (__added__) +* AWS::Batch::ComputeEnvironment.ComputeResources Ec2Configuration.DuplicatesAllowed (__added__) +* AWS::Batch::ComputeEnvironment.ComputeResources InstanceTypes.DuplicatesAllowed (__added__) +* AWS::Batch::ComputeEnvironment.ComputeResources SecurityGroupIds.DuplicatesAllowed (__added__) +* AWS::Batch::ComputeEnvironment.ComputeResources Subnets.DuplicatesAllowed (__added__) +* AWS::Batch::ComputeEnvironment.ComputeResources Tags.PrimitiveType (__deleted__) +* AWS::Batch::ComputeEnvironment.ComputeResources Tags.PrimitiveItemType (__added__) +* AWS::Batch::ComputeEnvironment.ComputeResources Tags.Type (__added__) +* AWS::EC2::SpotFleet.LaunchTemplateOverrides Priority (__added__) +* AWS::IoTAnalytics::Dataset.RetentionPeriod NumberOfDays.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Dataset.RetentionPeriod Unlimited.Required (__changed__) + * Old: true + * New: false +* AWS::NetworkFirewall::FirewallPolicy.FirewallPolicy StatefulDefaultActions.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::FirewallPolicy.FirewallPolicy StatefulRuleGroupReferences.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::FirewallPolicy.FirewallPolicy StatelessCustomActions.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::FirewallPolicy.FirewallPolicy StatelessDefaultActions.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::FirewallPolicy.FirewallPolicy StatelessFragmentDefaultActions.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::FirewallPolicy.FirewallPolicy StatelessRuleGroupReferences.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::FirewallPolicy.PublishMetricAction Dimensions.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::RuleGroup.IPSet Definition.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::RuleGroup.MatchAttributes DestinationPorts.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::RuleGroup.MatchAttributes Destinations.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::RuleGroup.MatchAttributes Protocols.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::RuleGroup.MatchAttributes SourcePorts.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::RuleGroup.MatchAttributes Sources.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::RuleGroup.MatchAttributes TCPFlags.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::RuleGroup.PortSet Definition.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::RuleGroup.PublishMetricAction Dimensions.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::RuleGroup.RuleDefinition Actions.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::RuleGroup.RuleOption Settings.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::RuleGroup.RulesSource StatefulRules.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::RuleGroup.RulesSourceList TargetTypes.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::RuleGroup.RulesSourceList Targets.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::RuleGroup.StatefulRule RuleOptions.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::RuleGroup.StatelessRulesAndCustomActions CustomActions.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::RuleGroup.StatelessRulesAndCustomActions StatelessRules.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::RuleGroup.TCPFlagField Flags.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::NetworkFirewall::RuleGroup.TCPFlagField Masks.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::SES::ConfigurationSetEventDestination.CloudWatchDestination DimensionConfigurations.DuplicatesAllowed (__added__) +* AWS::SES::ConfigurationSetEventDestination.EventDestination MatchingEventTypes.DuplicatesAllowed (__added__) +* AWS::WAFv2::WebACL.ManagedRuleGroupStatement ManagedRuleGroupConfigs (__added__) + +## Unapplied changes + +* AWS::AppIntegrations is at 53.1.0 + +# CloudFormation Resource Specification v56.0.0 + +## New Resource Types + +* AWS::AppRunner::VpcConnector +* AWS::CloudFormation::HookDefaultVersion +* AWS::CloudFormation::HookTypeConfig +* AWS::CloudFormation::HookVersion + +## Attribute Changes + +* AWS::AutoScaling::LaunchConfiguration Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html +* AWS::EKS::Nodegroup Id (__added__) +* AWS::SES::Template Id (__added__) +* AWS::SQS::Queue QueueUrl (__added__) +* AWS::SQS::Queue Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html + +## Property Changes + +* AWS::AppRunner::Service NetworkConfiguration (__added__) +* AWS::AutoScaling::LaunchConfiguration AssociatePublicIpAddress.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cf-as-launchconfig-associatepubip + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-associatepublicipaddress +* AWS::AutoScaling::LaunchConfiguration BlockDeviceMappings.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-blockdevicemappings + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-blockdevicemappings +* AWS::AutoScaling::LaunchConfiguration ClassicLinkVPCId.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-classiclinkvpcid + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-classiclinkvpcid +* AWS::AutoScaling::LaunchConfiguration ClassicLinkVPCSecurityGroups.DuplicatesAllowed (__deleted__) +* AWS::AutoScaling::LaunchConfiguration ClassicLinkVPCSecurityGroups.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-classiclinkvpcsecuritygroups + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-classiclinkvpcsecuritygroups +* AWS::AutoScaling::LaunchConfiguration EbsOptimized.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-ebsoptimized + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-ebsoptimized +* AWS::AutoScaling::LaunchConfiguration IamInstanceProfile.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-iaminstanceprofile + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-iaminstanceprofile +* AWS::AutoScaling::LaunchConfiguration ImageId.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-imageid + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-imageid +* AWS::AutoScaling::LaunchConfiguration InstanceId.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-instanceid + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-instanceid +* AWS::AutoScaling::LaunchConfiguration InstanceMonitoring.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-instancemonitoring + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-instancemonitoring +* AWS::AutoScaling::LaunchConfiguration InstanceType.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-instancetype + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-instancetype +* AWS::AutoScaling::LaunchConfiguration KernelId.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-kernelid + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-kernelid +* AWS::AutoScaling::LaunchConfiguration KeyName.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-keyname + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-keyname +* AWS::AutoScaling::LaunchConfiguration LaunchConfigurationName.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-autoscaling-launchconfig-launchconfigurationname + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-launchconfigurationname +* AWS::AutoScaling::LaunchConfiguration MetadataOptions.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-autoscaling-launchconfig-metadataoptions + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-metadataoptions +* AWS::AutoScaling::LaunchConfiguration PlacementTenancy.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-placementtenancy + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-placementtenancy +* AWS::AutoScaling::LaunchConfiguration RamDiskId.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-ramdiskid + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-ramdiskid +* AWS::AutoScaling::LaunchConfiguration SecurityGroups.DuplicatesAllowed (__deleted__) +* AWS::AutoScaling::LaunchConfiguration SecurityGroups.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-securitygroups + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-securitygroups +* AWS::AutoScaling::LaunchConfiguration SpotPrice.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-spotprice + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-spotprice +* AWS::AutoScaling::LaunchConfiguration UserData.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-userdata + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-userdata +* AWS::CustomerProfiles::Integration ObjectTypeNames (__added__) +* AWS::CustomerProfiles::Integration ObjectTypeName.Required (__changed__) + * Old: true + * New: false +* AWS::DocDB::DBCluster CopyTagsToSnapshot (__added__) +* AWS::EC2::Subnet AvailabilityZoneId (__added__) +* AWS::EC2::Subnet EnableDns64 (__added__) +* AWS::EC2::Subnet Ipv6Native (__added__) +* AWS::EC2::Subnet PrivateDnsNameOptionsOnLaunch (__added__) +* AWS::EC2::Subnet Ipv6CidrBlock.UpdateType (__changed__) + * Old: Mutable + * New: Conditional +* AWS::EC2::VPC Ipv4IpamPoolId (__added__) +* AWS::EC2::VPC Ipv4NetmaskLength (__added__) +* AWS::EC2::VPCCidrBlock Ipv4IpamPoolId (__added__) +* AWS::EC2::VPCCidrBlock Ipv4NetmaskLength (__added__) +* AWS::EC2::VPCCidrBlock Ipv6IpamPoolId (__added__) +* AWS::EC2::VPCCidrBlock Ipv6NetmaskLength (__added__) +* AWS::EKS::Nodegroup DiskSize.PrimitiveType (__changed__) + * Old: Double + * New: Integer +* AWS::EKS::Nodegroup InstanceTypes.DuplicatesAllowed (__added__) +* AWS::EKS::Nodegroup Subnets.DuplicatesAllowed (__added__) +* AWS::SQS::Queue ContentBasedDeduplication.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-contentbaseddeduplication + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-contentbaseddeduplication +* AWS::SQS::Queue DeduplicationScope.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-deduplicationscope + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-deduplicationscope +* AWS::SQS::Queue DelaySeconds.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-delayseconds + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-delayseconds +* AWS::SQS::Queue FifoQueue.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-fifoqueue + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-fifoqueue +* AWS::SQS::Queue FifoThroughputLimit.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-fifothroughputlimit + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-fifothroughputlimit +* AWS::SQS::Queue KmsDataKeyReusePeriodSeconds.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-kmsdatakeyreuseperiodseconds + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-kmsdatakeyreuseperiodseconds +* AWS::SQS::Queue KmsMasterKeyId.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-kmsmasterkeyid + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-kmsmasterkeyid +* AWS::SQS::Queue MaximumMessageSize.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-maxmesgsize + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-maximummessagesize +* AWS::SQS::Queue MessageRetentionPeriod.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-msgretentionperiod + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-messageretentionperiod +* AWS::SQS::Queue QueueName.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-name + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-queuename +* AWS::SQS::Queue ReceiveMessageWaitTimeSeconds.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-receivemsgwaittime + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-receivemessagewaittimeseconds +* AWS::SQS::Queue RedriveAllowPolicy.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-redriveallowpolicy + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-redriveallowpolicy +* AWS::SQS::Queue RedrivePolicy.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-redrive + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-redrivepolicy +* AWS::SQS::Queue Tags.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#cfn-sqs-queue-tags + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-tags +* AWS::SQS::Queue VisibilityTimeout.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-visiblitytimeout + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-visibilitytimeout + +## Property Type Changes + +* AWS::AppRunner::Service.EgressConfiguration (__added__) +* AWS::AppRunner::Service.NetworkConfiguration (__added__) +* AWS::CustomerProfiles::Integration.ObjectTypeMapping (__added__) +* AWS::EC2::Subnet.PrivateDnsNameOptionsOnLaunch (__added__) +* AWS::AutoScaling::LaunchConfiguration.BlockDevice DeleteOnTermination.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-template.html#cfn-as-launchconfig-blockdev-template-deleteonterm + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevice.html#cfn-autoscaling-launchconfiguration-blockdevice-deleteontermination +* AWS::AutoScaling::LaunchConfiguration.BlockDevice DeleteOnTermination.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::AutoScaling::LaunchConfiguration.BlockDevice Encrypted.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-template.html#cfn-as-launchconfig-blockdev-template-encrypted + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevice.html#cfn-autoscaling-launchconfiguration-blockdevice-encrypted +* AWS::AutoScaling::LaunchConfiguration.BlockDevice Encrypted.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::AutoScaling::LaunchConfiguration.BlockDevice Iops.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-template.html#cfn-as-launchconfig-blockdev-template-iops + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevice.html#cfn-autoscaling-launchconfiguration-blockdevice-iops +* AWS::AutoScaling::LaunchConfiguration.BlockDevice Iops.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::AutoScaling::LaunchConfiguration.BlockDevice SnapshotId.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-template.html#cfn-as-launchconfig-blockdev-template-snapshotid + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevice.html#cfn-autoscaling-launchconfiguration-blockdevice-snapshotid +* AWS::AutoScaling::LaunchConfiguration.BlockDevice SnapshotId.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::AutoScaling::LaunchConfiguration.BlockDevice Throughput.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-template.html#cfn-as-launchconfig-blockdev-template-throughput + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevice.html#cfn-autoscaling-launchconfiguration-blockdevice-throughput +* AWS::AutoScaling::LaunchConfiguration.BlockDevice Throughput.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::AutoScaling::LaunchConfiguration.BlockDevice VolumeSize.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-template.html#cfn-as-launchconfig-blockdev-template-volumesize + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevice.html#cfn-autoscaling-launchconfiguration-blockdevice-volumesize +* AWS::AutoScaling::LaunchConfiguration.BlockDevice VolumeSize.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::AutoScaling::LaunchConfiguration.BlockDevice VolumeType.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-template.html#cfn-as-launchconfig-blockdev-template-volumetype + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevice.html#cfn-autoscaling-launchconfiguration-blockdevice-volumetype +* AWS::AutoScaling::LaunchConfiguration.BlockDevice VolumeType.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::AutoScaling::LaunchConfiguration.BlockDeviceMapping DeviceName.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-mapping.html#cfn-as-launchconfig-blockdev-mapping-devicename + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevicemapping.html#cfn-autoscaling-launchconfiguration-blockdevicemapping-devicename +* AWS::AutoScaling::LaunchConfiguration.BlockDeviceMapping DeviceName.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::AutoScaling::LaunchConfiguration.BlockDeviceMapping Ebs.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-mapping.html#cfn-as-launchconfig-blockdev-mapping-ebs + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevicemapping.html#cfn-autoscaling-launchconfiguration-blockdevicemapping-ebs +* AWS::AutoScaling::LaunchConfiguration.BlockDeviceMapping Ebs.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::AutoScaling::LaunchConfiguration.BlockDeviceMapping NoDevice.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-mapping.html#cfn-as-launchconfig-blockdev-mapping-nodevice + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevicemapping.html#cfn-autoscaling-launchconfiguration-blockdevicemapping-nodevice +* AWS::AutoScaling::LaunchConfiguration.BlockDeviceMapping NoDevice.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::AutoScaling::LaunchConfiguration.BlockDeviceMapping VirtualName.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-mapping.html#cfn-as-launchconfig-blockdev-mapping-virtualname + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevicemapping.html#cfn-autoscaling-launchconfiguration-blockdevicemapping-virtualname +* AWS::AutoScaling::LaunchConfiguration.BlockDeviceMapping VirtualName.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::AutoScaling::LaunchConfiguration.MetadataOptions HttpEndpoint.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfig-metadataoptions.html#cfn-autoscaling-launchconfig-metadataoptions-httpendpoint + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-metadataoptions.html#cfn-autoscaling-launchconfiguration-metadataoptions-httpendpoint +* AWS::AutoScaling::LaunchConfiguration.MetadataOptions HttpEndpoint.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::AutoScaling::LaunchConfiguration.MetadataOptions HttpPutResponseHopLimit.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfig-metadataoptions.html#cfn-autoscaling-launchconfig-metadataoptions-httpputresponsehoplimit + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-metadataoptions.html#cfn-autoscaling-launchconfiguration-metadataoptions-httpputresponsehoplimit +* AWS::AutoScaling::LaunchConfiguration.MetadataOptions HttpPutResponseHopLimit.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::AutoScaling::LaunchConfiguration.MetadataOptions HttpTokens.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfig-metadataoptions.html#cfn-autoscaling-launchconfig-metadataoptions-httptokens + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-metadataoptions.html#cfn-autoscaling-launchconfiguration-metadataoptions-httptokens +* AWS::AutoScaling::LaunchConfiguration.MetadataOptions HttpTokens.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::DynamoDB::GlobalTable.ReplicaSpecification TableClass (__added__) +* AWS::EKS::Nodegroup.RemoteAccess SourceSecurityGroups.DuplicatesAllowed (__added__) +* AWS::EKS::Nodegroup.ScalingConfig DesiredSize.PrimitiveType (__changed__) + * Old: Double + * New: Integer +* AWS::EKS::Nodegroup.ScalingConfig MaxSize.PrimitiveType (__changed__) + * Old: Double + * New: Integer +* AWS::EKS::Nodegroup.ScalingConfig MinSize.PrimitiveType (__changed__) + * Old: Double + * New: Integer +* AWS::SES::Template.Template SubjectPart.Required (__changed__) + * Old: false + * New: true + +## Unapplied changes + +* AWS::AppIntegrations is at 53.1.0 + +# CloudFormation Resource Specification v55.0.0 + +## New Resource Types + + +## Attribute Changes + + +## Property Changes + +* AWS::RoboMaker::RobotApplication Environment (__added__) +* AWS::RoboMaker::RobotApplication RobotSoftwareSuite.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::RoboMaker::RobotApplication Sources.Required (__changed__) + * Old: true + * New: false +* AWS::RoboMaker::RobotApplication Tags.PrimitiveType (__deleted__) +* AWS::RoboMaker::RobotApplication Tags.PrimitiveItemType (__added__) +* AWS::RoboMaker::RobotApplication Tags.Type (__added__) + +## Property Type Changes + +* AWS::EC2::LaunchTemplate.PrivateDnsNameOptions (__added__) +* AWS::Events::Rule.SageMakerPipelineParameter (__added__) +* AWS::Events::Rule.SageMakerPipelineParameters (__added__) +* AWS::ApplicationInsights::Application.HANAPrometheusExporter AgreeToInstallHANADBClient.Required (__changed__) + * Old: false + * New: true +* AWS::ApplicationInsights::Application.HANAPrometheusExporter HANAPort.Required (__changed__) + * Old: false + * New: true +* AWS::ApplicationInsights::Application.HANAPrometheusExporter HANASID.Required (__changed__) + * Old: false + * New: true +* AWS::ApplicationInsights::Application.HANAPrometheusExporter HANASecretName.Required (__changed__) + * Old: false + * New: true +* AWS::Cognito::UserPool.SmsConfiguration SnsRegion (__added__) +* AWS::DMS::Endpoint.S3Settings AddColumnName (__added__) +* AWS::DMS::Endpoint.S3Settings CannedAclForObjects (__added__) +* AWS::DMS::Endpoint.S3Settings CdcInsertsAndUpdates (__added__) +* AWS::DMS::Endpoint.S3Settings CdcInsertsOnly (__added__) +* AWS::DMS::Endpoint.S3Settings CdcMaxBatchInterval (__added__) +* AWS::DMS::Endpoint.S3Settings CdcMinFileSize (__added__) +* AWS::DMS::Endpoint.S3Settings CdcPath (__added__) +* AWS::DMS::Endpoint.S3Settings CsvNoSupValue (__added__) +* AWS::DMS::Endpoint.S3Settings CsvNullValue (__added__) +* AWS::DMS::Endpoint.S3Settings DataFormat (__added__) +* AWS::DMS::Endpoint.S3Settings DataPageSize (__added__) +* AWS::DMS::Endpoint.S3Settings DatePartitionDelimiter (__added__) +* AWS::DMS::Endpoint.S3Settings DatePartitionEnabled (__added__) +* AWS::DMS::Endpoint.S3Settings DatePartitionSequence (__added__) +* AWS::DMS::Endpoint.S3Settings DatePartitionTimezone (__added__) +* AWS::DMS::Endpoint.S3Settings DictPageSizeLimit (__added__) +* AWS::DMS::Endpoint.S3Settings EnableStatistics (__added__) +* AWS::DMS::Endpoint.S3Settings EncodingType (__added__) +* AWS::DMS::Endpoint.S3Settings EncryptionMode (__added__) +* AWS::DMS::Endpoint.S3Settings IgnoreHeaderRows (__added__) +* AWS::DMS::Endpoint.S3Settings IncludeOpForFullLoad (__added__) +* AWS::DMS::Endpoint.S3Settings MaxFileSize (__added__) +* AWS::DMS::Endpoint.S3Settings ParquetTimestampInMillisecond (__added__) +* AWS::DMS::Endpoint.S3Settings ParquetVersion (__added__) +* AWS::DMS::Endpoint.S3Settings PreserveTransactions (__added__) +* AWS::DMS::Endpoint.S3Settings Rfc4180 (__added__) +* AWS::DMS::Endpoint.S3Settings RowGroupLength (__added__) +* AWS::DMS::Endpoint.S3Settings ServerSideEncryptionKmsKeyId (__added__) +* AWS::DMS::Endpoint.S3Settings TimestampColumnName (__added__) +* AWS::DMS::Endpoint.S3Settings UseCsvNoSupValue (__added__) +* AWS::DMS::Endpoint.S3Settings UseTaskStartTimeForFullLoadTimestamp (__added__) +* AWS::EC2::LaunchTemplate.LaunchTemplateData PrivateDnsNameOptions (__added__) +* AWS::EC2::LaunchTemplate.MetadataOptions InstanceMetadataTags (__added__) +* AWS::Events::Rule.Target SageMakerPipelineParameters (__added__) +* AWS::RoboMaker::RobotApplication.RobotSoftwareSuite Version.Required (__changed__) + * Old: true + * New: false + +## Unapplied changes + +* AWS::AppIntegrations is at 53.1.0 + +# CloudFormation Resource Specification v54.0.0 + +## New Resource Types + +* AWS::KafkaConnect::Connector +* AWS::Rekognition::Collection + +## Attribute Changes + + +## Property Changes + +* AWS::IVS::RecordingConfiguration ThumbnailConfiguration (__added__) +* AWS::Location::GeofenceCollection PricingPlan.Required (__changed__) + * Old: true + * New: false +* AWS::Location::Map PricingPlan.Required (__changed__) + * Old: true + * New: false +* AWS::Location::PlaceIndex PricingPlan.Required (__changed__) + * Old: true + * New: false +* AWS::Location::RouteCalculator PricingPlan.Required (__changed__) + * Old: true + * New: false +* AWS::Location::Tracker PricingPlan.Required (__changed__) + * Old: true + * New: false +* AWS::SecretsManager::RotationSchedule RotateImmediatelyOnUpdate (__added__) +* AWS::Timestream::Table MagneticStoreWriteProperties (__added__) + +## Property Type Changes + +* AWS::GuardDuty::Detector.CFNKubernetesAuditLogsConfiguration (__added__) +* AWS::GuardDuty::Detector.CFNKubernetesConfiguration (__added__) +* AWS::IVS::RecordingConfiguration.ThumbnailConfiguration (__added__) +* AWS::MSK::Cluster.ProvisionedThroughput (__added__) +* AWS::ECS::TaskDefinition.EFSVolumeConfiguration AuthorizationConfig.PrimitiveType (__deleted__) +* AWS::GuardDuty::Detector.CFNDataSourceConfigurations Kubernetes (__added__) +* AWS::MSK::Cluster.BrokerNodeGroupInfo StorageInfo.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::MSK::Cluster.EBSStorageInfo ProvisionedThroughput (__added__) +* AWS::MSK::Cluster.EBSStorageInfo VolumeSize.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::MSK::Cluster.StorageInfo EBSStorageInfo.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::Route53::RecordSetGroup.RecordSet Comment (__deleted__) +* AWS::SecretsManager::RotationSchedule.RotationRules Duration (__added__) +* AWS::SecretsManager::RotationSchedule.RotationRules ScheduleExpression (__added__) + +## Unapplied changes + +* AWS::AppIntegrations is at 53.1.0 + +# CloudFormation Resource Specification v53.1.0 + +## New Resource Types + +* AWS::Lightsail::Certificate +* AWS::Lightsail::Container +* AWS::Lightsail::Distribution + +## Attribute Changes + +* AWS::ServiceDiscovery::PrivateDnsNamespace HostedZoneId (__added__) +* AWS::ServiceDiscovery::PublicDnsNamespace HostedZoneId (__added__) + +## Property Changes + +* AWS::DMS::Endpoint GcpMySQLSettings (__added__) +* AWS::SageMaker::Device Device.PrimitiveType (__deleted__) +* AWS::SageMaker::Pipeline ParallelismConfiguration (__added__) + +## Property Type Changes + +* AWS::ApplicationInsights::Application.HAClusterPrometheusExporter (__added__) +* AWS::ApplicationInsights::Application.HANAPrometheusExporter (__added__) +* AWS::DMS::Endpoint.GcpMySQLSettings (__added__) +* AWS::SSM::MaintenanceWindowTask.CloudWatchOutputConfig (__added__) +* AWS::ApplicationInsights::Application.ConfigurationDetails HAClusterPrometheusExporter (__added__) +* AWS::ApplicationInsights::Application.ConfigurationDetails HANAPrometheusExporter (__added__) +* AWS::DataBrew::Job.OutputLocation BucketOwner (__added__) +* AWS::DataBrew::Job.S3Location BucketOwner (__added__) +* AWS::RoboMaker::SimulationApplication.RobotSoftwareSuite Version.Required (__changed__) + * Old: true + * New: false +* AWS::RoboMaker::SimulationApplication.SimulationSoftwareSuite Version.Required (__changed__) + * Old: true + * New: false +* AWS::SSM::MaintenanceWindowTask.MaintenanceWindowRunCommandParameters CloudWatchOutputConfig (__added__) +* AWS::SSM::MaintenanceWindowTask.MaintenanceWindowRunCommandParameters DocumentVersion (__added__) + +## Unapplied changes + +* AWS::ECS is at 51.0.0 + + +## Unapplied changes + +* AWS::ECS is at 51.0.0 +* AWS::SageMaker is at 51.0.0 + + +## Unapplied changes + +* AWS::ECS is at 51.0.0 +* AWS::SageMaker is at 51.0.0 + + +## Unapplied changes + +* AWS::ECS is at 51.0.0 +* AWS::SageMaker is at 51.0.0 + + +## Unapplied changes + +* AWS::ECS is at 51.0.0 +* AWS::SageMaker is at 51.0.0 + +# CloudFormation Resource Specification v53.0.0 + +## New Resource Types + +* AWS::AppStream::ApplicationEntitlementAssociation +* AWS::AppStream::Entitlement +* AWS::EC2::NetworkInsightsAccessScope +* AWS::EC2::NetworkInsightsAccessScopeAnalysis +* AWS::Forecast::Dataset +* AWS::Forecast::DatasetGroup +* AWS::InspectorV2::Filter +* AWS::KinesisVideo::SignalingChannel +* AWS::KinesisVideo::Stream +* AWS::Lightsail::Alarm +* AWS::Lightsail::Bucket +* AWS::Lightsail::LoadBalancer +* AWS::Lightsail::LoadBalancerTlsCertificate + +## Attribute Changes + +* AWS::EC2::Host HostId (__added__) +* AWS::EC2::IPAMScope IpamScopeType (__added__) +* AWS::EC2::VPNGatewayRoutePropagation Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpn-gatewayrouteprop.html + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpngatewayroutepropagation.html +* AWS::EC2::VPNGatewayRoutePropagation Id (__added__) +* AWS::IoTAnalytics::Channel Id (__added__) +* AWS::IoTAnalytics::Dataset Id (__added__) +* AWS::IoTAnalytics::Datastore Id (__added__) +* AWS::RDS::DBProxy VpcId (__deleted__) +* AWS::Redshift::EventSubscription EventCategoriesList.DuplicatesAllowed (__added__) + +## Property Changes + +* AWS::AppSync::FunctionConfiguration MaxBatchSize (__added__) +* AWS::AppSync::Resolver MaxBatchSize (__added__) +* AWS::AutoScaling::WarmPool InstanceReusePolicy (__added__) +* AWS::Config::OrganizationConfigRule OrganizationCustomCodeRuleMetadata (__added__) +* AWS::EC2::ClientVpnEndpoint ClientLoginBannerOptions (__added__) +* AWS::EC2::ClientVpnEndpoint SessionTimeoutHours (__added__) +* AWS::EC2::FlowLog DestinationOptions (__added__) +* AWS::EC2::IPAMScope IpamScopeType (__deleted__) +* AWS::EC2::VPNGatewayRoutePropagation RouteTableIds.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpn-gatewayrouteprop.html#cfn-ec2-vpngatewayrouteprop-routetableids + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpngatewayroutepropagation.html#cfn-ec2-vpngatewayroutepropagation-routetableids +* AWS::EC2::VPNGatewayRoutePropagation VpnGatewayId.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpn-gatewayrouteprop.html#cfn-ec2-vpngatewayrouteprop-vpngatewayid + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpngatewayroutepropagation.html#cfn-ec2-vpngatewayroutepropagation-vpngatewayid +* AWS::Events::EventBus Tags (__added__) +* AWS::GameLift::GameSessionQueue Tags (__added__) +* AWS::GameLift::MatchmakingConfiguration Tags (__added__) +* AWS::GameLift::MatchmakingRuleSet Tags (__added__) +* AWS::GameLift::Script Tags (__added__) +* AWS::IoT::JobTemplate JobExecutionsRetryConfig (__added__) +* AWS::IoTAnalytics::Channel Tags.DuplicatesAllowed (__added__) +* AWS::IoTAnalytics::Dataset Actions.DuplicatesAllowed (__added__) +* AWS::IoTAnalytics::Dataset ContentDeliveryRules.DuplicatesAllowed (__added__) +* AWS::IoTAnalytics::Dataset LateDataRules.DuplicatesAllowed (__added__) +* AWS::IoTAnalytics::Dataset Tags.DuplicatesAllowed (__added__) +* AWS::IoTAnalytics::Dataset Triggers.DuplicatesAllowed (__added__) +* AWS::IoTAnalytics::Datastore Tags.DuplicatesAllowed (__added__) +* AWS::RUM::AppMonitor Domain.Required (__changed__) + * Old: false + * New: true +* AWS::RUM::AppMonitor Name.Required (__changed__) + * Old: false + * New: true +* AWS::Redshift::EventSubscription EventCategories.DuplicatesAllowed (__added__) + +## Property Type Changes + +* AWS::KinesisAnalyticsV2::Application.CustomArtifactsConfiguration (__removed__) +* AWS::AutoScaling::WarmPool.InstanceReusePolicy (__added__) +* AWS::Config::OrganizationConfigRule.OrganizationCustomCodeRuleMetadata (__added__) +* AWS::EC2::ClientVpnEndpoint.ClientLoginBannerOptions (__added__) +* AWS::EC2::LaunchTemplate.InstanceRequirements (__added__) +* AWS::Events::EventBus.TagEntry (__added__) +* AWS::Glue::Crawler.MongoDBTarget (__added__) +* AWS::IoTSiteWise::Gateway.GreengrassV2 (__added__) +* AWS::NimbleStudio::LaunchProfile.StreamConfigurationSessionStorage (__added__) +* AWS::NimbleStudio::LaunchProfile.StreamingSessionStorageRoot (__added__) +* AWS::AppSync::Resolver.CachingConfig Ttl.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::LaunchTemplate.LaunchTemplateData InstanceRequirements (__added__) +* AWS::Glue::Crawler.S3Target DlqEventQueueArn (__added__) +* AWS::Glue::Crawler.S3Target EventQueueArn (__added__) +* AWS::Glue::Crawler.S3Target SampleSize (__added__) +* AWS::Glue::Crawler.Targets MongoDBTargets (__added__) +* AWS::IoTAnalytics::Dataset.ContainerAction Variables.DuplicatesAllowed (__added__) +* AWS::IoTAnalytics::Dataset.DatasetContentVersionValue DatasetName.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-variable-datasetcontentversionvalue.html#cfn-iotanalytics-dataset-variable-datasetcontentversionvalue-datasetname + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-datasetcontentversionvalue.html#cfn-iotanalytics-dataset-datasetcontentversionvalue-datasetname +* AWS::IoTAnalytics::Dataset.DatasetContentVersionValue DatasetName.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Dataset.OutputFileUriValue FileName.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-variable-outputfileurivalue.html#cfn-iotanalytics-dataset-variable-outputfileurivalue-filename + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-outputfileurivalue.html#cfn-iotanalytics-dataset-outputfileurivalue-filename +* AWS::IoTAnalytics::Dataset.OutputFileUriValue FileName.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Dataset.QueryAction Filters.DuplicatesAllowed (__added__) +* AWS::IoTAnalytics::Dataset.Schedule ScheduleExpression.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-trigger-schedule.html#cfn-iotanalytics-dataset-trigger-schedule-scheduleexpression + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-schedule.html#cfn-iotanalytics-dataset-schedule-scheduleexpression +* AWS::IoTAnalytics::Datastore.DatastorePartitions Partitions.DuplicatesAllowed (__added__) +* AWS::IoTAnalytics::Datastore.IotSiteWiseMultiLayerStorage CustomerManagedS3Storage.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Datastore.SchemaDefinition Columns.DuplicatesAllowed (__added__) +* AWS::IoTAnalytics::Pipeline.AddAttributes Attributes.PrimitiveType (__deleted__) +* AWS::IoTAnalytics::Pipeline.AddAttributes Attributes.PrimitiveItemType (__added__) +* AWS::IoTAnalytics::Pipeline.AddAttributes Attributes.Type (__added__) +* AWS::IoTAnalytics::Pipeline.AddAttributes Attributes.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.AddAttributes Name.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.Channel ChannelName.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.Channel Name.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.Datastore DatastoreName.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.Datastore Name.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.DeviceRegistryEnrich Attribute.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.DeviceRegistryEnrich Name.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.DeviceRegistryEnrich RoleArn.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.DeviceRegistryEnrich ThingName.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.DeviceShadowEnrich Attribute.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.DeviceShadowEnrich Name.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.DeviceShadowEnrich RoleArn.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.DeviceShadowEnrich ThingName.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.Filter Filter.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.Filter Name.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.Lambda BatchSize.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.Lambda LambdaName.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.Lambda Name.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.Math Attribute.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.Math Math.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.Math Name.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.RemoveAttributes Attributes.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.RemoveAttributes Name.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.SelectAttributes Attributes.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Pipeline.SelectAttributes Name.Required (__changed__) + * Old: false + * New: true +* AWS::IoTSiteWise::Gateway.GatewayPlatform GreengrassV2 (__added__) +* AWS::IoTSiteWise::Gateway.GatewayPlatform Greengrass.Required (__changed__) + * Old: true + * New: false +* AWS::KinesisAnalyticsV2::Application.ZeppelinApplicationConfiguration CustomArtifactsConfiguration.ItemType (__added__) +* AWS::KinesisAnalyticsV2::Application.ZeppelinApplicationConfiguration CustomArtifactsConfiguration.Type (__changed__) + * Old: CustomArtifactsConfiguration + * New: List +* AWS::Lex::BotAlias.BotAliasLocaleSettingsItem BotAliasLocaleSetting.Required (__changed__) + * Old: false + * New: true +* AWS::Lex::BotAlias.BotAliasLocaleSettingsItem LocaleId.Required (__changed__) + * Old: false + * New: true +* AWS::Lex::BotAlias.TextLogDestination CloudWatch.Type (__added__) +* AWS::MediaLive::Channel.HlsGroupSettings ProgramDateTimeClock (__added__) +* AWS::MediaLive::Channel.InputSettings Scte35Pid (__added__) +* AWS::NimbleStudio::LaunchProfile.StreamConfiguration MaxStoppedSessionLengthInMinutes (__added__) +* AWS::NimbleStudio::LaunchProfile.StreamConfiguration SessionStorage (__added__) +* AWS::Transfer::Server.ProtocolDetails TlsSessionResumptionMode (__added__) + +## Unapplied changes + +* AWS::ECS is at 51.0.0 +* AWS::SageMaker is at 51.0.0 + + +# Serverless Application Model (SAM) Resource Specification v2016-10-31 + +## New Resource Types + + +## Attribute Changes + + +## Property Changes + +* AWS::Serverless::Function Architectures (__added__) + +## Property Type Changes + + # CloudFormation Resource Specification v51.0.0 ## New Resource Types diff --git a/packages/@aws-cdk/cfnspec/build-tools/create-missing-libraries.ts b/packages/@aws-cdk/cfnspec/build-tools/create-missing-libraries.ts index 656d5ae16a53b..6c0ac50aa9ee9 100644 --- a/packages/@aws-cdk/cfnspec/build-tools/create-missing-libraries.ts +++ b/packages/@aws-cdk/cfnspec/build-tools/create-missing-libraries.ts @@ -21,7 +21,9 @@ async function main() { } // eslint-disable-next-line @typescript-eslint/no-require-imports - const version = require('../package.json').version; + const cfnSpecPkgJson = require('../package.json'); + const version = cfnSpecPkgJson.version; + const jestTypesVersion = cfnSpecPkgJson.devDependencies['@types/jest']; // iterate over all cloudformation namespaces for (const namespace of cfnspec.namespaces()) { @@ -172,7 +174,7 @@ async function main() { '@aws-cdk/cdk-build-tools': version, '@aws-cdk/cfn2ts': version, '@aws-cdk/pkglint': version, - '@types/jest': '^26.0.22', + '@types/jest': jestTypesVersion, }, dependencies: { '@aws-cdk/core': version, @@ -293,12 +295,11 @@ async function main() { await addDependencyToMegaPackage(path.join('@aws-cdk', 'cloudformation-include'), module.packageName, version, ['dependencies', 'peerDependencies']); await addDependencyToMegaPackage('aws-cdk-lib', module.packageName, version, ['devDependencies']); await addDependencyToMegaPackage('monocdk', module.packageName, version, ['devDependencies']); - await addDependencyToMegaPackage('decdk', module.packageName, version, ['dependencies']); } } /** - * A few of our packages (e.g., decdk, aws-cdk-lib) require a dependency on every service package. + * A few of our packages (e.g., aws-cdk-lib) require a dependency on every service package. * This automates adding the dependency (and peer dependency) to the package.json. */ async function addDependencyToMegaPackage(megaPackageName: string, packageName: string, version: string, dependencyTypes: string[]) { diff --git a/packages/@aws-cdk/cfnspec/build-tools/update.sh b/packages/@aws-cdk/cfnspec/build-tools/update.sh index 470b4d4de4d6a..449825636182d 100755 --- a/packages/@aws-cdk/cfnspec/build-tools/update.sh +++ b/packages/@aws-cdk/cfnspec/build-tools/update.sh @@ -61,13 +61,17 @@ update-spec \ spec-source/specification/000_cfn/000_official \ true true +old_version=$(cat cfn.version) +new_version=$(node -p "require('${scriptdir}/../spec-source/specification/000_cfn/000_official/001_Version.json').ResourceSpecificationVersion") echo >&2 "Recording new version..." rm -f cfn.version -node -p "require('${scriptdir}/../spec-source/specification/000_cfn/000_official/001_Version.json').ResourceSpecificationVersion" > cfn.version +echo "$new_version" > cfn.version - -echo >&2 "Reporting outdated specs..." -node build-tools/report-issues spec-source/specification/000_cfn/000_official/ outdated >> CHANGELOG.md.new +# Only report outdated specs if we made changes, otherwise we're stuck reporting changes every time. +if [[ "$new_version" != "$old_version" ]]; then + echo >&2 "Reporting outdated specs..." + node build-tools/report-issues spec-source/specification/000_cfn/000_official/ outdated >> CHANGELOG.md.new +fi update-spec \ "Serverless Application Model (SAM) Resource Specification" \ @@ -85,8 +89,7 @@ node ${scriptdir}/create-missing-libraries.js || { exit 1 } -# update decdk dep list -(cd ${scriptdir}/../../../decdk && node ./deps.js || true) +# update monocdk dep list (cd ${scriptdir}/../../../monocdk && yarn gen || true) # append old changelog after new and replace as the last step because otherwise we will not be idempotent diff --git a/packages/@aws-cdk/cfnspec/cfn.version b/packages/@aws-cdk/cfnspec/cfn.version index 92837e4d8f0cc..81168fdcef41f 100644 --- a/packages/@aws-cdk/cfnspec/cfn.version +++ b/packages/@aws-cdk/cfnspec/cfn.version @@ -1 +1 @@ -51.0.0 +57.0.0 diff --git a/packages/@aws-cdk/cfnspec/package.json b/packages/@aws-cdk/cfnspec/package.json index 879c7b8e436c7..f299732ab20a0 100644 --- a/packages/@aws-cdk/cfnspec/package.json +++ b/packages/@aws-cdk/cfnspec/package.json @@ -32,12 +32,12 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", "@types/fs-extra": "^8.1.2", - "@types/jest": "^27.0.3", - "@types/md5": "^2.3.1", + "@types/jest": "^27.4.1", + "@types/md5": "^2.3.2", "fast-json-patch": "^2.2.1", - "jest": "^27.4.5", + "jest": "^27.5.1", "json-diff": "^0.7.1", - "sort-json": "^2.0.0" + "sort-json": "^2.0.1" }, "dependencies": { "fs-extra": "^9.1.0", @@ -45,7 +45,8 @@ }, "repository": { "url": "https://github.com/aws/aws-cdk.git", - "type": "git" + "type": "git", + "directory": "packages/@aws-cdk/cfnspec" }, "license": "Apache-2.0", "author": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/cfn-docs/cfn-docs.json b/packages/@aws-cdk/cfnspec/spec-source/cfn-docs/cfn-docs.json index aca564579db44..a4e99585786e1 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/cfn-docs/cfn-docs.json +++ b/packages/@aws-cdk/cfnspec/spec-source/cfn-docs/cfn-docs.json @@ -512,7 +512,7 @@ "properties": { "AccessToken": "Personal Access token for 3rd party source control system for an Amplify app, used to create webhook and read-only deploy key. Token is not stored.\n\n*Length Constraints:* Minimum length of 1. Maximum length of 255.", "AutoBranchCreationConfig": "Sets the configuration for your automatic branch creation.", - "BasicAuthConfig": "The credentials for basic authorization for an Amplify app.", + "BasicAuthConfig": "The credentials for basic authorization for an Amplify app. You must base64-encode the authorization credentials and provide them in the format `user:password` .", "BuildSpec": "The build specification (build spec) for an Amplify app.\n\n*Length Constraints:* Minimum length of 1. Maximum length of 25000.\n\n*Pattern:* (?s).+", "CustomHeaders": "The custom HTTP headers for an Amplify app.\n\n*Length Constraints:* Minimum length of 0. Maximum length of 25000.\n\n*Pattern:* (?s).*", "CustomRules": "The custom rewrite and redirect rules for an Amplify app.", @@ -577,7 +577,7 @@ "description": "The AWS::Amplify::Branch resource creates a new branch within an app.", "properties": { "AppId": "The unique ID for an Amplify app.\n\n*Length Constraints:* Minimum length of 1. Maximum length of 20.\n\n*Pattern:* d[a-z0-9]+", - "BasicAuthConfig": "The basic authorization credentials for a branch of an Amplify app.", + "BasicAuthConfig": "The basic authorization credentials for a branch of an Amplify app. You must base64-encode the authorization credentials and provide them in the format `user:password` .", "BranchName": "The name for the branch.\n\n*Length Constraints:* Minimum length of 1. Maximum length of 255.\n\n*Pattern:* (?s).+", "BuildSpec": "The build specification (build spec) for the branch.\n\n*Length Constraints:* Minimum length of 1. Maximum length of 25000.\n\n*Pattern:* (?s).+", "Description": "The description for the branch that is part of an Amplify app.\n\n*Length Constraints:* Maximum length of 1000.\n\n*Pattern:* (?s).*", @@ -647,13 +647,13 @@ }, "description": "The AWS::AmplifyUIBuilder::Component resource specifies a component within an Amplify app. A component is a user interface (UI) element that you can customize. Use `ComponentChild` to configure an instance of a `Component` . A `ComponentChild` instance inherits the configuration of the main `Component` .", "properties": { - "BindingProperties": "The information to connect a component's properties to data at runtime.", + "BindingProperties": "The information to connect a component's properties to data at runtime. You can't specify `tags` as a valid property for `bindingProperties` .", "Children": "A list of the component's `ComponentChild` instances.", - "CollectionProperties": "The data binding configuration for the component's properties. Use this for a collection component.", + "CollectionProperties": "The data binding configuration for the component's properties. Use this for a collection component. You can't specify `tags` as a valid property for `collectionProperties` .", "ComponentType": "The type of the component. This can be an Amplify custom UI component or another custom component.", "Name": "The name of the component.", - "Overrides": "Describes the component's properties that can be overriden in a customized instance of the component.", - "Properties": "Describes the component's properties.", + "Overrides": "Describes the component's properties that can be overriden in a customized instance of the component. You can't specify `tags` as a valid property for `overrides` .", + "Properties": "Describes the component's properties. You can't specify `tags` as a valid property for `properties` .", "SourceId": "The unique ID of the component in its original source system, such as Figma.", "Tags": "One or more key-value pairs to use when tagging the component.", "Variants": "A list of the component's variants. A variant is a unique style configuration of a main component." @@ -688,7 +688,7 @@ "Children": "The list of `ComponentChild` instances for this component.", "ComponentType": "The type of the child component.", "Name": "The name of the child component.", - "Properties": "Describes the properties of the child component." + "Properties": "Describes the properties of the child component. You can't specify `tags` as a valid property for `properties` ." } }, "AWS::AmplifyUIBuilder::Component.ComponentConditionProperty": { @@ -759,7 +759,7 @@ "attributes": {}, "description": "The `ComponentVariant` property specifies the style configuration of a unique variation of a main component.", "properties": { - "Overrides": "The properties of the component variant that can be overriden when customizing an instance of the component.", + "Overrides": "The properties of the component variant that can be overriden when customizing an instance of the component. You can't specify `tags` as a valid property for `overrides` .", "VariantValues": "The combination of variants that comprise this variant." } }, @@ -827,7 +827,7 @@ }, "AWS::ApiGateway::Account": { "attributes": { - "Id": "", + "Id": "The ID for the account. For example: `abc123` .", "Ref": "`Ref` returns the ID of the resource, such as `mysta-accou-01234b567890example` ." }, "description": "The `AWS::ApiGateway::Account` resource specifies the IAM role that Amazon API Gateway uses to write API logs to Amazon CloudWatch Logs.\n\n> If an API Gateway resource has never been created in your AWS account , you must add a dependency on another API Gateway resource, such as an [AWS::ApiGateway::RestApi](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-restapi.html) or [AWS::ApiGateway::ApiKey](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-apikey.html) resource.\n> \n> If an API Gateway resource has been created in your AWS account , no dependency is required (even if the resource was deleted).", @@ -862,7 +862,7 @@ }, "AWS::ApiGateway::Authorizer": { "attributes": { - "AuthorizerId": "", + "AuthorizerId": "The ID for the authorizer. For example: `abc123` .", "Ref": "`Ref` returns the authorizer's ID, such as `abcde1` ." }, "description": "The `AWS::ApiGateway::Authorizer` resource creates an authorization layer that API Gateway activates for methods that have authorization enabled. API Gateway activates the authorizer when a client calls those methods.", @@ -902,6 +902,7 @@ }, "AWS::ApiGateway::Deployment": { "attributes": { + "DeploymentId": "The ID for the deployment. For example: `abc123` .", "Ref": "`Ref` returns the deployment ID, such as `123abc` ." }, "description": "The `AWS::ApiGateway::Deployment` resource deploys an API Gateway `RestApi` resource to a stage so that clients can call the API over the internet. The stage acts as an environment.", @@ -975,7 +976,7 @@ "MetricsEnabled": "Indicates whether Amazon CloudWatch metrics are enabled for methods in the stage.", "Tags": "An array of arbitrary tags (key-value pairs) to associate with the stage.", "ThrottlingBurstLimit": "The target request burst rate limit. This allows more requests through for a period of time than the target rate limit. For more information, see [Manage API Request Throttling](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html) in the *API Gateway Developer Guide* .", - "ThrottlingRateLimit": "The Atarget request steady-state rate limit. For more information, see [Manage API Request Throttling](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html) in the *API Gateway Developer Guide* .", + "ThrottlingRateLimit": "The target request steady-state rate limit. For more information, see [Manage API Request Throttling](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html) in the *API Gateway Developer Guide* .", "TracingEnabled": "Specifies whether active tracing with X-ray is enabled for this stage.\n\nFor more information, see [Trace API Gateway API Execution with AWS X-Ray](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-xray.html) in the *API Gateway Developer Guide* .", "Variables": "A map that defines the stage variables. Variable names must consist of alphanumeric characters, and the values must match the following regular expression: `[A-Za-z0-9-._~:/?#&=,]+` ." } @@ -1048,7 +1049,7 @@ }, "AWS::ApiGateway::GatewayResponse": { "attributes": { - "Id": "" + "Id": "The ID for the gateway response. For example: `abc123` ." }, "description": "The `AWS::ApiGateway::GatewayResponse` resource creates a gateway response for your API. For more information, see [API Gateway Responses](https://docs.aws.amazon.com/apigateway/latest/developerguide/customize-gateway-responses.html#api-gateway-gatewayResponse-definition) in the *API Gateway Developer Guide* .", "properties": { @@ -1149,7 +1150,7 @@ "AWS::ApiGateway::Resource": { "attributes": { "Ref": "`Ref` returns the resource ID, such as `abc123` .", - "ResourceId": "" + "ResourceId": "The ID for the resource. For example: `abc123` ." }, "description": "The `AWS::ApiGateway::Resource` resource creates a resource in an API.", "properties": { @@ -1258,7 +1259,7 @@ }, "AWS::ApiGateway::UsagePlan": { "attributes": { - "Id": "", + "Id": "The ID for the usage plan. For example: `abc123` .", "Ref": "`Ref` returns the usage plan ID, such as `abc123` ." }, "description": "The `AWS::ApiGateway::UsagePlan` resource creates a usage plan for deployed APIs. A usage plan sets a target for the throttling and quota limits on individual client API keys. For more information, see [Creating and Using API Usage Plans in Amazon API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-usage-plans.html) in the *API Gateway Developer Guide* .\n\nIn some cases clients can exceed the targets that you set. Don\u2019t rely on usage plans to control costs. Consider using [AWS Budgets](https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-managing-costs.html) to monitor costs and [AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) to manage API requests.", @@ -1317,7 +1318,7 @@ "properties": { "Description": "A description of the VPC link.", "Name": "A name for the VPC link.", - "Tags": "", + "Tags": "An array of arbitrary tags (key-value pairs) to associate with the VPC link.", "TargetArns": "The ARN of network load balancer of the VPC targeted by the VPC link. The network load balancer must be owned by the same AWS account of the API owner." } }, @@ -1693,7 +1694,7 @@ "attributes": { "Ref": "`Ref` returns the application ID." }, - "description": "The `AWS::AppConfig::Application` resource creates an application, which is a logical unit of code that provides capabilities for your customers. For example, an application can be a microservice that runs on Amazon EC2 instances, a mobile application installed by your users, a serverless application using Amazon API Gateway and AWS Lambda , or any system you run on behalf of others.\n\nAWS AppConfig requires that you create resources and deploy a configuration in the following order:\n\n- Create an application\n- Create an environment\n- Create a configuration profile\n- Create a deployment strategy\n- Deploy the configuration\n\nFor more information, see [AWS AppConfig](https://docs.aws.amazon.com/appconfig/latest/userguide/what-is-appconfig.html) in the *AWS AppConfig User Guide* .", + "description": "The `AWS::AppConfig::Application` resource creates an application. In AWS AppConfig , an application is simply an organizational construct like a folder. This organizational construct has a relationship with some unit of executable code. For example, you could create an application called MyMobileApp to organize and manage configuration data for a mobile application installed by your users.\n\nAWS AppConfig requires that you create resources and deploy a configuration in the following order:\n\n- Create an application\n- Create an environment\n- Create a configuration profile\n- Create a deployment strategy\n- Deploy the configuration\n\nFor more information, see [AWS AppConfig](https://docs.aws.amazon.com/appconfig/latest/userguide/what-is-appconfig.html) in the *AWS AppConfig User Guide* .", "properties": { "Description": "A description of the application.", "Name": "A name for the application.", @@ -1720,7 +1721,7 @@ "Name": "A name for the configuration profile.", "RetrievalRoleArn": "The ARN of an IAM role with permission to access the configuration at the specified `LocationUri` .\n\n> A retrieval role ARN is not required for configurations stored in the AWS AppConfig hosted configuration store. It is required for all other sources that store your configuration.", "Tags": "Metadata to assign to the configuration profile. Tags help organize and categorize your AWS AppConfig resources. Each tag consists of a key and an optional value, both of which you define.", - "Type": "The type of configurations that the configuration profile contains. A configuration can be a feature flag used for enabling or disabling new features or a freeform configuration used to introduce changes to your application.", + "Type": "The type of configurations contained in the profile. AWS AppConfig supports `feature flags` and `freeform` configurations. We recommend you create feature flag configurations to enable or disable new features and freeform configurations to distribute configurations to an application. When calling this API, enter one of the following values for `Type` :\n\n`AWS.AppConfig.FeatureFlags`\n\n`AWS.Freeform`", "Validators": "A list of methods for validating the configuration." } }, @@ -1734,7 +1735,7 @@ }, "AWS::AppConfig::ConfigurationProfile.Validators": { "attributes": {}, - "description": "A validator provides a syntactic or semantic check to ensure the configuration that you want to deploy functions as intended. To validate your application configuration data, you provide a schema or a Lambda function that runs against the configuration. The configuration deployment or update can only proceed when the configuration data is valid.", + "description": "A validator provides a syntactic or semantic check to ensure the configuration that you want to deploy functions as intended. To validate your application configuration data, you provide a schema or an AWS Lambda function that runs against the configuration. The configuration deployment or update can only proceed when the configuration data is valid.", "properties": { "Content": "Either the JSON Schema content or the Amazon Resource Name (ARN) of an Lambda function.", "Type": "AWS AppConfig supports validators of type `JSON_SCHEMA` and `LAMBDA`" @@ -1820,7 +1821,7 @@ "attributes": { "Ref": "`Ref` returns the version number." }, - "description": "Create a new configuration in the AWS AppConfig hosted configuration store. Configurations must be 64 KB or smaller. The AWS AppConfig hosted configuration store provides the following benefits over other configuration store options.\n\n- You don't need to set up and configure other services such as Amazon Simple Storage Service ( Amazon S3 ) or Parameter Store.\n- You don't need to configure AWS Identity and Access Management ( IAM ) permissions to use the configuration store.\n- You can store configurations in any content type.\n- There is no cost to use the store.\n- You can create a configuration and add it to the store when you create a configuration profile.", + "description": "Create a new configuration in the AWS AppConfig hosted configuration store. Configurations must be 1 MB or smaller. The AWS AppConfig hosted configuration store provides the following benefits over other configuration store options.\n\n- You don't need to set up and configure other services such as Amazon Simple Storage Service ( Amazon S3 ) or Parameter Store.\n- You don't need to configure AWS Identity and Access Management ( IAM ) permissions to use the configuration store.\n- You can store configurations in any content type.\n- There is no cost to use the store.\n- You can create a configuration and add it to the store when you create a configuration profile.", "properties": { "ApplicationId": "The application ID.", "ConfigurationProfileId": "The configuration profile ID.", @@ -2521,6 +2522,31 @@ "Object": "The object specified in the Zendesk flow source." } }, + "AWS::AppIntegrations::DataIntegration": { + "attributes": { + "DataIntegrationArn": "The Amazon Resource Name (ARN) for the DataIntegration.", + "Id": "A unique identifier.", + "Ref": "`Ref` returns the DataIntegration name. For example:\n\n`{ \"Ref\": \"myDataIntegrationName\" }`" + }, + "description": "Creates and persists a DataIntegration resource.", + "properties": { + "Description": "A description of the DataIntegration.", + "KmsKey": "The KMS key for the DataIntegration.", + "Name": "The name of the DataIntegration.", + "ScheduleConfig": "The name of the data and how often it should be pulled from the source.", + "SourceURI": "The URI of the data source.", + "Tags": "An array of key-value pairs to apply to this resource.\n\nFor more information, see [Tag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) ." + } + }, + "AWS::AppIntegrations::DataIntegration.ScheduleConfig": { + "attributes": {}, + "description": "The name of the data and how often it should be pulled from the source.", + "properties": { + "FirstExecutionFrom": "The start date for objects to import in the first flow run as an Unix/epoch timestamp in milliseconds or in ISO-8601 format.", + "Object": "The name of the object to pull from the data source.", + "ScheduleExpression": "How often the data should be pulled from data source." + } + }, "AWS::AppIntegrations::EventIntegration": { "attributes": { "Associations": "The association status of the event integration, returned as an array of EventIntegrationAssociation objects.", @@ -3752,13 +3778,14 @@ }, "description": "Specify an AWS App Runner service by using the `AWS::AppRunner::Service` resource in an AWS CloudFormation template. \n\nThe `AWS::AppRunner::Service` resource is an AWS App Runner resource type that specifies an App Runner service.", "properties": { - "AutoScalingConfigurationArn": "The Amazon Resource Name (ARN) of an App Runner automatic scaling configuration resource that you want to associate with your service. If not provided, App Runner associates the latest revision of a default auto scaling configuration.", + "AutoScalingConfigurationArn": "The Amazon Resource Name (ARN) of an App Runner automatic scaling configuration resource that you want to associate with the App Runner service. If not provided, App Runner associates the latest revision of a default auto scaling configuration.", "EncryptionConfiguration": "An optional custom encryption key that App Runner uses to encrypt the copy of your source repository that it maintains and your service logs. By default, App Runner uses an AWS managed key .", - "HealthCheckConfiguration": "The settings for the health check that AWS App Runner performs to monitor the health of your service.", + "HealthCheckConfiguration": "The settings for the health check that AWS App Runner performs to monitor the health of the App Runner service.", "InstanceConfiguration": "The runtime configuration of instances (scaling units) of the App Runner service.", - "ServiceName": "A name for the new service. It must be unique across all the running App Runner services in your AWS account in the AWS Region .", + "NetworkConfiguration": "Configuration settings related to network traffic of the web application that the App Runner service runs.", + "ServiceName": "A name for the App Runner service. It must be unique across all the running App Runner services in your AWS account in the AWS Region .\n\nIf you don't specify a name, AWS CloudFormation generates a name for your Service.", "SourceConfiguration": "The source to deploy to the App Runner service. It can be a code or an image repository.", - "Tags": "An optional list of metadata items that you can associate with your service resource. A tag is a key-value pair." + "Tags": "An optional list of metadata items that you can associate with the App Runner service resource. A tag is a key-value pair." } }, "AWS::AppRunner::Service.AuthenticationConfiguration": { @@ -3797,6 +3824,14 @@ "SourceCodeVersion": "The version that should be used within the source code repository." } }, + "AWS::AppRunner::Service.EgressConfiguration": { + "attributes": {}, + "description": "Describes configuration settings related to outbound network traffic of an AWS App Runner service.", + "properties": { + "EgressType": "The type of egress configuration.\n\nSet to `DEFAULT` for access to resources hosted on public networks.\n\nSet to `VPC` to associate your service to a custom VPC specified by `VpcConnectorArn` .", + "VpcConnectorArn": "The Amazon Resource Name (ARN) of the App Runner VPC connector that you want to associate with your App Runner service. Only valid when `EgressType = VPC` ." + } + }, "AWS::AppRunner::Service.EncryptionConfiguration": { "attributes": {}, "description": "Describes a custom encryption key that AWS App Runner uses to encrypt copies of the source repository and service logs.", @@ -3851,6 +3886,13 @@ "Value": "The value string to which the key name is mapped." } }, + "AWS::AppRunner::Service.NetworkConfiguration": { + "attributes": {}, + "description": "Describes configuration settings related to network traffic of an AWS App Runner service. Consists of embedded objects for each configurable network feature.", + "properties": { + "EgressConfiguration": "Network configuration settings for outbound message traffic." + } + }, "AWS::AppRunner::Service.SourceCodeVersion": { "attributes": {}, "description": "Identifies a version of code that AWS App Runner refers to within a source code repository.", @@ -3869,6 +3911,20 @@ "ImageRepository": "The description of a source image repository.\n\nYou must provide either this member or `CodeRepository` (but not both)." } }, + "AWS::AppRunner::VpcConnector": { + "attributes": { + "Ref": "", + "VpcConnectorArn": "The Amazon Resource Name (ARN) of this VPC connector.", + "VpcConnectorRevision": "The revision of this VPC connector. It's unique among all the active connectors ( `\"Status\": \"ACTIVE\"` ) that share the same `Name` .\n\n> At this time, App Runner supports only one revision per name." + }, + "description": "Specify an AWS App Runner VPC connector by using the `AWS::AppRunner::VpcConnector` resource in an AWS CloudFormation template. \n\nThe `AWS::AppRunner::VpcConnector` resource is an AWS App Runner resource type that specifies an App Runner VPC connector.\n\nApp Runner requires this resource when you want to associate your App Runner service to a custom Amazon Virtual Private Cloud ( Amazon VPC ).", + "properties": { + "SecurityGroups": "A list of IDs of security groups that App Runner should use for access to AWS resources under the specified subnets. If not specified, App Runner uses the default security group of the Amazon VPC. The default security group allows all outbound traffic.", + "Subnets": "A list of IDs of subnets that App Runner should use when it associates your service with a custom Amazon VPC. Specify IDs of subnets of a single Amazon VPC. App Runner determines the Amazon VPC from the subnets you specify.", + "Tags": "A list of metadata items that you can associate with your VPC connector resource. A tag is a key-value pair.", + "VpcConnectorName": "A name for the VPC connector.\n\nIf you don't specify a name, AWS CloudFormation generates a name for your VPC connector." + } + }, "AWS::AppStream::AppBlock": { "attributes": { "Arn": "The ARN of the app block.", @@ -3933,6 +3989,17 @@ "S3Key": "The S3 key of the S3 object." } }, + "AWS::AppStream::ApplicationEntitlementAssociation": { + "attributes": { + "Ref": "When you pass the logical ID of this resource to the intrinsic `Ref` function, `Ref` returns the combination of the `StackName` , `EntitlementName` , and `ApplicationIdentifier` , such as `abcdefStack|abcdefEntitlement|abcdefApplication` .\n\nFor more information about using the `Ref` function, see [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) ." + }, + "description": "Associates an application to an entitlement.", + "properties": { + "ApplicationIdentifier": "The identifier of the application.", + "EntitlementName": "The name of the entitlement.", + "StackName": "The name of the stack." + } + }, "AWS::AppStream::ApplicationFleetAssociation": { "attributes": { "Ref": "When you pass the logical ID of this resource to the intrinsic `Ref` function, `Ref` returns a combination of the `FleetName` and `ApplicationArn` , such as `aabcdefgFleet|arn:aws:appstream:us-west-2:123456789123:application/abcdefg` .\n\nFor more information about using the `Ref` function, see [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) ." @@ -3960,6 +4027,29 @@ "AccountPassword": "The password for the account." } }, + "AWS::AppStream::Entitlement": { + "attributes": { + "CreatedTime": "The time when the entitlement was created.", + "LastModifiedTime": "The time when the entitlement was last modified.", + "Ref": "When you pass the logical ID of this resource to the intrinsic `Ref` function, `Ref` returns the combination of the `StackName` and `Name` , such as `abcdefStack|abcdefEntitlement` .\n\nFor more information about using the `Ref` function, see [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) ." + }, + "description": "Creates an entitlement to control access, based on user attributes, to specific applications within a stack. Entitlements apply to SAML 2.0 federated user identities. Amazon AppStream 2.0 user pool and streaming URL users are entitled to all applications in a stack. Entitlements don't apply to the desktop stream view application or to applications managed by a dynamic app provider using the Dynamic Application Framework.", + "properties": { + "AppVisibility": "Specifies whether to entitle all apps or only selected apps.", + "Attributes": "The attributes of the entitlement.", + "Description": "The description of the entitlement.", + "Name": "The name of the entitlement.", + "StackName": "The name of the stack." + } + }, + "AWS::AppStream::Entitlement.Attribute": { + "attributes": {}, + "description": "An attribute that belongs to an entitlement. Application entitlements work by matching a supported SAML 2.0 attribute name to a value when a user identity federates to an AppStream 2.0 SAML application.", + "properties": { + "Name": "A supported AWS IAM SAML PrincipalTag attribute that is matched to a value when a user identity federates to an AppStream 2.0 SAML application.\n\nThe following are supported values:\n\n- roles\n- department\n- organization\n- groups\n- title\n- costCenter\n- userType", + "Value": "A value that is matched to a supported SAML attribute name when a user identity federates to an AppStream 2.0 SAML application." + } + }, "AWS::AppStream::Fleet": { "attributes": {}, "description": "The `AWS::AppStream::Fleet` resource creates a fleet for Amazon AppStream 2.0. A fleet consists of streaming instances that run a specified image when using Always-On or On-Demand.", @@ -4304,6 +4394,7 @@ "DataSourceName": "The name of data source this function will attach.", "Description": "The `Function` description.", "FunctionVersion": "The version of the request mapping template. Currently, only the 2018-05-29 version of the template is supported.", + "MaxBatchSize": "The maximum number of resolver request inputs that will be sent to a single AWS Lambda function in a `BatchInvoke` operation.", "Name": "The name of the function.", "RequestMappingTemplate": "The `Function` request mapping template. Functions support only the 2018-05-29 version of the request mapping template.", "RequestMappingTemplateS3Location": "Describes a Sync configuration for a resolver.\n\nContains information on which Conflict Detection, as well as Resolution strategy, should be performed when the resolver is invoked.", @@ -4440,6 +4531,7 @@ "DataSourceName": "The resolver data source name.", "FieldName": "The GraphQL field on a type that invokes the resolver.", "Kind": "The resolver type.\n\n- *UNIT* : A UNIT resolver type. A UNIT resolver is the default resolver type. You can use a UNIT resolver to run a GraphQL query against a single data source.\n- *PIPELINE* : A PIPELINE resolver type. You can use a PIPELINE resolver to invoke a series of `Function` objects in a serial manner. You can use a pipeline resolver to run a GraphQL query against multiple data sources.", + "MaxBatchSize": "The maximum number of resolver request inputs that will be sent to a single AWS Lambda function in a `BatchInvoke` operation.", "PipelineConfig": "Functions linked with the pipeline resolver.", "RequestMappingTemplate": "The request mapping template.\n\nRequest mapping templates are optional when using a Lambda data source. For all other data sources, a request mapping template is required.", "RequestMappingTemplateS3Location": "The location of a request mapping template in an Amazon S3 bucket. Use this if you want to provision with a template file in Amazon S3 rather than embedding it in your CloudFormation template.", @@ -4531,7 +4623,7 @@ }, "description": "The `AWS::ApplicationAutoScaling::ScalingPolicy` resource defines a scaling policy that Application Auto Scaling uses to adjust the capacity of a scalable target.\n\nFor more information, see [PutScalingPolicy](https://docs.aws.amazon.com/autoscaling/application/APIReference/API_PutScalingPolicy.html) in the *Application Auto Scaling API Reference* . For more information about Application Auto Scaling scaling policies, see [Target tracking scaling policies](https://docs.aws.amazon.com/autoscaling/application/userguide/application-auto-scaling-target-tracking.html) and [Step scaling policies](https://docs.aws.amazon.com/autoscaling/application/userguide/application-auto-scaling-step-scaling-policies.html) in the *Application Auto Scaling User Guide* .", "properties": { - "PolicyName": "The name of the scaling policy.", + "PolicyName": "The name of the scaling policy.\n\nUpdates to the name of a target tracking scaling policy are not supported, unless you also update the metric used for scaling. To change only a target tracking scaling policy's name, first delete the policy by removing the existing `AWS::ApplicationAutoScaling::ScalingPolicy` resource from the template and updating the stack. Then, recreate the resource with the same settings and a different name.", "PolicyType": "The scaling policy type.\n\nThe following policy types are supported:\n\n`TargetTrackingScaling` \u2014Not supported for Amazon EMR\n\n`StepScaling` \u2014Not supported for DynamoDB, Amazon Comprehend, Lambda, Amazon Keyspaces, Amazon MSK, Amazon ElastiCache, or Neptune.", "ResourceId": "The identifier of the resource associated with the scaling policy. This string consists of the resource type and unique identifier.\n\n- ECS service - The resource type is `service` and the unique identifier is the cluster name and service name. Example: `service/default/sample-webapp` .\n- Spot Fleet - The resource type is `spot-fleet-request` and the unique identifier is the Spot Fleet request ID. Example: `spot-fleet-request/sfr-73fbd2ce-aa30-494c-8788-1cee4EXAMPLE` .\n- EMR cluster - The resource type is `instancegroup` and the unique identifier is the cluster ID and instance group ID. Example: `instancegroup/j-2EEZNYKUA1NTV/ig-1791Y4E1L8YI0` .\n- AppStream 2.0 fleet - The resource type is `fleet` and the unique identifier is the fleet name. Example: `fleet/sample-fleet` .\n- DynamoDB table - The resource type is `table` and the unique identifier is the table name. Example: `table/my-table` .\n- DynamoDB global secondary index - The resource type is `index` and the unique identifier is the index name. Example: `table/my-table/index/my-table-index` .\n- Aurora DB cluster - The resource type is `cluster` and the unique identifier is the cluster name. Example: `cluster:my-db-cluster` .\n- SageMaker endpoint variant - The resource type is `variant` and the unique identifier is the resource ID. Example: `endpoint/my-end-point/variant/KMeansClustering` .\n- Custom resources are not supported with a resource type. This parameter must specify the `OutputValue` from the CloudFormation template stack used to access the resources. The unique identifier is defined by the service provider. More information is available in our [GitHub repository](https://docs.aws.amazon.com/https://github.com/aws/aws-auto-scaling-custom-resource) .\n- Amazon Comprehend document classification endpoint - The resource type and unique identifier are specified using the endpoint ARN. Example: `arn:aws:comprehend:us-west-2:123456789012:document-classifier-endpoint/EXAMPLE` .\n- Amazon Comprehend entity recognizer endpoint - The resource type and unique identifier are specified using the endpoint ARN. Example: `arn:aws:comprehend:us-west-2:123456789012:entity-recognizer-endpoint/EXAMPLE` .\n- Lambda provisioned concurrency - The resource type is `function` and the unique identifier is the function name with a function version or alias name suffix that is not `$LATEST` . Example: `function:my-function:prod` or `function:my-function:1` .\n- Amazon Keyspaces table - The resource type is `table` and the unique identifier is the table name. Example: `keyspace/mykeyspace/table/mytable` .\n- Amazon MSK cluster - The resource type and unique identifier are specified using the cluster ARN. Example: `arn:aws:kafka:us-east-1:123456789012:cluster/demo-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-5` .\n- Amazon ElastiCache replication group - The resource type is `replication-group` and the unique identifier is the replication group name. Example: `replication-group/mycluster` .\n- Neptune cluster - The resource type is `cluster` and the unique identifier is the cluster name. Example: `cluster:mycluster` .", "ScalableDimension": "The scalable dimension. This string consists of the service namespace, resource type, and scaling property.\n\n- `ecs:service:DesiredCount` - The desired task count of an ECS service.\n- `elasticmapreduce:instancegroup:InstanceCount` - The instance count of an EMR Instance Group.\n- `ec2:spot-fleet-request:TargetCapacity` - The target capacity of a Spot Fleet.\n- `appstream:fleet:DesiredCapacity` - The desired capacity of an AppStream 2.0 fleet.\n- `dynamodb:table:ReadCapacityUnits` - The provisioned read capacity for a DynamoDB table.\n- `dynamodb:table:WriteCapacityUnits` - The provisioned write capacity for a DynamoDB table.\n- `dynamodb:index:ReadCapacityUnits` - The provisioned read capacity for a DynamoDB global secondary index.\n- `dynamodb:index:WriteCapacityUnits` - The provisioned write capacity for a DynamoDB global secondary index.\n- `rds:cluster:ReadReplicaCount` - The count of Aurora Replicas in an Aurora DB cluster. Available for Aurora MySQL-compatible edition and Aurora PostgreSQL-compatible edition.\n- `sagemaker:variant:DesiredInstanceCount` - The number of EC2 instances for a SageMaker model endpoint variant.\n- `custom-resource:ResourceType:Property` - The scalable dimension for a custom resource provided by your own application or service.\n- `comprehend:document-classifier-endpoint:DesiredInferenceUnits` - The number of inference units for an Amazon Comprehend document classification endpoint.\n- `comprehend:entity-recognizer-endpoint:DesiredInferenceUnits` - The number of inference units for an Amazon Comprehend entity recognizer endpoint.\n- `lambda:function:ProvisionedConcurrency` - The provisioned concurrency for a Lambda function.\n- `cassandra:table:ReadCapacityUnits` - The provisioned read capacity for an Amazon Keyspaces table.\n- `cassandra:table:WriteCapacityUnits` - The provisioned write capacity for an Amazon Keyspaces table.\n- `kafka:broker-storage:VolumeSize` - The provisioned volume size (in GiB) for brokers in an Amazon MSK cluster.\n- `elasticache:replication-group:NodeGroups` - The number of node groups for an Amazon ElastiCache replication group.\n- `elasticache:replication-group:Replicas` - The number of replicas per node group for an Amazon ElastiCache replication group.\n- `neptune:cluster:ReadReplicaCount` - The count of read replicas in an Amazon Neptune DB cluster.", @@ -4543,13 +4635,13 @@ }, "AWS::ApplicationAutoScaling::ScalingPolicy.CustomizedMetricSpecification": { "attributes": {}, - "description": "Contains customized metric specification information for a target tracking scaling policy for Application Auto Scaling.\n\nFor information about the available metrics for a service, see [AWS services that publish CloudWatch metrics](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/aws-services-cloudwatch-metrics.html) in the *Amazon CloudWatch User Guide* .\n\nTo create your customized metric specification:\n\n- Add values for each required parameter from CloudWatch. You can use an existing metric, or a new metric that you create. To use your own metric, you must first publish the metric to CloudWatch. For more information, see [Publish custom metrics](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/publishingMetrics.html) in the *Amazon CloudWatch User Guide* .\n- Choose a metric that changes proportionally with capacity. The value of the metric should increase or decrease in inverse proportion to the number of capacity units. That is, the value of the metric should decrease when capacity increases, and increase when capacity decreases.\n\nFor more information about CloudWatch, see [Amazon CloudWatch concepts](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html) .\n\n`CustomizedMetricSpecification` is a property of the [AWS::ApplicationAutoScaling::ScalingPolicy TargetTrackingScalingPolicyConfiguration](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationautoscaling-scalingpolicy-targettrackingscalingpolicyconfiguration.html) property type.", + "description": "Contains customized metric specification information for a target tracking scaling policy for Application Auto Scaling.\n\nFor information about the available metrics for a service, see [AWS services that publish CloudWatch metrics](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/aws-services-cloudwatch-metrics.html) in the *Amazon CloudWatch User Guide* .\n\nTo create your customized metric specification:\n\n- Add values for each required parameter from CloudWatch. You can use an existing metric, or a new metric that you create. To use your own metric, you must first publish the metric to CloudWatch. For more information, see [Publish custom metrics](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/publishingMetrics.html) in the *Amazon CloudWatch User Guide* .\n- Choose a metric that changes proportionally with capacity. The value of the metric should increase or decrease in inverse proportion to the number of capacity units. That is, the value of the metric should decrease when capacity increases, and increase when capacity decreases.\n\nFor an example of how creating new metrics can be useful, see [Scaling based on Amazon SQS](https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-using-sqs-queue.html) in the *Amazon EC2 Auto Scaling User Guide* . This topic mentions Auto Scaling groups, but the same scenario for Amazon SQS can apply to the target tracking scaling policies that you create for a Spot Fleet by using Application Auto Scaling.\n\nFor more information about the CloudWatch terminology below, see [Amazon CloudWatch concepts](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html) .\n\n`CustomizedMetricSpecification` is a property of the [AWS::ApplicationAutoScaling::ScalingPolicy TargetTrackingScalingPolicyConfiguration](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationautoscaling-scalingpolicy-targettrackingscalingpolicyconfiguration.html) property type.", "properties": { "Dimensions": "The dimensions of the metric.\n\nConditional: If you published your metric with dimensions, you must specify the same dimensions in your scaling policy.", - "MetricName": "The name of the metric.", + "MetricName": "The name of the metric. To get the exact metric name, namespace, and dimensions, inspect the [Metric](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_Metric.html) object that is returned by a call to [ListMetrics](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_ListMetrics.html) .", "Namespace": "The namespace of the metric.", "Statistic": "The statistic of the metric.", - "Unit": "The unit of the metric." + "Unit": "The unit of the metric. For a complete list of the units that CloudWatch supports, see the [MetricDatum](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_MetricDatum.html) data type in the *Amazon CloudWatch API Reference* ." } }, "AWS::ApplicationAutoScaling::ScalingPolicy.MetricDimension": { @@ -4650,7 +4742,7 @@ "ComponentName": "The name of the component.", "CustomComponentConfiguration": "Customized monitoring settings. Required if CUSTOM mode is configured in `ComponentConfigurationMode` .", "DefaultOverwriteComponentConfiguration": "Customized overwrite monitoring settings. Required if CUSTOM mode is configured in `ComponentConfigurationMode` .", - "Tier": "The tier of the application component. Supported tiers include `DOT_NET_WORKER` , `DOT_NET_WEB` , `DOT_NET_CORE` , `SQL_SERVER` , and `DEFAULT` ." + "Tier": "The tier of the application component. Supported tiers include `DOT_NET_CORE` , `DOT_NET_WORKER` , `DOT_NET_WEB` , `SQL_SERVER` , `SQL_SERVER_ALWAYSON_AVAILABILITY_GROUP` , `SQL_SERVER_FAILOVER_CLUSTER_INSTANCE` , `MYSQL` , `POSTGRESQL` , `JAVA_JMX` , `ORACLE` , `SAP_HANA_MULTI_NODE` , `SAP_HANA_SINGLE_NODE` , `SAP_HANA_HIGH_AVAILABILITY` , `SHAREPOINT` . `ACTIVE_DIRECTORY` , and `DEFAULT` ." } }, "AWS::ApplicationInsights::Application.ConfigurationDetails": { @@ -4659,6 +4751,8 @@ "properties": { "AlarmMetrics": "A list of metrics to monitor for the component. All component types can use `AlarmMetrics` .", "Alarms": "A list of alarms to monitor for the component. All component types can use `Alarm` .", + "HAClusterPrometheusExporter": "The HA cluster Prometheus Exporter settings.", + "HANAPrometheusExporter": "The HANA DB Prometheus Exporter settings.", "JMXPrometheusExporter": "A list of Java metrics to monitor for the component.", "Logs": "A list of logs to monitor for the component. Only Amazon EC2 instances can use `Logs` .", "WindowsEvents": "A list of Windows Events to monitor for the component. Only Amazon EC2 instances running on Windows can use `WindowsEvents` ." @@ -4672,6 +4766,24 @@ "ResourceList": "The list of resource ARNs that belong to the component." } }, + "AWS::ApplicationInsights::Application.HAClusterPrometheusExporter": { + "attributes": {}, + "description": "The `AWS::ApplicationInsights::Application HAClusterPrometheusExporter` property type defines the HA cluster Prometheus Exporter settings. For more information, see the [component configuration](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/component-config-sections.html#component-configuration-prometheus) in the CloudWatch Application Insights documentation.", + "properties": { + "PrometheusPort": "The target port to which Prometheus sends metrics. If not specified, the default port 9668 is used." + } + }, + "AWS::ApplicationInsights::Application.HANAPrometheusExporter": { + "attributes": {}, + "description": "The `AWS::ApplicationInsights::Application HANAPrometheusExporter` property type defines the HANA DB Prometheus Exporter settings. For more information, see the [component configuration](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/component-config-sections.html#component-configuration-prometheus) in the CloudWatch Application Insights documentation.", + "properties": { + "AgreeToInstallHANADBClient": "Designates whether you agree to install the HANA DB client.", + "HANAPort": "The HANA database port by which the exporter will query HANA metrics.", + "HANASID": "The three-character SAP system ID (SID) of the SAP HANA system.", + "HANASecretName": "The AWS Secrets Manager secret that stores HANA monitoring user credentials. The HANA Prometheus exporter uses these credentials to connect to the database and query HANA metrics.", + "PrometheusPort": "The target port to which Prometheus sends metrics. If not specified, the default port 9668 is used." + } + }, "AWS::ApplicationInsights::Application.JMXPrometheusExporter": { "attributes": {}, "description": "The `AWS::ApplicationInsights::Application JMXPrometheusExporter` property type defines the JMXPrometheus Exporter configuration. For more information, see the [component configuration](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/component-config-sections.html#component-configuration-prometheus) in the CloudWatch Application Insights documentation.", @@ -4688,7 +4800,7 @@ "Encoding": "The type of encoding of the logs to be monitored. The specified encoding should be included in the list of CloudWatch agent supported encodings. If not provided, CloudWatch Application Insights uses the default encoding type for the log type:\n\n- `APPLICATION/DEFAULT` : utf-8 encoding\n- `SQL_SERVER` : utf-16 encoding\n- `IIS` : ascii encoding", "LogGroupName": "The CloudWatch log group name to be associated with the monitored log.", "LogPath": "The path of the logs to be monitored. The log path must be an absolute Windows or Linux system file path. For more information, see [CloudWatch Agent Configuration File: Logs Section](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html#CloudWatch-Agent-Configuration-File-Logssection) .", - "LogType": "The log type decides the log patterns against which Application Insights analyzes the log. The log type is selected from the following: `SQL_SERVER` , `IIS` , `APPLICATION` , and `DEFAULT` .", + "LogType": "The log type decides the log patterns against which Application Insights analyzes the log. The log type is selected from the following: `SQL_SERVER` , `MYSQL` , `MYSQL_SLOW_QUERY` , `POSTGRESQL` , `ORACLE_ALERT` , `ORACLE_LISTENER` , `IIS` , `APPLICATION` , `WINDOWS_EVENTS` , `WINDOWS_EVENTS_ACTIVE_DIRECTORY` , `WINDOWS_EVENTS_DNS` , `WINDOWS_EVENTS_IIS` , `WINDOWS_EVENTS_SHAREPOINT` , `SQL_SERVER_ALWAYSON_AVAILABILITY_GROUP` , `SQL_SERVER_FAILOVER_CLUSTER_INSTANCE` , `STEP_FUNCTION` , `API_GATEWAY_ACCESS` , `API_GATEWAY_EXECUTION` , `SAP_HANA_LOGS` , `SAP_HANA_TRACE` , `SAP_HANA_HIGH_AVAILABILITY` , and `DEFAULT` .", "PatternSet": "The log pattern set." } }, @@ -4754,12 +4866,12 @@ "NamedQueryId": "The unique ID of the query.", "Ref": "`Ref` returns the resource name." }, - "description": "The `AWS::Athena::NamedQuery` resource specifies an Amazon Athena saved query, where `QueryString` is the list of SQL query statements that comprise the query.", + "description": "The `AWS::Athena::NamedQuery` resource specifies an Amazon Athena saved query, where `QueryString` contains the SQL query statements that make up the query.", "properties": { "Database": "The database to which the query belongs.", "Description": "The query description.", "Name": "The query name.", - "QueryString": "The SQL query statements that comprise the query.", + "QueryString": "The SQL statements that make up the query.", "WorkGroup": "The name of the workgroup that contains the named query." } }, @@ -4943,7 +5055,7 @@ "Cooldown": "The amount of time, in seconds, after a scaling activity completes before another scaling activity can start. The default value is `300` . This setting applies when using simple scaling policies, but not when using other scaling policies or scheduled scaling. For more information, see [Scaling cooldowns for Amazon EC2 Auto Scaling](https://docs.aws.amazon.com/autoscaling/ec2/userguide/Cooldown.html) in the *Amazon EC2 Auto Scaling User Guide* .", "DesiredCapacity": "The desired capacity is the initial capacity of the Auto Scaling group at the time of its creation and the capacity it attempts to maintain. It can scale beyond this capacity if you configure automatic scaling.\n\nThe number must be greater than or equal to the minimum size of the group and less than or equal to the maximum size of the group. If you do not specify a desired capacity when creating the stack, the default is the minimum size of the group.\n\nCloudFormation marks the Auto Scaling group as successful (by setting its status to CREATE_COMPLETE) when the desired capacity is reached. However, if a maximum Spot price is set in the launch template or launch configuration that you specified, then desired capacity is not used as a criteria for success. Whether your request is fulfilled depends on Spot Instance capacity and your maximum price.", "DesiredCapacityType": "The unit of measurement for the value specified for desired capacity. Amazon EC2 Auto Scaling supports `DesiredCapacityType` for attribute-based instance type selection only. For more information, see [Creating an Auto Scaling group using attribute-based instance type selection](https://docs.aws.amazon.com/autoscaling/ec2/userguide/create-asg-instance-type-requirements.html) in the *Amazon EC2 Auto Scaling User Guide* .\n\nBy default, Amazon EC2 Auto Scaling specifies `units` , which translates into number of instances.\n\nValid values: `units` | `vcpu` | `memory-mib`", - "HealthCheckGracePeriod": "The amount of time, in seconds, that Amazon EC2 Auto Scaling waits before checking the health status of an EC2 instance that has come into service. The default value is `0` . For more information, see [Health checks for Auto Scaling instances](https://docs.aws.amazon.com/autoscaling/ec2/userguide/healthcheck.html) in the *Amazon EC2 Auto Scaling User Guide* .\n\nIf you are adding an `ELB` health check, you must specify this property.", + "HealthCheckGracePeriod": "The amount of time, in seconds, that Amazon EC2 Auto Scaling waits before checking the health status of an EC2 instance that has come into service and marking it unhealthy due to a failed health check. The default value is `0` . For more information, see [Health checks for Auto Scaling instances](https://docs.aws.amazon.com/autoscaling/ec2/userguide/healthcheck.html) in the *Amazon EC2 Auto Scaling User Guide* .\n\nIf you are adding an `ELB` health check, you must specify this property.", "HealthCheckType": "The service to use for the health checks. The valid values are `EC2` (default) and `ELB` . If you configure an Auto Scaling group to use load balancer (ELB) health checks, it considers the instance unhealthy if it fails either the EC2 status checks or the load balancer health checks. For more information, see [Health checks for Auto Scaling instances](https://docs.aws.amazon.com/autoscaling/ec2/userguide/healthcheck.html) in the *Amazon EC2 Auto Scaling User Guide* .", "InstanceId": "The ID of the instance used to base the launch configuration on. If specified, Amazon EC2 Auto Scaling uses the configuration values from the specified instance to create a new launch configuration. For more information, see [Creating an Auto Scaling group using an EC2 instance](https://docs.aws.amazon.com/autoscaling/ec2/userguide/create-asg-from-instance.html) in the *Amazon EC2 Auto Scaling User Guide* .\n\nTo get the instance ID, use the EC2 [DescribeInstances](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html) API operation.\n\nIf you specify `LaunchTemplate` , `MixedInstancesPolicy` , or `LaunchConfigurationName` , don't specify `InstanceId` .", "LaunchConfigurationName": "The name of the [launch configuration](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html) to use to launch instances.\n\nIf you specify `LaunchTemplate` , `MixedInstancesPolicy` , or `InstanceId` , don't specify `LaunchConfigurationName` .", @@ -5009,9 +5121,9 @@ "MemoryGiBPerVCpu": "The minimum and maximum amount of memory per vCPU for an instance type, in GiB.\n\nDefault: No minimum or maximum", "MemoryMiB": "The minimum and maximum instance memory size for an instance type, in MiB.", "NetworkInterfaceCount": "The minimum and maximum number of network interfaces for an instance type.\n\nDefault: No minimum or maximum", - "OnDemandMaxPricePercentageOverLowestPrice": "The price protection threshold for On-Demand Instances. This is the maximum you\u2019ll pay for an On-Demand Instance, expressed as a percentage higher than the cheapest M, C, or R instance type with your specified attributes. When Amazon EC2 Auto Scaling selects instance types with your attributes, we will exclude instance types whose price is higher than your threshold. The parameter accepts an integer, which Amazon EC2 Auto Scaling interprets as a percentage. To turn off price protection, specify a high value, such as `999999` .\n\nDefault: `20`", + "OnDemandMaxPricePercentageOverLowestPrice": "The price protection threshold for On-Demand Instances. This is the maximum you\u2019ll pay for an On-Demand Instance, expressed as a percentage higher than the cheapest M, C, or R instance type with your specified attributes. When Amazon EC2 Auto Scaling selects instance types with your attributes, we will exclude instance types whose price is higher than your threshold. The parameter accepts an integer, which Amazon EC2 Auto Scaling interprets as a percentage. To turn off price protection, specify a high value, such as `999999` .\n\nIf you set `DesiredCapacityType` to `vcpu` or `memory-mib` , the price protection threshold is applied based on the per vCPU or per memory price instead of the per instance price.\n\nDefault: `20`", "RequireHibernateSupport": "Indicates whether instance types must provide On-Demand Instance hibernation support.\n\nDefault: `false`", - "SpotMaxPricePercentageOverLowestPrice": "The price protection threshold for Spot Instances. This is the maximum you\u2019ll pay for a Spot Instance, expressed as a percentage higher than the cheapest M, C, or R instance type with your specified attributes. When Amazon EC2 Auto Scaling selects instance types with your attributes, we will exclude instance types whose price is higher than your threshold. The parameter accepts an integer, which Amazon EC2 Auto Scaling interprets as a percentage. To turn off price protection, specify a high value, such as `999999` .\n\nDefault: `100`", + "SpotMaxPricePercentageOverLowestPrice": "The price protection threshold for Spot Instances. This is the maximum you\u2019ll pay for a Spot Instance, expressed as a percentage higher than the cheapest M, C, or R instance type with your specified attributes. When Amazon EC2 Auto Scaling selects instance types with your attributes, we will exclude instance types whose price is higher than your threshold. The parameter accepts an integer, which Amazon EC2 Auto Scaling interprets as a percentage. To turn off price protection, specify a high value, such as `999999` .\n\nIf you set `DesiredCapacityType` to `vcpu` or `memory-mib` , the price protection threshold is applied based on the per vCPU or per memory price instead of the per instance price.\n\nDefault: `100`", "TotalLocalStorageGB": "The minimum and maximum total local storage size for an instance type, in GB.\n\nDefault: No minimum or maximum", "VCpuCount": "The minimum and maximum number of vCPUs for an instance type." } @@ -5204,7 +5316,7 @@ "attributes": { "Ref": "When the logical ID of this resource is provided to the `Ref` intrinsic function, `Ref` returns the resource name. For example: `mylifecyclehook` .\n\nFor more information about using the `Ref` function, see [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) ." }, - "description": "The `AWS::AutoScaling::LifecycleHook` resource specifies lifecycle hooks for an Auto Scaling group. These hooks enable an Auto Scaling group to be aware of events in the Auto Scaling instance lifecycle, and then perform a custom action when the corresponding lifecycle event occurs. A lifecycle hook provides a specified amount of time (one hour by default) to complete the lifecycle action before the instance transitions to the next state.\n\nThere are two types of lifecycle hooks that can be implemented: launch lifecycle hooks and termination lifecycle hooks. Use a launch lifecycle hook to prepare instances for use or to delay instances from registering behind the load balancer before their configuration has been applied completely. Use a termination lifecycle hook to prepare running instances to be shut down.\n\nFor more information, see [Amazon EC2 Auto Scaling lifecycle hooks](https://docs.aws.amazon.com/autoscaling/ec2/userguide/lifecycle-hooks.html) in the *Amazon EC2 Auto Scaling User Guide* and [PutLifecycleHook](https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_PutLifecycleHook.html) in the *Amazon EC2 Auto Scaling API Reference* .", + "description": "The `AWS::AutoScaling::LifecycleHook` resource specifies lifecycle hooks for an Auto Scaling group. These hooks let you create solutions that are aware of events in the Auto Scaling instance lifecycle, and then perform a custom action on instances when the corresponding lifecycle event occurs. A lifecycle hook provides a specified amount of time (one hour by default) to wait for the action to complete before the instance transitions to the next state.\n\nUse lifecycle hooks to prepare new instances for use or to delay them from being registered behind a load balancer before their configuration has been applied completely. You can also use lifecycle hooks to prepare running instances to be terminated by, for example, downloading logs or other data.\n\nFor more information, see [Amazon EC2 Auto Scaling lifecycle hooks](https://docs.aws.amazon.com/autoscaling/ec2/userguide/lifecycle-hooks.html) in the *Amazon EC2 Auto Scaling User Guide* and [PutLifecycleHook](https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_PutLifecycleHook.html) in the *Amazon EC2 Auto Scaling API Reference* .", "properties": { "AutoScalingGroupName": "The name of the Auto Scaling group for the lifecycle hook.", "DefaultResult": "The action the Auto Scaling group takes when the lifecycle hook timeout elapses or if an unexpected failure occurs. The valid values are `CONTINUE` and `ABANDON` (default).\n\nFor more information, see [Adding lifecycle hooks](https://docs.aws.amazon.com/autoscaling/ec2/userguide/adding-lifecycle-hooks.html) in the *Amazon EC2 Auto Scaling User Guide* .", @@ -5347,11 +5459,19 @@ "description": "The `AWS::AutoScaling::WarmPool` resource creates a pool of pre-initialized EC2 instances that sits alongside the Auto Scaling group. Whenever your application needs to scale out, the Auto Scaling group can draw on the warm pool to meet its new desired capacity.\n\nWhen you create a warm pool, you can define a minimum size. When your Auto Scaling group scales out and the size of the warm pool shrinks, Amazon EC2 Auto Scaling launches new instances into the warm pool to maintain its minimum size.\n\nFor more information, see [Warm pools for Amazon EC2 Auto Scaling](https://docs.aws.amazon.com/autoscaling/ec2/userguide/ec2-auto-scaling-warm-pools.html) in the *Amazon EC2 Auto Scaling User Guide* .\n\n> CloudFormation supports the `UpdatePolicy` attribute for Auto Scaling groups. During an update, if `UpdatePolicy` is set to `AutoScalingRollingUpdate` , CloudFormation replaces `InService` instances only. Instances in the warm pool are not replaced. The difference in which instances are replaced can potentially result in different instance configurations after the stack update completes. If `UpdatePolicy` is set to `AutoScalingReplacingUpdate` , you do not encounter this issue because CloudFormation replaces both the Auto Scaling group and the warm pool.", "properties": { "AutoScalingGroupName": "The name of the Auto Scaling group.", + "InstanceReusePolicy": "", "MaxGroupPreparedCapacity": "Specifies the maximum number of instances that are allowed to be in the warm pool or in any state except `Terminated` for the Auto Scaling group. This is an optional property. Specify it only if you do not want the warm pool size to be determined by the difference between the group's maximum capacity and its desired capacity.\n\n> If a value for `MaxGroupPreparedCapacity` is not specified, Amazon EC2 Auto Scaling launches and maintains the difference between the group's maximum capacity and its desired capacity. If you specify a value for `MaxGroupPreparedCapacity` , Amazon EC2 Auto Scaling uses the difference between the `MaxGroupPreparedCapacity` and the desired capacity instead.\n> \n> The size of the warm pool is dynamic. Only when `MaxGroupPreparedCapacity` and `MinSize` are set to the same value does the warm pool have an absolute size. \n\nIf the desired capacity of the Auto Scaling group is higher than the `MaxGroupPreparedCapacity` , the capacity of the warm pool is 0, unless you specify a value for `MinSize` . To remove a value that you previously set, include the property but specify -1 for the value.", "MinSize": "Specifies the minimum number of instances to maintain in the warm pool. This helps you to ensure that there is always a certain number of warmed instances available to handle traffic spikes. Defaults to 0 if not specified.", "PoolState": "Sets the instance state to transition to after the lifecycle actions are complete. Default is `Stopped` ." } }, + "AWS::AutoScaling::WarmPool.InstanceReusePolicy": { + "attributes": {}, + "description": "", + "properties": { + "ReuseOnScaleIn": "" + } + }, "AWS::AutoScalingPlans::ScalingPlan": { "attributes": { "Ref": "When you pass the logical ID of an `AWS::AutoScalingPlans::ScalingPlan` resource to the intrinsic `Ref` function, the function returns the Amazon Resource Name (ARN) of the scaling plan. The format of the ARN is as follows:\n\n`arn:aws:autoscaling: *region* : *123456789012:* scalingPlan:scalingPlanName/ *plan-name* :scalingPlanVersion/ *plan-version*`\n\nFor more information about using the `Ref` function, see [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) ." @@ -5464,7 +5584,7 @@ "Ref": "`Ref` returns `BackupPlanId` .", "VersionId": "Unique, randomly generated, Unicode, UTF-8 encoded strings that are at most 1,024 bytes long. Version Ids cannot be edited." }, - "description": "Contains an optional backup plan display name and an array of `BackupRule` objects, each of which specifies a backup rule. Each rule in a backup plan is a separate scheduled task and can back up a different selection of AWS resources.", + "description": "Contains an optional backup plan display name and an array of `BackupRule` objects, each of which specifies a backup rule. Each rule in a backup plan is a separate scheduled task and can back up a different selection of AWS resources.\n\nFor a sample AWS CloudFormation template, see the [AWS Backup Developer Guide](https://docs.aws.amazon.com/aws-backup/latest/devguide/assigning-resources.html#assigning-resources-cfn) .", "properties": { "BackupPlan": "Uniquely identifies the backup plan to be associated with the selection of resources.", "BackupPlanTags": "To help organize your resources, you can assign your own metadata to the resources that you create. Each tag is a key-value pair. The specified tags are assigned to all backups created with this plan." @@ -5563,7 +5683,7 @@ "AccessPolicy": "A resource-based policy that is used to manage access permissions on the target backup vault.", "BackupVaultName": "The name of a logical container where backups are stored. Backup vaults are identified by names that are unique to the account used to create them and the AWS Region where they are created. They consist of lowercase letters, numbers, and hyphens.", "BackupVaultTags": "Metadata that you can assign to help organize the resources that you create. Each tag is a key-value pair.", - "EncryptionKeyArn": "The server-side encryption key that is used to protect your backups; for example, `arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab` .", + "EncryptionKeyArn": "A server-side encryption key you can specify to encrypt your backups from services that support full AWS Backup management; for example, `arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab` . If you specify a key, you must specify its ARN, not its alias. If you do not specify a key, AWS Backup creates a KMS key for you by default.\n\nTo learn which AWS Backup services support full AWS Backup management and how AWS Backup handles encryption for backups from services that do not yet support full AWS Backup , see [Encryption for backups in AWS Backup](https://docs.aws.amazon.com/aws-backup/latest/devguide/encryption.html)", "LockConfiguration": "Configuration for [AWS Backup Vault Lock](https://docs.aws.amazon.com/aws-backup/latest/devguide/vault-lock.html) .", "Notifications": "The SNS event notifications for the specified backup vault." } @@ -5644,21 +5764,21 @@ }, "AWS::Budgets::Budget.BudgetData": { "attributes": {}, - "description": "Represents the output of the `CreateBudget` operation. The content consists of the detailed metadata and data file information, and the current status of the `budget` object.\n\nThis is the ARN pattern for a budget:\n\n`arn:aws:budgets::AccountId:budget/budgetName`", + "description": "Represents the output of the `CreateBudget` operation. The content consists of the detailed metadata and data file information, and the current status of the `budget` object.\n\nThis is the Amazon Resource Name (ARN) pattern for a budget:\n\n`arn:aws:budgets::AccountId:budget/budgetName`", "properties": { - "BudgetLimit": "The total amount of cost, usage, RI utilization, RI coverage, Savings Plans utilization, or Savings Plans coverage that you want to track with your budget.\n\n`BudgetLimit` is required for cost or usage budgets, but optional for RI or Savings Plans utilization or coverage budgets. RI and Savings Plans utilization or coverage budgets default to `100` , which is the only valid value for RI or Savings Plans utilization or coverage budgets. You can't use `BudgetLimit` with `PlannedBudgetLimits` for `CreateBudget` and `UpdateBudget` actions.", + "BudgetLimit": "The total amount of cost, usage, RI utilization, RI coverage, Savings Plans utilization, or Savings Plans coverage that you want to track with your budget.\n\n`BudgetLimit` is required for cost or usage budgets, but optional for RI or Savings Plans utilization or coverage budgets. RI and Savings Plans utilization or coverage budgets default to `100` . This is the only valid value for RI or Savings Plans utilization or coverage budgets. You can't use `BudgetLimit` with `PlannedBudgetLimits` for `CreateBudget` and `UpdateBudget` actions.", "BudgetName": "The name of a budget. The value must be unique within an account. `BudgetName` can't include `:` and `\\` characters. If you don't include value for `BudgetName` in the template, Billing and Cost Management assigns your budget a randomly generated name.", - "BudgetType": "Whether this budget tracks costs, usage, RI utilization, RI coverage, Savings Plans utilization, or Savings Plans coverage.", + "BudgetType": "Specifies whether this budget tracks costs, usage, RI utilization, RI coverage, Savings Plans utilization, or Savings Plans coverage.", "CostFilters": "The cost filters, such as `Region` , `Service` , `member account` , `Tag` , or `Cost Category` , that are applied to a budget.\n\nAWS Budgets supports the following services as a `Service` filter for RI budgets:\n\n- Amazon EC2\n- Amazon Redshift\n- Amazon Relational Database Service\n- Amazon ElastiCache\n- Amazon OpenSearch Service", "CostTypes": "The types of costs that are included in this `COST` budget.\n\n`USAGE` , `RI_UTILIZATION` , `RI_COVERAGE` , `SAVINGS_PLANS_UTILIZATION` , and `SAVINGS_PLANS_COVERAGE` budgets do not have `CostTypes` .", - "PlannedBudgetLimits": "A map containing multiple `BudgetLimit` , including current or future limits.\n\n`PlannedBudgetLimits` is available for cost or usage budget and supports monthly and quarterly `TimeUnit` .\n\nFor monthly budgets, provide 12 months of `PlannedBudgetLimits` values. This must start from the current month and include the next 11 months. The `key` is the start of the month, `UTC` in epoch seconds.\n\nFor quarterly budgets, provide 4 quarters of `PlannedBudgetLimits` value entries in standard calendar quarter increments. This must start from the current quarter and include the next 3 quarters. The `key` is the start of the quarter, `UTC` in epoch seconds.\n\nIf the planned budget expires before 12 months for monthly or 4 quarters for quarterly, provide the `PlannedBudgetLimits` values only for the remaining periods.\n\nIf the budget begins at a date in the future, provide `PlannedBudgetLimits` values from the start date of the budget.\n\nAfter all of the `BudgetLimit` values in `PlannedBudgetLimits` are used, the budget continues to use the last limit as the `BudgetLimit` . At that point, the planned budget provides the same experience as a fixed budget.\n\n`DescribeBudget` and `DescribeBudgets` response along with `PlannedBudgetLimits` will also contain `BudgetLimit` representing the current month or quarter limit present in `PlannedBudgetLimits` . This only applies to budgets created with `PlannedBudgetLimits` . Budgets created without `PlannedBudgetLimits` will only contain `BudgetLimit` , and no `PlannedBudgetLimits` .", + "PlannedBudgetLimits": "A map containing multiple `BudgetLimit` , including current or future limits.\n\n`PlannedBudgetLimits` is available for cost or usage budget and supports both monthly and quarterly `TimeUnit` .\n\nFor monthly budgets, provide 12 months of `PlannedBudgetLimits` values. This must start from the current month and include the next 11 months. The `key` is the start of the month, `UTC` in epoch seconds.\n\nFor quarterly budgets, provide four quarters of `PlannedBudgetLimits` value entries in standard calendar quarter increments. This must start from the current quarter and include the next three quarters. The `key` is the start of the quarter, `UTC` in epoch seconds.\n\nIf the planned budget expires before 12 months for monthly or four quarters for quarterly, provide the `PlannedBudgetLimits` values only for the remaining periods.\n\nIf the budget begins at a date in the future, provide `PlannedBudgetLimits` values from the start date of the budget.\n\nAfter all of the `BudgetLimit` values in `PlannedBudgetLimits` are used, the budget continues to use the last limit as the `BudgetLimit` . At that point, the planned budget provides the same experience as a fixed budget.\n\n`DescribeBudget` and `DescribeBudgets` response along with `PlannedBudgetLimits` also contain `BudgetLimit` representing the current month or quarter limit present in `PlannedBudgetLimits` . This only applies to budgets that are created with `PlannedBudgetLimits` . Budgets that are created without `PlannedBudgetLimits` only contain `BudgetLimit` . They don't contain `PlannedBudgetLimits` .", "TimePeriod": "The period of time that is covered by a budget. The period has a start date and an end date. The start date must come before the end date. There are no restrictions on the end date.\n\nThe start date for a budget. If you created your budget and didn't specify a start date, the start date defaults to the start of the chosen time period (MONTHLY, QUARTERLY, or ANNUALLY). For example, if you create your budget on January 24, 2019, choose `MONTHLY` , and don't set a start date, the start date defaults to `01/01/19 00:00 UTC` . The defaults are the same for the AWS Billing and Cost Management console and the API.\n\nYou can change your start date with the `UpdateBudget` operation.\n\nAfter the end date, AWS deletes the budget and all associated notifications and subscribers.", "TimeUnit": "The length of time until a budget resets the actual and forecasted spend. `DAILY` is available only for `RI_UTILIZATION` and `RI_COVERAGE` budgets." } }, "AWS::Budgets::Budget.CostTypes": { "attributes": {}, - "description": "The types of cost that are included in a `COST` budget, such as tax and subscriptions.\n\n`USAGE` , `RI_UTILIZATION` , `RI_COVERAGE` , `SAVINGS_PLANS_UTILIZATION` , and `SAVINGS_PLANS_COVERAGE` budgets do not have `CostTypes` .", + "description": "The types of cost that are included in a `COST` budget, such as tax and subscriptions.\n\n`USAGE` , `RI_UTILIZATION` , `RI_COVERAGE` , `SAVINGS_PLANS_UTILIZATION` , and `SAVINGS_PLANS_COVERAGE` budgets don't have `CostTypes` .", "properties": { "IncludeCredit": "Specifies whether a budget includes credits.\n\nThe default value is `true` .", "IncludeDiscount": "Specifies whether a budget includes discounts.\n\nThe default value is `true` .", @@ -5675,11 +5795,11 @@ }, "AWS::Budgets::Budget.Notification": { "attributes": {}, - "description": "A notification that is associated with a budget. A budget can have up to ten notifications.\n\nEach notification must have at least one subscriber. A notification can have one SNS subscriber and up to 10 email subscribers, for a total of 11 subscribers.\n\nFor example, if you have a budget for 200 dollars and you want to be notified when you go over 160 dollars, create a notification with the following parameters:\n\n- A notificationType of `ACTUAL`\n- A `thresholdType` of `PERCENTAGE`\n- A `comparisonOperator` of `GREATER_THAN`\n- A notification `threshold` of `80`", + "description": "A notification that's associated with a budget. A budget can have up to ten notifications.\n\nEach notification must have at least one subscriber. A notification can have one SNS subscriber and up to 10 email subscribers, for a total of 11 subscribers.\n\nFor example, if you have a budget for 200 dollars and you want to be notified when you go over 160 dollars, create a notification with the following parameters:\n\n- A notificationType of `ACTUAL`\n- A `thresholdType` of `PERCENTAGE`\n- A `comparisonOperator` of `GREATER_THAN`\n- A notification `threshold` of `80`", "properties": { - "ComparisonOperator": "The comparison that is used for this notification.", - "NotificationType": "Whether the notification is for how much you have spent ( `ACTUAL` ) or for how much you're forecasted to spend ( `FORECASTED` ).", - "Threshold": "The threshold that is associated with a notification. Thresholds are always a percentage, and many customers find value being alerted between 50% - 200% of the budgeted amount. The maximum limit for your threshold is 1,000,000% above the budgeted amount.", + "ComparisonOperator": "The comparison that's used for this notification.", + "NotificationType": "Specifies whether the notification is for how much you have spent ( `ACTUAL` ) or for how much that you're forecasted to spend ( `FORECASTED` ).", + "Threshold": "The threshold that's associated with a notification. Thresholds are always a percentage, and many customers find value being alerted between 50% - 200% of the budgeted amount. The maximum limit for your threshold is 1,000,000% above the budgeted amount.", "ThresholdType": "The type of threshold for a notification. For `ABSOLUTE_VALUE` thresholds, AWS notifies you when you go over or are forecasted to go over your total cost threshold. For `PERCENTAGE` thresholds, AWS notifies you when you go over or are forecasted to go over a certain percentage of your forecasted spend. For example, if you have a budget for 200 dollars and you have a `PERCENTAGE` threshold of 80%, AWS notifies you when you go over 160 dollars." } }, @@ -5687,16 +5807,16 @@ "attributes": {}, "description": "A notification with subscribers. A notification can have one SNS subscriber and up to 10 email subscribers, for a total of 11 subscribers.", "properties": { - "Notification": "The notification that is associated with a budget.", + "Notification": "The notification that's associated with a budget.", "Subscribers": "A list of subscribers who are subscribed to this notification." } }, "AWS::Budgets::Budget.Spend": { "attributes": {}, - "description": "The amount of cost or usage that is measured for a budget.\n\nFor example, a `Spend` for `3 GB` of S3 usage would have the following parameters:\n\n- An `Amount` of `3`\n- A `unit` of `GB`", + "description": "The amount of cost or usage that's measured for a budget.\n\nFor example, a `Spend` for `3 GB` of S3 usage has the following parameters:\n\n- An `Amount` of `3`\n- A `unit` of `GB`", "properties": { - "Amount": "The cost or usage amount that is associated with a budget forecast, actual spend, or budget threshold.", - "Unit": "The unit of measurement that is used for the budget forecast, actual spend, or budget threshold, such as USD or GB." + "Amount": "The cost or usage amount that's associated with a budget forecast, actual spend, or budget threshold.", + "Unit": "The unit of measurement that's used for the budget forecast, actual spend, or budget threshold, such as USD or GBP." } }, "AWS::Budgets::Budget.Subscriber": { @@ -5711,7 +5831,7 @@ "attributes": {}, "description": "The period of time that is covered by a budget. The period has a start date and an end date. The start date must come before the end date. There are no restrictions on the end date.", "properties": { - "End": "The end date for a budget. If you didn't specify an end date, AWS set your end date to `06/15/87 00:00 UTC` . The defaults are the same for the AWS Billing and Cost Management console and the API.\n\nAfter the end date, AWS deletes the budget and all associated notifications and subscribers. You can change your end date with the `UpdateBudget` operation.", + "End": "The end date for a budget. If you didn't specify an end date, AWS set your end date to `06/15/87 00:00 UTC` . The defaults are the same for the AWS Billing and Cost Management console and the API.\n\nAfter the end date, AWS deletes the budget and all the associated notifications and subscribers. You can change your end date with the `UpdateBudget` operation.", "Start": "The start date for a budget. If you created your budget and didn't specify a start date, the start date defaults to the start of the chosen time period (MONTHLY, QUARTERLY, or ANNUALLY). For example, if you create your budget on January 24, 2019, choose `MONTHLY` , and don't set a start date, the start date defaults to `01/01/19 00:00 UTC` . The defaults are the same for the AWS Billing and Cost Management console and the API.\n\nYou can change your start date with the `UpdateBudget` operation.\n\nValid values depend on the value of `BudgetType` :\n\n- If `BudgetType` is `COST` or `USAGE` : Valid values are `MONTHLY` , `QUARTERLY` , and `ANNUALLY` .\n- If `BudgetType` is `RI_UTILIZATION` or `RI_COVERAGE` : Valid values are `DAILY` , `MONTHLY` , `QUARTERLY` , and `ANNUALLY` ." } }, @@ -5777,7 +5897,7 @@ }, "AWS::Budgets::BudgetsAction.Subscriber": { "attributes": {}, - "description": "The subscriber to a budget notification. The subscriber consists of a subscription type and either an Amazon SNS topic or an email address.\n\nFor example, an email subscriber would have the following parameters:\n\n- A `subscriptionType` of `EMAIL`\n- An `address` of `example@example.com`", + "description": "The subscriber to a budget notification. The subscriber consists of a subscription type and either an Amazon SNS topic or an email address.\n\nFor example, an email subscriber has the following parameters:\n\n- A `subscriptionType` of `EMAIL`\n- An `address` of `example@example.com`", "properties": { "Address": "The address that AWS sends budget notifications to, either an SNS topic or an email.\n\nWhen you create a subscriber, the value of `Address` can't contain line breaks.", "Type": "The type of notification that AWS sends to a subscriber." @@ -5832,7 +5952,7 @@ }, "description": "The `AWS::CE::CostCategory` resource creates groupings of cost that you can use across products in the AWS Billing and Cost Management console, such as Cost Explorer and AWS Budgets. For more information, see [Managing Your Costs with Cost Categories](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/manage-cost-categories.html) in the *AWS Billing and Cost Management User Guide* .", "properties": { - "DefaultValue": "", + "DefaultValue": "The default value for the cost category.", "Name": "The unique name of the Cost Category.", "RuleVersion": "The rule schema version in this particular Cost Category.", "Rules": "The array of CostCategoryRule in JSON array format.\n\n> Rules are processed in order. If there are multiple rules that match the line item, then the first rule to match is used to determine that Cost Category value.", @@ -5964,7 +6084,7 @@ "description": "`DomainValidationOption` is a property of the [AWS::CertificateManager::Certificate](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-certificatemanager-certificate.html) resource that specifies the AWS Certificate Manager ( ACM ) certificate domain to validate. Depending on the chosen validation method, ACM checks the domain's DNS record for a validation CNAME, or it attempts to send a validation email message to the domain owner.", "properties": { "DomainName": "A fully qualified domain name (FQDN) in the certificate request.", - "HostedZoneId": "The `HostedZoneId` option, which is available if you are using Route 53 as your domain registrar, causes ACM to add your CNAME to the domain record. Your list of `DomainValidationOptions` must contain one and only one of the domain-validation options, and the `HostedZoneId` can be used only when `DNS` is specified as your validation method.\n\nUse the Route 53 `ListHostedZones` API to discover IDs for available hosted zones.\n\n> The `ListHostedZones` API returns IDs in the format \"/hostedzone/Z111111QQQQQQQ\", but CloudFormation requires the IDs to be in the format \"Z111111QQQQQQQ\". \n\nWhen you change your `DomainValidationOptions` , a new resource is created.", + "HostedZoneId": "The `HostedZoneId` option, which is available if you are using Route 53 as your domain registrar, causes ACM to add your CNAME to the domain record. Your list of `DomainValidationOptions` must contain one and only one of the domain-validation options, and the `HostedZoneId` can be used only when `DNS` is specified as your validation method.\n\nUse the Route 53 `ListHostedZones` API to discover IDs for available hosted zones.\n\nThis option is required for publicly trusted certificates.\n\n> The `ListHostedZones` API returns IDs in the format \"/hostedzone/Z111111QQQQQQQ\", but CloudFormation requires the IDs to be in the format \"Z111111QQQQQQQ\". \n\nWhen you change your `DomainValidationOptions` , a new resource is created.", "ValidationDomain": "The domain name to which you want ACM to send validation emails. This domain name is the suffix of the email addresses that you want ACM to use. This must be the same as the `DomainName` value or a superdomain of the `DomainName` value. For example, if you request a certificate for `testing.example.com` , you can specify `example.com` as this value. In that case, ACM sends domain validation emails to the following five addresses:\n\n- admin@example.com\n- administrator@example.com\n- hostmaster@example.com\n- postmaster@example.com\n- webmaster@example.com" } }, @@ -5976,7 +6096,7 @@ "description": "The `AWS::Chatbot::SlackChannelConfiguration` resource configures a Slack channel to allow users to use AWS Chatbot with AWS CloudFormation templates.\n\nThis resource requires some setup to be done in the AWS Chatbot console. To provide the required Slack workspace ID, you must perform the initial authorization flow with Slack in the AWS Chatbot console, then copy and paste the workspace ID from the console. For more details, see steps 1-4 in [Setting Up AWS Chatbot with Slack](https://docs.aws.amazon.com/chatbot/latest/adminguide/setting-up.html#Setup_intro) in the *AWS Chatbot User Guide* .", "properties": { "ConfigurationName": "The name of the configuration.", - "GuardrailPolicies": "The list of IAM policy ARNs that are applied as channel guardrails. The AWS managed 'AdministratorAccess' policy is applied as a default if this is not set.", + "GuardrailPolicies": "The list of IAM policy ARNs that are applied as channel guardrails. The AWS managed 'AdministratorAccess' policy is applied as a default if this is not set. Currently, only 1 IAM policy is supported.", "IamRoleArn": "The ARN of the IAM role that defines the permissions for AWS Chatbot .\n\nThis is a user-definworked role that AWS Chatbot will assume. This is not the service-linked role. For more information, see [IAM Policies for AWS Chatbot](https://docs.aws.amazon.com/chatbot/latest/adminguide/chatbot-iam-policies.html) .", "LoggingLevel": "Specifies the logging level for this configuration. This property affects the log entries pushed to Amazon CloudWatch Logs.\n\nLogging levels include `ERROR` , `INFO` , or `NONE` .", "SlackChannelId": "The ID of the Slack channel.\n\nTo get the ID, open Slack, right click on the channel name in the left pane, then choose Copy Link. The channel ID is the 9-character string at the end of the URL. For example, `ABCBBLZZZ` .", @@ -6017,7 +6137,57 @@ "attributes": {}, "description": "In a CloudFormation template, you use the `AWS::CloudFormation::CustomResource` or `Custom:: *String*` resource type to specify custom resources.\n\nCustom resources provide a way for you to write custom provisioning logic in CloudFormation template and have CloudFormation run it during a stack operation, such as when you create, update or delete a stack. For more information, see [Custom resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html) .\n\n> If you use the [VPC endpoints](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-endpoints.html) feature, custom resources in the VPC must have access to CloudFormation -specific Amazon Simple Storage Service ( Amazon S3 ) buckets. Custom resources must send responses to a presigned Amazon S3 URL. If they can't send responses to Amazon S3 , CloudFormation won't receive a response and the stack operation fails. For more information, see [Setting up VPC endpoints for AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-vpce-bucketnames.html) .", "properties": { - "ServiceToken": "> Only one property is defined by AWS for a custom resource: `ServiceToken` . All other properties are defined by the service provider. \n\nThe service token that was given to the template developer by the service provider to access the service, such as an Amazon SNS topic ARN or Lambda function ARN. The service token must be from the same Region in which you are creating the stack.\n\nUpdates are not supported." + "ServiceToken": "> Only one property is defined by AWS for a custom resource: `ServiceToken` . All other properties are defined by the service provider. \n\nThe service token that was given to the template developer by the service provider to access the service, such as an Amazon SNS topic ARN or Lambda function ARN. The service token must be from the same Region in which you are creating the stack.\n\nUpdates aren't supported." + } + }, + "AWS::CloudFormation::HookDefaultVersion": { + "attributes": { + "Arn": "The Amazon Resource Number (ARN) of the activated extension, in this account and Region.", + "Ref": "`Ref` returns the Amazon Resource Name (ARN) of the resource version. For example:\n\n`arn:aws:cloudformation:us-west-2:012345678901:type/hook/Sample-CloudFormation-Hook/00000001`" + }, + "description": "The `HookDefaultVersion` resource specifies the default version of the hook. The default version of the hook is used in CloudFormation operations for this AWS account and AWS Region .", + "properties": { + "TypeName": "The name of the hook.\n\nYou must specify either `TypeVersionArn` , or `TypeName` and `VersionId` .", + "TypeVersionArn": "The version ID of the type configuration.\n\nYou must specify either `TypeVersionArn` , or `TypeName` and `VersionId` .", + "VersionId": "The version ID of the type specified.\n\nYou must specify either `TypeVersionArn` , or `TypeName` and `VersionId` ." + } + }, + "AWS::CloudFormation::HookTypeConfig": { + "attributes": { + "ConfigurationArn": "The Amazon Resource Number (ARN) of the activated hook type configuration, in this account and Region.", + "Ref": "`Ref` returns the ARN of the resource version. For example:\n\n`arn:aws:cloudformation:us-west-2:123456789012:type-configuration/hook/My-Sample-Hook/default`" + }, + "description": "The `HookTypeConfig` resource specifies the configuration of a hook.", + "properties": { + "Configuration": "Specifies the activated hook type configuration, in this AWS account and AWS Region .\n\nYou must specify either `TypeName` and `Configuration` or `TypeARN` and `Configuration` .", + "ConfigurationAlias": "Specifies the activated hook type configuration, in this AWS account and AWS Region .\n\nDefaults to `default` alias. Hook types currently support default configuration alias.", + "TypeArn": "The Amazon Resource Number (ARN) for the hook to set `Configuration` for.\n\nYou must specify either `TypeName` and `Configuration` or `TypeARN` and `Configuration` .", + "TypeName": "The unique name for your hook. Specifies a three-part namespace for your hook, with a recommended pattern of `Organization::Service::Hook` .\n\nYou must specify either `TypeName` and `Configuration` or `TypeARN` and `Configuration` ." + } + }, + "AWS::CloudFormation::HookVersion": { + "attributes": { + "Arn": "The Amazon Resource Name (ARN) of the hook.", + "IsDefaultVersion": "Whether the specified hook version is set as the default version.", + "Ref": "`Ref` returns the ARN of the resource version. For example:\n\n`arn:aws:cloudformation:us-west-2:012345678901:type/hook/Sample-CloudFormation-Hook/00000001`", + "TypeArn": "The Amazon Resource Number (ARN) assigned to this version of the hook.", + "VersionId": "The ID of this version of the hook.", + "Visibility": "The scope at which the resource is visible and usable in CloudFormation operations.\n\nValid values include:\n\n- `PRIVATE` : The resource is only visible and usable within the account in which it's registered. CloudFormation marks any resources you register as `PRIVATE` .\n- `PUBLIC` : The resource is publicly visible and usable within any Amazon account." + }, + "description": "The `HookVersion` resource publishes new or first hook version to the AWS CloudFormation registry.", + "properties": { + "ExecutionRoleArn": "The Amazon Resource Name (ARN) of the task execution role that grants the hook permission.", + "LoggingConfig": "Contains logging configuration information for an extension.", + "SchemaHandlerPackage": "A URL to the Amazon S3 bucket containing the hook project package that contains the necessary files for the hook you want to register.\n\nFor information on generating a schema handler package for the resource you want to register, see [submit](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-cli-submit.html) in the *CloudFormation CLI User Guide for Extension Development* .\n\n> The user registering the resource must be able to access the package in the S3 bucket. That's, the user must have [GetObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html) permissions for the schema handler package. For more information, see [Actions, Resources, and Condition Keys for Amazon S3](https://docs.aws.amazon.com/IAM/latest/UserGuide/list_amazons3.html) in the *AWS Identity and Access Management User Guide* .", + "TypeName": "The unique name for your hook. Specifies a three-part namespace for your hook, with a recommended pattern of `Organization::Service::Hook` .\n\n> The following organization namespaces are reserved and can't be used in your hook type names:\n> \n> - `Alexa`\n> - `AMZN`\n> - `Amazon`\n> - `ASK`\n> - `AWS`\n> - `Custom`\n> - `Dev`" + } + }, + "AWS::CloudFormation::HookVersion.LoggingConfig": { + "attributes": {}, + "description": "The `LoggingConfig` property type specifies logging configuration information for an extension.", + "properties": { + "LogGroupName": "The Amazon CloudWatch Logs group to which CloudFormation sends error logging information when invoking the extension's handlers.", + "LogRoleArn": "The Amazon Resource Name (ARN) of the role that CloudFormation should assume when sending log entries to CloudWatch Logs." } }, "AWS::CloudFormation::Macro": { @@ -6028,8 +6198,8 @@ "properties": { "Description": "A description of the macro.", "FunctionName": "The Amazon Resource Name (ARN) of the underlying AWS Lambda function that you want AWS CloudFormation to invoke when the macro is run.", - "LogGroupName": "The Amazon CloudWatch log group to which AWS CloudFormation sends error logging information when invoking the macro's underlying AWS Lambda function.", - "LogRoleARN": "The ARN of the role AWS CloudFormation should assume when sending log entries to CloudWatch logs.", + "LogGroupName": "The CloudWatch Logs group to which AWS CloudFormation sends error logging information when invoking the macro's underlying AWS Lambda function.", + "LogRoleARN": "The ARN of the role AWS CloudFormation should assume when sending log entries to CloudWatch Logs .", "Name": "The name of the macro. The name of the macro must be unique across all macros in the account." } }, @@ -6037,7 +6207,7 @@ "attributes": { "Ref": "`Ref` returns the Amazon Resource Name (ARN) of the module version." }, - "description": "Specifies the default version of a module. The default version of the module will be used in CloudFormation operations for this account and region.\n\nTo register a module version, use the `[AWS::CloudFormation::ModuleVersion](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-moduleversion.html)` resource.\n\nFor more information using modules, see [Using modules to encapsulate and reuse resource configurations](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/modules.html) and [Registering extensions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/registry.html#registry-register) in the *CloudFormation User Guide* . For information on developing modules, see [Developing modules](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/modules.html) in the *CloudFormation CLI User Guide* .", + "description": "Specifies the default version of a module. The default version of the module will be used in CloudFormation operations for this account and Region.\n\nTo register a module version, use the `[AWS::CloudFormation::ModuleVersion](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-moduleversion.html)` resource.\n\nFor more information using modules, see [Using modules to encapsulate and reuse resource configurations](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/modules.html) and [Registering extensions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/registry.html#registry-register) in the *CloudFormation User Guide* . For information on developing modules, see [Developing modules](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/modules.html) in the *CloudFormation CLI User Guide* .", "properties": { "Arn": "The Amazon Resource Name (ARN) of the module version to set as the default version.\n\nConditional: You must specify either `Arn` , or `ModuleName` and `VersionId` .", "ModuleName": "The name of the module.\n\nConditional: You must specify either `Arn` , or `ModuleName` and `VersionId` .", @@ -6054,12 +6224,12 @@ "Schema": "The schema that defines the module.", "TimeCreated": "When the specified module version was registered.", "VersionId": "The ID of this version of the module.", - "Visibility": "The scope at which the module is visible and usable in CloudFormation operations.\n\nValid values include:\n\n- `PRIVATE` : The module is only visible and usable within the account in which it is registered.\n- `PUBLIC` : The module is publicly visible and usable within any Amazon account." + "Visibility": "The scope at which the module is visible and usable in CloudFormation operations.\n\nValid values include:\n\n- `PRIVATE` : The module is only visible and usable within the account in which it's registered.\n- `PUBLIC` : The module is publicly visible and usable within any Amazon account." }, - "description": "Registers the specified version of the module with the CloudFormation service. Registering a module makes it available for use in CloudFormation templates in your AWS account and region.\n\nTo specify a module version as the default version, use the `[AWS::CloudFormation::ModuleDefaultVersion](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-moduledefaultversion.html)` resource.\n\nFor more information using modules, see [Using modules to encapsulate and reuse resource configurations](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/modules.html) and [Registering extensions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/registry.html#registry-register) in the *CloudFormation User Guide* . For information on developing modules, see [Developing modules](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/modules.html) in the *CloudFormation CLI User Guide* .", + "description": "Registers the specified version of the module with the CloudFormation service. Registering a module makes it available for use in CloudFormation templates in your AWS account and Region.\n\nTo specify a module version as the default version, use the `[AWS::CloudFormation::ModuleDefaultVersion](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-moduledefaultversion.html)` resource.\n\nFor more information using modules, see [Using modules to encapsulate and reuse resource configurations](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/modules.html) and [Registering extensions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/registry.html#registry-register) in the *CloudFormation User Guide* . For information on developing modules, see [Developing modules](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/modules.html) in the *CloudFormation CLI User Guide* .", "properties": { "ModuleName": "The name of the module being registered.", - "ModulePackage": "A URL to the S3 bucket containing the package that contains the template fragment and schema files for the module version to register.\n\n> The user registering the module version must be able to access the module package in the S3 bucket. That is, the user needs to have [GetObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html) permissions for the package. For more information, see [Actions, Resources, and Condition Keys for Amazon S3](https://docs.aws.amazon.com/IAM/latest/UserGuide/list_amazons3.html) in the *AWS Identity and Access Management User Guide* ." + "ModulePackage": "A URL to the S3 bucket containing the package that contains the template fragment and schema files for the module version to register.\n\n> The user registering the module version must be able to access the module package in the S3 bucket. That's, the user needs to have [GetObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html) permissions for the package. For more information, see [Actions, Resources, and Condition Keys for Amazon S3](https://docs.aws.amazon.com/IAM/latest/UserGuide/list_amazons3.html) in the *AWS Identity and Access Management User Guide* ." } }, "AWS::CloudFormation::PublicTypeVersion": { @@ -6069,11 +6239,11 @@ "Ref": "`Ref` returns the Amazon Resource Number (ARN) assigned to the public extension upon publication. For example:\n\n`{ \"Ref\": \"arn:aws:cloudformation:us-east-1::type/resource/2a33349e7e606a8ad2e30e3c84521f93123456/My-Extension/2.1.3\" }`", "TypeVersionArn": "The Amazon Resource Number (ARN) assigned to this version of the extension." }, - "description": "Tests and publishes a registered extension as a public, third-party extension.\n\nCloudFormation first tests the extension to make sure it meets all necessary requirements for being published in the CloudFormation registry. If it does, CloudFormation then publishes it to the registry as a public third-party extension in this region. Public extensions are available for use by all CloudFormation users.\n\n- For resource types, testing includes passing all contracts tests defined for the type.\n- For modules, testing includes determining if the module's model meets all necessary requirements.\n\nFor more information, see [Testing your public extension prior to publishing](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/publish-extension.html#publish-extension-testing) in the *CloudFormation CLI User Guide* .\n\nIf you don't specify a version, CloudFormation uses the default version of the extension in your account and region for testing.\n\nTo perform testing, CloudFormation assumes the execution role specified when the type was registered.\n\nAn extension must have a test status of `PASSED` before it can be published. For more information, see [Publishing extensions to make them available for public use](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-publish.html) in the *CloudFormation CLI User Guide* .", + "description": "Tests and publishes a registered extension as a public, third-party extension.\n\nCloudFormation first tests the extension to make sure it meets all necessary requirements for being published in the CloudFormation registry. If it does, CloudFormation then publishes it to the registry as a public third-party extension in this Region. Public extensions are available for use by all CloudFormation users.\n\n- For resource types, testing includes passing all contracts tests defined for the type.\n- For modules, testing includes determining if the module's model meets all necessary requirements.\n\nFor more information, see [Testing your public extension prior to publishing](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/publish-extension.html#publish-extension-testing) in the *CloudFormation CLI User Guide* .\n\nIf you don't specify a version, CloudFormation uses the default version of the extension in your account and Region for testing.\n\nTo perform testing, CloudFormation assumes the execution role specified when the type was registered.\n\nAn extension must have a test status of `PASSED` before it can be published. For more information, see [Publishing extensions to make them available for public use](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-publish.html) in the *CloudFormation CLI User Guide* .", "properties": { "Arn": "The Amazon Resource Number (ARN) of the extension.\n\nConditional: You must specify `Arn` , or `TypeName` and `Type` .", "LogDeliveryBucket": "The S3 bucket to which CloudFormation delivers the contract test execution logs.\n\nCloudFormation delivers the logs by the time contract testing has completed and the extension has been assigned a test type status of `PASSED` or `FAILED` .\n\nThe user initiating the stack operation must be able to access items in the specified S3 bucket. Specifically, the user needs the following permissions:\n\n- GetObject\n- PutObject\n\nFor more information, see [Actions, Resources, and Condition Keys for Amazon S3](https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazons3.html) in the *AWS Identity and Access Management User Guide* .", - "PublicVersionNumber": "The version number to assign to this version of the extension.\n\nUse the following format, and adhere to semantic versioning when assigning a version number to your extension:\n\n`MAJOR.MINOR.PATCH`\n\nFor more information, see [Semantic Versioning 2.0.0](https://docs.aws.amazon.com/https://semver.org/) .\n\nIf you do not specify a version number, CloudFormation increments the version number by one minor version release.\n\nYou cannot specify a version number the first time you publish a type. AWS CloudFormation automatically sets the first version number to be `1.0.0` .", + "PublicVersionNumber": "The version number to assign to this version of the extension.\n\nUse the following format, and adhere to semantic versioning when assigning a version number to your extension:\n\n`MAJOR.MINOR.PATCH`\n\nFor more information, see [Semantic Versioning 2.0.0](https://docs.aws.amazon.com/https://semver.org/) .\n\nIf you don't specify a version number, CloudFormation increments the version number by one minor version release.\n\nYou cannot specify a version number the first time you publish a type. AWS CloudFormation automatically sets the first version number to be `1.0.0` .", "Type": "The type of the extension to test.\n\nConditional: You must specify `Arn` , or `TypeName` and `Type` .", "TypeName": "The name of the extension to test.\n\nConditional: You must specify `Arn` , or `TypeName` and `Type` ." } @@ -6108,13 +6278,13 @@ "attributes": { "Arn": "The Amazon Resource Name (ARN) of the resource version.", "IsDefaultVersion": "Whether the resource version is set as the default version.", - "ProvisioningType": "The provisioning behavior of the resource type. CloudFormation determines the provisioning type during registration, based on the types of handlers in the schema handler package submitted.\n\nValid values include:\n\n- `FULLY_MUTABLE` : The resource type includes an update handler to process updates to the type during stack update operations.\n- `IMMUTABLE` : The resource type does not include an update handler, so the type cannot be updated and must instead be replaced during stack update operations.\n- `NON_PROVISIONABLE` : The resource type does not include all of the following handlers, and therefore cannot actually be provisioned.\n\n- create\n- read\n- delete", + "ProvisioningType": "The provisioning behavior of the resource type. CloudFormation determines the provisioning type during registration, based on the types of handlers in the schema handler package submitted.\n\nValid values include:\n\n- `FULLY_MUTABLE` : The resource type includes an update handler to process updates to the type during stack update operations.\n- `IMMUTABLE` : The resource type doesn't include an update handler, so the type can't be updated and must instead be replaced during stack update operations.\n- `NON_PROVISIONABLE` : The resource type doesn't include all the following handlers, and therefore can't actually be provisioned.\n\n- create\n- read\n- delete", "Ref": "`Ref` returns the ARN of the resource version. For example:\n\n`arn:aws:cloudformation:us-west-2:012345678901:type/resource/Sample-CloudFormation-Resource/00000001`", "TypeArn": "The Amazon Resource Name (ARN) of the resource.", "VersionId": "The ID of a specific version of the resource. The version ID is the value at the end of the Amazon Resource Name (ARN) assigned to the resource version when it is registered.", "Visibility": "The scope at which the resource is visible and usable in CloudFormation operations.\n\nValid values include:\n\n- `PRIVATE` : The resource is only visible and usable within the account in which it's registered. CloudFormation marks any resources you register as `PRIVATE` .\n- `PUBLIC` : The resource is publicly visible and usable within any Amazon account." }, - "description": "Registers a resource version with the CloudFormation service. Registering a resource version makes it available for use in CloudFormation templates in your AWS account , and includes:\n\n- Validating the resource schema.\n- Determining which handlers, if any, have been specified for the resource.\n- Making the resource available for use in your account.\n\nFor more information on how to develop resources and ready them for registration, see [Creating Resource Providers](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-types.html) in the *CloudFormation CLI User Guide* .\n\nYou can have a maximum of 50 resource versions registered at a time. This maximum is per account and per region.", + "description": "Registers a resource version with the CloudFormation service. Registering a resource version makes it available for use in CloudFormation templates in your AWS account , and includes:\n\n- Validating the resource schema.\n- Determining which handlers, if any, have been specified for the resource.\n- Making the resource available for use in your account.\n\nFor more information on how to develop resources and ready them for registration, see [Creating Resource Providers](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-types.html) in the *CloudFormation CLI User Guide* .\n\nYou can have a maximum of 50 resource versions registered at a time. This maximum is per account and per Region.", "properties": { "ExecutionRoleArn": "The Amazon Resource Name (ARN) of the IAM role for CloudFormation to assume when invoking the resource. If your resource calls AWS APIs in any of its handlers, you must create an *[IAM execution role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html)* that includes the necessary permissions to call those AWS APIs, and provision that execution role in your account. When CloudFormation needs to invoke the resource type handler, CloudFormation assumes this execution role to create a temporary session token, which it then passes to the resource type handler, thereby supplying your resource type with the appropriate credentials.", "LoggingConfig": "Logging configuration information for a resource.", @@ -6134,13 +6304,13 @@ "attributes": { "Ref": "`Ref` returns the stack ID. For example:\n\n`arn:aws:cloudformation:us-east-2:123456789012:stack/mystack-mynestedstack-sggfrhxhum7w/f449b250-b969-11e0-a185-5081d0136786`" }, - "description": "The `AWS::CloudFormation::Stack` type nests a stack as a resource in a top-level template.\n\nYou can add output values from a nested stack within the containing template. You use the [GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html) function with the nested stack's logical name and the name of the output value in the nested stack in the format `Outputs. *NestedStackOutputName*` .\n\n> We strongly recommend that updates to nested stacks are run from the parent stack. \n\nWhen you apply template changes to update a top-level stack, CloudFormation updates the top-level stack and initiates an update to its nested stacks. CloudFormation updates the resources of modified nested stacks, but doesn't update the resources of unmodified nested stacks. For more information, see [CloudFormation stack updates](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks.html) .\n\n> You must acknowledge IAM capabilities for nested stacks that contain IAM resources. Also, verify that you have cancel update stack permissions, which is required if an update rolls back. For more information about IAM and CloudFormation , see [Controlling access with AWS Identity and Access Management](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-template.html) .", + "description": "The `AWS::CloudFormation::Stack` resource nests a stack as a resource in a top-level template.\n\nYou can add output values from a nested stack within the containing template. You use the [GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html) function with the nested stack's logical name and the name of the output value in the nested stack in the format `Outputs. *NestedStackOutputName*` .\n\n> We strongly recommend that updates to nested stacks are run from the parent stack. \n\nWhen you apply template changes to update a top-level stack, CloudFormation updates the top-level stack and initiates an update to its nested stacks. CloudFormation updates the resources of modified nested stacks, but doesn't update the resources of unmodified nested stacks. For more information, see [CloudFormation stack updates](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks.html) .\n\n> You must acknowledge IAM capabilities for nested stacks that contain IAM resources. Also, verify that you have cancel update stack permissions, which is required if an update rolls back. For more information about IAM and CloudFormation , see [Controlling access with AWS Identity and Access Management](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-template.html) .", "properties": { - "NotificationARNs": "The Simple Notification Service (SNS) topic ARNs to publish stack related events. You can find your SNS topic ARNs using the SNS console or your Command Line Interface (CLI).", + "NotificationARNs": "The Amazon Simple Notification Service (Amazon SNS) topic ARNs to publish stack related events. You can find your Amazon SNS topic ARNs using the Amazon SNS console or your Command Line Interface (CLI).", "Parameters": "The set value pairs that represent the parameters passed to CloudFormation when this nested stack is created. Each parameter has a name corresponding to a parameter defined in the embedded template and a value representing the value that you want to set for the parameter.\n\n> If you use the `Ref` function to pass a parameter value to a nested stack, comma-delimited list parameters must be of type `String` . In other words, you can't pass values that are of type `CommaDelimitedList` to nested stacks. \n\nConditional. Required if the nested stack requires input parameters.\n\nWhether an update causes interruptions depends on the resources that are being updated. An update never causes a nested stack to be replaced.", "Tags": "Key-value pairs to associate with this stack. AWS CloudFormation also propagates these tags to the resources created in the stack. A maximum number of 50 tags can be specified.", "TemplateURL": "Location of file containing the template body. The URL must point to a template (max size: 460,800 bytes) that's located in an Amazon S3 bucket. For more information, see [Template anatomy](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-anatomy.html) .\n\nWhether an update causes interruptions depends on the resources that are being updated. An update never causes a nested stack to be replaced.", - "TimeoutInMinutes": "The length of time, in minutes, that CloudFormation waits for the nested stack to reach the `CREATE_COMPLETE` state. The default is no timeout. When CloudFormation detects that the nested stack has reached the `CREATE_COMPLETE` state, it marks the nested stack resource as `CREATE_COMPLETE` in the parent stack and resumes creating the parent stack. If the timeout period expires before the nested stack reaches `CREATE_COMPLETE` , CloudFormation marks the nested stack as failed and rolls back both the nested stack and parent stack.\n\nUpdates are not supported." + "TimeoutInMinutes": "The length of time, in minutes, that CloudFormation waits for the nested stack to reach the `CREATE_COMPLETE` state. The default is no timeout. When CloudFormation detects that the nested stack has reached the `CREATE_COMPLETE` state, it marks the nested stack resource as `CREATE_COMPLETE` in the parent stack and resumes creating the parent stack. If the timeout period expires before the nested stack reaches `CREATE_COMPLETE` , CloudFormation marks the nested stack as failed and rolls back both the nested stack and parent stack.\n\nUpdates aren't supported." } }, "AWS::CloudFormation::StackSet": { @@ -6148,7 +6318,7 @@ "Ref": "When you pass the logical ID of this resource to the intrinsic `Ref` function, `Ref` returns the StackSetId.", "StackSetId": "The ID of the stack that you're creating." }, - "description": "The `AWS::CloudFormation::StackSet` enables you to provision stacks into AWS accounts and across Regions by using a single CloudFormation template. In the stack set, you specify the template to use, as well as any parameters and capabilities that the template requires.", + "description": "The `AWS::CloudFormation::StackSet` enables you to provision stacks into AWS accounts and across Regions by using a single CloudFormation template. In the stack set, you specify the template to use, in addition to any parameters and capabilities that the template requires.", "properties": { "AdministrationRoleARN": "The Amazon Resource Number (ARN) of the IAM role to use to create this stack set. Specify an IAM role only if you are using customized administrator roles to control which users or groups can manage specific stack sets within the same administrator account.\n\nUse customized administrator roles to control which users or groups can manage specific stack sets within the same administrator account. For more information, see [Prerequisites: Granting Permissions for Stack Set Operations](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-prereqs.html) in the *AWS CloudFormation User Guide* .\n\n*Minimum* : `20`\n\n*Maximum* : `2048`", "AutoDeployment": "[ `Service-managed` permissions] Describes whether StackSets automatically deploys to AWS Organizations accounts that are added to a target organization or organizational unit (OU).", @@ -6191,7 +6361,7 @@ "FailureTolerancePercentage": "The percentage of accounts, per Region, for which this stack operation can fail before AWS CloudFormation stops the operation in that Region. If the operation is stopped in a Region, AWS CloudFormation doesn't attempt the operation in any subsequent Regions.\n\nWhen calculating the number of accounts based on the specified percentage, AWS CloudFormation rounds *down* to the next whole number.\n\nConditional: You must specify either `FailureToleranceCount` or `FailureTolerancePercentage` , but not both.", "MaxConcurrentCount": "The maximum number of accounts in which to perform this operation at one time. This is dependent on the value of `FailureToleranceCount` . `MaxConcurrentCount` is at most one more than the `FailureToleranceCount` .\n\nNote that this setting lets you specify the *maximum* for operations. For large deployments, under certain circumstances the actual number of accounts acted upon concurrently may be lower due to service throttling.\n\nConditional: You must specify either `MaxConcurrentCount` or `MaxConcurrentPercentage` , but not both.", "MaxConcurrentPercentage": "The maximum percentage of accounts in which to perform this operation at one time.\n\nWhen calculating the number of accounts based on the specified percentage, AWS CloudFormation rounds down to the next whole number. This is true except in cases where rounding down would result is zero. In this case, CloudFormation sets the number as one instead.\n\nNote that this setting lets you specify the *maximum* for operations. For large deployments, under certain circumstances the actual number of accounts acted upon concurrently may be lower due to service throttling.\n\nConditional: You must specify either `MaxConcurrentCount` or `MaxConcurrentPercentage` , but not both.", - "RegionConcurrencyType": "The concurrency type of deploying StackSets operations in regions, could be in parallel or one region at a time.\n\n*Allowed values* : `SEQUENTIAL` | `PARALLEL`", + "RegionConcurrencyType": "The concurrency type of deploying StackSets operations in Regions, could be in parallel or one Region at a time.\n\n*Allowed values* : `SEQUENTIAL` | `PARALLEL`", "RegionOrder": "The order of the Regions where you want to perform the stack operation." } }, @@ -6199,7 +6369,7 @@ "attributes": {}, "description": "The Parameter data type.", "properties": { - "ParameterKey": "The key associated with the parameter. If you don't specify a key and value for a particular parameter, AWS CloudFormation uses the default value that is specified in your template.", + "ParameterKey": "The key associated with the parameter. If you don't specify a key and value for a particular parameter, AWS CloudFormation uses the default value that's specified in your template.", "ParameterValue": "The input value associated with the parameter." } }, @@ -6217,7 +6387,7 @@ "Arn": "The Amazon Resource Number (ARN) of the activated extension, in this account and Region.", "Ref": "`Ref` returns the Amazon Resource Number (ARN) of the activated extension, in this account and Region.\n\n`{ \"Ref\": \"arn:aws:cloudformation:us-east-1:123456789013:type/resource/My-Example\" }`" }, - "description": "Activates a public third-party extension, making it available for use in stack templates. For more information, see [Using public extensions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/registry-public.html) in the *CloudFormation User Guide* .\n\nOnce you have activated a public third-party extension in your account and region, use [SetTypeConfiguration](https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_SetTypeConfiguration.html) to specify configuration properties for the extension. For more information, see [Configuring extensions at the account level](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/registry-register.html#registry-set-configuration) in the *CloudFormation User Guide* .", + "description": "Activates a public third-party extension, making it available for use in stack templates. For more information, see [Using public extensions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/registry-public.html) in the *AWS CloudFormation User Guide* .\n\nOnce you have activated a public third-party extension in your account and region, use [SetTypeConfiguration](https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_SetTypeConfiguration.html) to specify configuration properties for the extension. For more information, see [Configuring extensions at the account level](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/registry-register.html#registry-set-configuration) in the *CloudFormation User Guide* .", "properties": { "AutoUpdate": "Whether to automatically update the extension in this account and region when a new *minor* version is published by the extension publisher. Major versions released by the publisher must be manually updated.\n\nThe default is `true` .", "ExecutionRoleArn": "The name of the IAM execution role to use to activate the extension.", @@ -6235,8 +6405,8 @@ "attributes": {}, "description": "Contains logging configuration information for an extension.", "properties": { - "LogGroupName": "The Amazon CloudWatch log group to which CloudFormation sends error logging information when invoking the extension's handlers.", - "LogRoleArn": "The ARN of the role that CloudFormation should assume when sending log entries to CloudWatch logs." + "LogGroupName": "The Amazon CloudWatch Logs group to which CloudFormation sends error logging information when invoking the extension's handlers.", + "LogRoleArn": "The Amazon Resource Name (ARN) of the role that CloudFormation should assume when sending log entries to CloudWatch Logs." } }, "AWS::CloudFormation::WaitCondition": { @@ -6244,10 +6414,10 @@ "Data": "A JSON object that contains the `UniqueId` and `Data` values from the wait condition signal(s) for the specified wait condition. For more information about wait condition signals, see [Wait condition signal JSON format](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-waitcondition.html#using-cfn-waitcondition-signaljson) .\n\nExample return value for a wait condition with 2 signals:\n\n`{ \"Signal1\" : \"Step 1 complete.\" , \"Signal2\" : \"Step 2 complete.\" }`", "Ref": "`Ref` returns the resource name." }, - "description": "> For Amazon EC2 and Auto Scaling resources, we recommend that you use a `CreationPolicy` attribute instead of wait conditions. Add a CreationPolicy attribute to those resources, and use the cfn-signal helper script to signal when an instance creation process has completed successfully. \n\nYou can use a wait condition for situations like the following:\n\n- To coordinate stack resource creation with configuration actions that are external to the stack creation.\n- To track the status of a configuration process.\n\nFor these situations, we recommend that you associate a [CreationPolicy](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-creationpolicy.html) attribute with the wait condition so that you don't have to use a wait condition handle. For more information and an example, see [Creating wait conditions in a template](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-waitcondition.html) . If you use a CreationPolicy with a wait condition, do not specify any of the wait condition's properties.\n\n> If you use the [VPC endpoints](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-endpoints.html) feature, resources in the VPC that respond to wait conditions must have access to CloudFormation , specific Amazon Simple Storage Service ( Amazon S3 ) buckets. Resources must send wait condition responses to a presigned Amazon S3 URL. If they can't send responses to Amazon S3 , CloudFormation won't receive a response and the stack operation fails. For more information, see [Setting up VPC endpoints for AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-vpce-bucketnames.html) .", + "description": "> For Amazon EC2 and Auto Scaling resources, we recommend that you use a `CreationPolicy` attribute instead of wait conditions. Add a CreationPolicy attribute to those resources, and use the cfn-signal helper script to signal when an instance creation process has completed successfully. \n\nYou can use a wait condition for situations like the following:\n\n- To coordinate stack resource creation with configuration actions that are external to the stack creation.\n- To track the status of a configuration process.\n\nFor these situations, we recommend that you associate a [CreationPolicy](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-creationpolicy.html) attribute with the wait condition so that you don't have to use a wait condition handle. For more information and an example, see [Creating wait conditions in a template](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-waitcondition.html) . If you use a CreationPolicy with a wait condition, don't specify any of the wait condition's properties.\n\n> If you use the [VPC endpoints](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-endpoints.html) feature, resources in the VPC that respond to wait conditions must have access to CloudFormation , specific Amazon Simple Storage Service ( Amazon S3 ) buckets. Resources must send wait condition responses to a presigned Amazon S3 URL. If they can't send responses to Amazon S3 , CloudFormation won't receive a response and the stack operation fails. For more information, see [Setting up VPC endpoints for AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-vpce-bucketnames.html) .", "properties": { - "Count": "The number of success signals that CloudFormation must receive before it continues the stack creation process. When the wait condition receives the requisite number of success signals, CloudFormation resumes the creation of the stack. If the wait condition does not receive the specified number of success signals before the Timeout period expires, CloudFormation assumes that the wait condition has failed and rolls the stack back.\n\nUpdates are not supported.", - "Handle": "A reference to the wait condition handle used to signal this wait condition. Use the `Ref` intrinsic function to specify an [AWS::CloudFormation::WaitConditionHandle](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-waitconditionhandle.html) resource.\n\nAnytime you add a WaitCondition resource during a stack update, you must associate the wait condition with a new WaitConditionHandle resource. Don't reuse an old wait condition handle that has already been defined in the template. If you reuse a wait condition handle, the wait condition might evaluate old signals from a previous create or update stack command.\n\nUpdates are not supported.", + "Count": "The number of success signals that CloudFormation must receive before it continues the stack creation process. When the wait condition receives the requisite number of success signals, CloudFormation resumes the creation of the stack. If the wait condition doesn't receive the specified number of success signals before the Timeout period expires, CloudFormation assumes that the wait condition has failed and rolls the stack back.\n\nUpdates aren't supported.", + "Handle": "A reference to the wait condition handle used to signal this wait condition. Use the `Ref` intrinsic function to specify an [AWS::CloudFormation::WaitConditionHandle](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-waitconditionhandle.html) resource.\n\nAnytime you add a WaitCondition resource during a stack update, you must associate the wait condition with a new WaitConditionHandle resource. Don't reuse an old wait condition handle that has already been defined in the template. If you reuse a wait condition handle, the wait condition might evaluate old signals from a previous create or update stack command.\n\nUpdates aren't supported.", "Timeout": "The length of time (in seconds) to wait for the number of signals that the `Count` property specifies. `Timeout` is a minimum-bound property, meaning the timeout occurs no sooner than the time you specify, but can occur shortly thereafter. The maximum time that can be specified for this property is 12 hours (43200 seconds).\n\nUpdates aren't supported." } }, @@ -7094,7 +7264,7 @@ }, "AWS::CloudWatch::AnomalyDetector.Dimension": { "attributes": {}, - "description": "A dimension is a name/value pair that is part of the identity of a metric. You can assign up to 10 dimensions to a metric. Because dimensions are part of the unique identifier for a metric, whenever you add a unique name/value pair to one of your metrics, you are creating a new variation of that metric.", + "description": "A dimension is a name/value pair that is part of the identity of a metric. Because dimensions are part of the unique identifier for a metric, whenever you add a unique name/value pair to one of your metrics, you are creating a new variation of that metric. For example, many Amazon EC2 metrics publish `InstanceId` as a dimension name, and the actual instance ID as the value for that dimension.\n\nYou can assign up to 10 dimensions to a metric.", "properties": { "Name": "The name of the dimension.", "Value": "The value of the dimension. Dimension values must contain only ASCII characters and must include at least one non-whitespace character." @@ -7576,8 +7746,8 @@ "attributes": {}, "description": "Information about the Amazon S3 bucket that contains the code that will be committed to the new repository. Changes to this property are ignored after initial resource creation.", "properties": { - "Bucket": "The name of the Amazon S3 bucket that contains the ZIP file with the content that will be committed to the new repository. This can be specified using an ARN or the name of the bucket in the AWS account . Changes to this property are ignored after initial resource creation.", - "Key": "The key to use for accessing the Amazon S3 bucket. Changes to this property are ignored after initial resource creation.", + "Bucket": "The name of the Amazon S3 bucket that contains the ZIP file with the content that will be committed to the new repository. This can be specified using the name of the bucket in the AWS account . Changes to this property are ignored after initial resource creation.", + "Key": "The key to use for accessing the Amazon S3 bucket. Changes to this property are ignored after initial resource creation. For more information, see [Creating object key names](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html) and [Uploading objects](https://docs.aws.amazon.com/AmazonS3/latest/userguide/upload-objects.html) in the Amazon S3 User Guide.", "ObjectVersion": "The object version of the ZIP file, if versioning is enabled for the Amazon S3 bucket. Changes to this property are ignored after initial resource creation." } }, @@ -7970,7 +8140,7 @@ "properties": { "Category": "A category defines what kind of action can be taken in the stage, and constrains the provider type for the action. Valid categories are limited to one of the values below.\n\n- `Source`\n- `Build`\n- `Test`\n- `Deploy`\n- `Invoke`\n- `Approval`", "Owner": "The creator of the action being called. There are three valid values for the `Owner` field in the action category section within your pipeline structure: `AWS` , `ThirdParty` , and `Custom` . For more information, see [Valid Action Types and Providers in CodePipeline](https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#actions-valid-providers) .", - "Provider": "The provider of the service being called by the action. Valid providers are determined by the action category. For example, an action in the Deploy category type might have a provider of CodeDeploy, which would be specified as CodeDeploy. For more information, see [Valid Action Types and Providers in CodePipeline](https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#actions-valid-providers) .", + "Provider": "The provider of the service being called by the action. Valid providers are determined by the action category. For example, an action in the Deploy category type might have a provider of CodeDeploy, which would be specified as `CodeDeploy` . For more information, see [Valid Action Types and Providers in CodePipeline](https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#actions-valid-providers) .", "Version": "A string that describes the action version." } }, @@ -8247,14 +8417,14 @@ "EmailVerificationMessage": "A string representing the email verification message. EmailVerificationMessage is allowed only if [EmailSendingAccount](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_EmailConfigurationType.html#CognitoUserPools-Type-EmailConfigurationType-EmailSendingAccount) is DEVELOPER.", "EmailVerificationSubject": "A string representing the email verification subject. EmailVerificationSubject is allowed only if [EmailSendingAccount](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_EmailConfigurationType.html#CognitoUserPools-Type-EmailConfigurationType-EmailSendingAccount) is DEVELOPER.", "EnabledMfas": "Enables MFA on a specified user pool. To disable all MFAs after it has been enabled, set MfaConfiguration to \u201cOFF\u201d and remove EnabledMfas. MFAs can only be all disabled if MfaConfiguration is OFF. Once SMS_MFA is enabled, SMS_MFA can only be disabled by setting MfaConfiguration to \u201cOFF\u201d. Can be one of the following values:\n\n- `SMS_MFA` - Enables SMS MFA for the user pool. SMS_MFA can only be enabled if SMS configuration is provided.\n- `SOFTWARE_TOKEN_MFA` - Enables software token MFA for the user pool.\n\nAllowed values: `SMS_MFA` | `SOFTWARE_TOKEN_MFA`", - "LambdaConfig": "The Lambda trigger configuration information for the new user pool.\n\n> In a push model, event sources (such as Amazon S3 and custom applications) need permission to invoke a function. So you will need to make an extra call to add permission for these event sources to invoke your Lambda function.\n> \n> For more information on using the Lambda API to add permission, see [AddPermission](https://docs.aws.amazon.com/lambda/latest/dg/API_AddPermission.html) .\n> \n> For adding permission using the AWS CLI , see [add-permission](https://docs.aws.amazon.com/cli/latest/reference/lambda/add-permission.html) .", - "MfaConfiguration": "The multi-factor (MFA) configuration. Valid values include:\n\n- `OFF` MFA will not be used for any users.\n- `ON` MFA is required for all users to sign in.\n- `OPTIONAL` MFA will be required only for individual users who have an MFA factor enabled.", + "LambdaConfig": "The Lambda trigger configuration information for the new user pool.\n\n> In a push model, event sources (such as Amazon S3 and custom applications) need permission to invoke a function. So you must make an extra call to add permission for these event sources to invoke your Lambda function.\n> \n> For more information on using the Lambda API to add permission, see [AddPermission](https://docs.aws.amazon.com/lambda/latest/dg/API_AddPermission.html) .\n> \n> For adding permission using the AWS CLI , see [add-permission](https://docs.aws.amazon.com/cli/latest/reference/lambda/add-permission.html) .", + "MfaConfiguration": "The multi-factor (MFA) configuration. Valid values include:\n\n- `OFF` MFA won't be used for any users.\n- `ON` MFA is required for all users to sign in.\n- `OPTIONAL` MFA will be required only for individual users who have an MFA factor activated.", "Policies": "The policy associated with a user pool.", "Schema": "The schema attributes for the new user pool. These attributes can be standard or custom attributes.\n\n> During a user pool update, you can add new schema attributes but you cannot modify or delete an existing schema attribute.", "SmsAuthenticationMessage": "A string representing the SMS authentication message.", "SmsConfiguration": "The SMS configuration.", "SmsVerificationMessage": "A string representing the SMS verification message.", - "UserPoolAddOns": "Used to enable advanced security risk detection. Set the key `AdvancedSecurityMode` to the value \"AUDIT\".", + "UserPoolAddOns": "Enables advanced security risk detection. Set the key `AdvancedSecurityMode` to the value \"AUDIT\".", "UserPoolName": "A string used to name the user pool.", "UserPoolTags": "The tag keys and values to assign to the user pool. A tag is a label that you can use to categorize and manage user pools in different ways, such as by purpose, owner, environment, or other criteria.", "UsernameAttributes": "Determines whether email addresses or phone numbers can be specified as user names when a user signs up. Possible values: `phone_number` or `email` .\n\nThis user pool property cannot be updated.", @@ -8275,31 +8445,31 @@ "properties": { "AllowAdminCreateUserOnly": "Set to `True` if only the administrator is allowed to create user profiles. Set to `False` if users can sign themselves up via an app.", "InviteMessageTemplate": "The message template to be used for the welcome message to new users.\n\nSee also [Customizing User Invitation Messages](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-settings-message-customizations.html#cognito-user-pool-settings-user-invitation-message-customization) .", - "UnusedAccountValidityDays": "The user account expiration limit, in days, after which the account is no longer usable. To reset the account after that time limit, you must call `AdminCreateUser` again, specifying `\"RESEND\"` for the `MessageAction` parameter. The default value for this parameter is 7.\n\n> If you set a value for `TemporaryPasswordValidityDays` in `PasswordPolicy` , that value will be used and `UnusedAccountValidityDays` will be deprecated for that user pool." + "UnusedAccountValidityDays": "The user account expiration limit, in days, after which the account is no longer usable. To reset the account after that time limit, you must call `AdminCreateUser` again, specifying `\"RESEND\"` for the `MessageAction` parameter. The default value for this parameter is 7.\n\n> If you set a value for `TemporaryPasswordValidityDays` in `PasswordPolicy` , that value will be used, and `UnusedAccountValidityDays` will be no longer be an available parameter for that user pool." } }, "AWS::Cognito::UserPool.CustomEmailSender": { "attributes": {}, - "description": "", + "description": "A custom email sender AWS Lambda trigger.", "properties": { - "LambdaArn": "", - "LambdaVersion": "" + "LambdaArn": "The Amazon Resource Name (ARN) of the AWS Lambda function that Amazon Cognito triggers to send email notifications to users.", + "LambdaVersion": "The Lambda version represents the signature of the \"request\" attribute in the \"event\" information that Amazon Cognito passes to your custom email sender AWS Lambda function. The only supported value is `V1_0` ." } }, "AWS::Cognito::UserPool.CustomSMSSender": { "attributes": {}, - "description": "", + "description": "A custom SMS sender AWS Lambda trigger.", "properties": { - "LambdaArn": "", - "LambdaVersion": "" + "LambdaArn": "The Amazon Resource Name (ARN) of the AWS Lambda function that Amazon Cognito triggers to send SMS notifications to users.", + "LambdaVersion": "The Lambda version represents the signature of the \"request\" attribute in the \"event\" information Amazon Cognito passes to your custom SMS sender Lambda function. The only supported value is `V1_0` ." } }, "AWS::Cognito::UserPool.DeviceConfiguration": { "attributes": {}, - "description": "The configuration for the user pool's device tracking.", + "description": "The device tracking configuration for a user pool. A user pool with device tracking deactivated returns a null value.\n\n> When you provide values for any DeviceConfiguration field, you activate device tracking.", "properties": { - "ChallengeRequiredOnNewDevice": "Indicates whether a challenge is required on a new device. Only applicable to a new device.", - "DeviceOnlyRememberedOnUserPrompt": "If true, a device is only remembered on user prompt." + "ChallengeRequiredOnNewDevice": "When true, device authentication can replace SMS and time-based one-time password (TOTP) factors for multi-factor authentication (MFA).\n\n> Users that sign in with devices that have not been confirmed or remembered will still have to provide a second factor, whether or not ChallengeRequiredOnNewDevice is true, when your user pool requires MFA.", + "DeviceOnlyRememberedOnUserPrompt": "When true, users can opt in to remembering their device. Your app code must use callback functions to return the user's choice." } }, "AWS::Cognito::UserPool.EmailConfiguration": { @@ -8307,10 +8477,10 @@ "description": "The email configuration.", "properties": { "ConfigurationSet": "The set of configuration rules that can be applied to emails sent using Amazon SES. A configuration set is applied to an email by including a reference to the configuration set in the headers of the email. Once applied, all of the rules in that configuration set are applied to the email. Configuration sets can be used to apply the following types of rules to emails:\n\n- Event publishing \u2013 Amazon SES can track the number of send, delivery, open, click, bounce, and complaint events for each email sent. Use event publishing to send information about these events to other AWS services such as SNS and CloudWatch.\n- IP pool management \u2013 When leasing dedicated IP addresses with Amazon SES, you can create groups of IP addresses, called dedicated IP pools. You can then associate the dedicated IP pools with configuration sets.", - "EmailSendingAccount": "Specifies whether Amazon Cognito emails your users by using its built-in email functionality or your Amazon SES email configuration. Specify one of the following values:\n\n- **COGNITO_DEFAULT** - When Amazon Cognito emails your users, it uses its built-in email functionality. When you use the default option, Amazon Cognito allows only a limited number of emails each day for your user pool. For typical production environments, the default email limit is below the required delivery volume. To achieve a higher delivery volume, specify DEVELOPER to use your Amazon SES email configuration.\n\nTo look up the email delivery limit for the default option, see [Limits in Amazon Cognito](https://docs.aws.amazon.com/cognito/latest/developerguide/limits.html) in the *Amazon Cognito Developer Guide* .\n\nThe default FROM address is no-reply@verificationemail.com. To customize the FROM address, provide the ARN of an Amazon SES verified email address for the `SourceArn` parameter.\n\nIf EmailSendingAccount is COGNITO_DEFAULT, the following parameters aren't allowed:\n\n- EmailVerificationMessage\n- EmailVerificationSubject\n- InviteMessageTemplate.EmailMessage\n- InviteMessageTemplate.EmailSubject\n- VerificationMessageTemplate.EmailMessage\n- VerificationMessageTemplate.EmailMessageByLink\n- VerificationMessageTemplate.EmailSubject,\n- VerificationMessageTemplate.EmailSubjectByLink\n\n> DEVELOPER EmailSendingAccount is required.\n- **DEVELOPER** - When Amazon Cognito emails your users, it uses your Amazon SES configuration. Amazon Cognito calls Amazon SES on your behalf to send email from your verified email address. When you use this option, the email delivery limits are the same limits that apply to your Amazon SES verified email address in your AWS account .\n\nIf you use this option, you must provide the ARN of an Amazon SES verified email address for the `SourceArn` parameter.\n\nBefore Amazon Cognito can email your users, it requires additional permissions to call Amazon SES on your behalf. When you update your user pool with this option, Amazon Cognito creates a *service-linked role* , which is a type of IAM role, in your AWS account . This role contains the permissions that allow Amazon Cognito to access Amazon SES and send email messages with your address. For more information about the service-linked role that Amazon Cognito creates, see [Using Service-Linked Roles for Amazon Cognito](https://docs.aws.amazon.com/cognito/latest/developerguide/using-service-linked-roles.html) in the *Amazon Cognito Developer Guide* .", + "EmailSendingAccount": "Specifies whether Amazon Cognito emails your users by using its built-in email functionality or your Amazon Simple Email Service email configuration. Specify one of the following values:\n\n- **COGNITO_DEFAULT** - When Amazon Cognito emails your users, it uses its built-in email functionality. When you use the default option, Amazon Cognito allows only a limited number of emails each day for your user pool. For typical production environments, the default email limit is less than the required delivery volume. To achieve a higher delivery volume, specify DEVELOPER to use your Amazon SES email configuration.\n\nTo look up the email delivery limit for the default option, see [Limits in](https://docs.aws.amazon.com/cognito/latest/developerguide/limits.html) in the *Developer Guide* .\n\nThe default FROM address is `no-reply@verificationemail.com` . To customize the FROM address, provide the Amazon Resource Name (ARN) of an Amazon SES verified email address for the `SourceArn` parameter.\n\nIf EmailSendingAccount is COGNITO_DEFAULT, you can't use the following parameters:\n\n- EmailVerificationMessage\n- EmailVerificationSubject\n- InviteMessageTemplate.EmailMessage\n- InviteMessageTemplate.EmailSubject\n- VerificationMessageTemplate.EmailMessage\n- VerificationMessageTemplate.EmailMessageByLink\n- VerificationMessageTemplate.EmailSubject,\n- VerificationMessageTemplate.EmailSubjectByLink\n\n> DEVELOPER EmailSendingAccount is required.\n- **DEVELOPER** - When Amazon Cognito emails your users, it uses your Amazon SES configuration. Amazon Cognito calls Amazon SES on your behalf to send email from your verified email address. When you use this option, the email delivery limits are the same limits that apply to your Amazon SES verified email address in your AWS account .\n\nIf you use this option, you must provide the ARN of an Amazon SES verified email address for the `SourceArn` parameter.\n\nBefore Amazon Cognito can email your users, it requires additional permissions to call Amazon SES on your behalf. When you update your user pool with this option, Amazon Cognito creates a *service-linked role* , which is a type of role, in your AWS account . This role contains the permissions that allow to access Amazon SES and send email messages with your address. For more information about the service-linked role that Amazon Cognito creates, see [Using Service-Linked Roles for Amazon Cognito](https://docs.aws.amazon.com/cognito/latest/developerguide/using-service-linked-roles.html) in the *Amazon Cognito Developer Guide* .", "From": "Identifies either the sender's email address or the sender's name with their email address. For example, `testuser@example.com` or `Test User ` . This address appears before the body of the email.", - "ReplyToEmailAddress": "The destination to which the receiver of the email should reply to.", - "SourceArn": "The Amazon Resource Name (ARN) of a verified email address in Amazon SES. This email address is used in one of the following ways, depending on the value that you specify for the `EmailSendingAccount` parameter:\n\n- If you specify `COGNITO_DEFAULT` , Amazon Cognito uses this address as the custom FROM address when it emails your users by using its built-in email account.\n- If you specify `DEVELOPER` , Amazon Cognito emails your users with this address by calling Amazon SES on your behalf." + "ReplyToEmailAddress": "The destination to which the receiver of the email should reply.", + "SourceArn": "The ARN of a verified email address in Amazon SES. Amazon Cognito uses this email address in one of the following ways, depending on the value that you specify for the `EmailSendingAccount` parameter:\n\n- If you specify `COGNITO_DEFAULT` , Amazon Cognito uses this address as the custom FROM address when it emails your users using its built-in email account.\n- If you specify `DEVELOPER` , Amazon Cognito emails your users with this address by calling Amazon SES on your behalf." } }, "AWS::Cognito::UserPool.InviteMessageTemplate": { @@ -8327,11 +8497,11 @@ "description": "Specifies the configuration for AWS Lambda triggers.", "properties": { "CreateAuthChallenge": "Creates an authentication challenge.", - "CustomEmailSender": "", + "CustomEmailSender": "A custom email sender AWS Lambda trigger.", "CustomMessage": "A custom Message AWS Lambda trigger.", - "CustomSMSSender": "", + "CustomSMSSender": "A custom SMS sender AWS Lambda trigger.", "DefineAuthChallenge": "Defines the authentication challenge.", - "KMSKeyID": "", + "KMSKeyID": "The Amazon Resource Name of a AWS Key Management Service ( AWS KMS ) key. Amazon Cognito uses the key to encrypt codes and temporary passwords sent to `CustomEmailSender` and `CustomSMSSender` .", "PostAuthentication": "A post-authentication AWS Lambda trigger.", "PostConfirmation": "A post-confirmation AWS Lambda trigger.", "PreAuthentication": "A pre-authentication AWS Lambda trigger.", @@ -8343,7 +8513,7 @@ }, "AWS::Cognito::UserPool.NumberAttributeConstraints": { "attributes": {}, - "description": "The minimum and maximum value of an attribute that is of the number data type.", + "description": "The minimum and maximum values of an attribute that is of the number data type.", "properties": { "MaxValue": "The maximum value of an attribute that is of the number data type.", "MinValue": "The minimum value of an attribute that is of the number data type." @@ -8353,12 +8523,12 @@ "attributes": {}, "description": "The password policy type.", "properties": { - "MinimumLength": "The minimum length of the password policy that you have set. Cannot be less than 6.", + "MinimumLength": "The minimum length of the password in the policy that you have set. This value can't be less than 6.", "RequireLowercase": "In the password policy that you have set, refers to whether you have required users to use at least one lowercase letter in their password.", "RequireNumbers": "In the password policy that you have set, refers to whether you have required users to use at least one number in their password.", "RequireSymbols": "In the password policy that you have set, refers to whether you have required users to use at least one symbol in their password.", "RequireUppercase": "In the password policy that you have set, refers to whether you have required users to use at least one uppercase letter in their password.", - "TemporaryPasswordValidityDays": "In the password policy you have set, refers to the number of days a temporary password is valid. If the user does not sign-in during this time, their password will need to be reset by an administrator.\n\n> When you set `TemporaryPasswordValidityDays` for a user pool, you will no longer be able to set the deprecated `UnusedAccountValidityDays` value for that user pool." + "TemporaryPasswordValidityDays": "The number of days a temporary password is valid in the password policy. If the user doesn't sign in during this time, an administrator must reset their password.\n\n> When you set `TemporaryPasswordValidityDays` for a user pool, you can no longer set the deprecated `UnusedAccountValidityDays` value for that user pool." } }, "AWS::Cognito::UserPool.Policies": { @@ -8382,10 +8552,10 @@ "properties": { "AttributeDataType": "The attribute data type.", "DeveloperOnlyAttribute": "> We recommend that you use [WriteAttributes](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_UserPoolClientType.html#CognitoUserPools-Type-UserPoolClientType-WriteAttributes) in the user pool client to control how attributes can be mutated for new use cases instead of using `DeveloperOnlyAttribute` . \n\nSpecifies whether the attribute type is developer only. This attribute can only be modified by an administrator. Users will not be able to modify this attribute using their access token.", - "Mutable": "Specifies whether the value of the attribute can be changed.\n\nFor any user pool attribute that's mapped to an identity provider attribute, you must set this parameter to `true` . Amazon Cognito updates mapped attributes when users sign in to your application through an identity provider. If an attribute is immutable, Amazon Cognito throws an error when it attempts to update the attribute. For more information, see [Specifying Identity Provider Attribute Mappings for Your User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-specifying-attribute-mapping.html) .", + "Mutable": "Specifies whether the value of the attribute can be changed.\n\nFor any user pool attribute that is mapped to an identity provider attribute, you must set this parameter to `true` . Amazon Cognito updates mapped attributes when users sign in to your application through an identity provider. If an attribute is immutable, Amazon Cognito throws an error when it attempts to update the attribute. For more information, see [Specifying Identity Provider Attribute Mappings for Your User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-specifying-attribute-mapping.html) .", "Name": "A schema attribute of the name type.", "NumberAttributeConstraints": "Specifies the constraints for an attribute of the number type.", - "Required": "Specifies whether a user pool attribute is required. If the attribute is required and the user does not provide a value, registration or sign-in will fail.", + "Required": "Specifies whether a user pool attribute is required. If the attribute is required and the user doesn't provide a value, registration or sign-in will fail.", "StringAttributeConstraints": "Specifies the constraints for an attribute of the string type." } }, @@ -8394,7 +8564,8 @@ "description": "The SMS configuration type that includes the settings the Cognito User Pool needs to call for the Amazon SNS service to send an SMS message from your AWS account . The Cognito User Pool makes the request to the Amazon SNS Service by using an IAM role that you provide for your AWS account .", "properties": { "ExternalId": "The external ID is a value. We recommend you use `ExternalId` to add security to your IAM role, which is used to call Amazon SNS to send SMS messages for your user pool. If you provide an `ExternalId` , the Cognito User Pool uses it when attempting to assume your IAM role. You can also set your roles trust policy to require the `ExternalID` . If you use the Cognito Management Console to create a role for SMS MFA, Cognito creates a role with the required permissions and a trust policy that uses `ExternalId` .", - "SnsCallerArn": "The Amazon Resource Name (ARN) of the Amazon Simple Notification Service (SNS) caller. This is the ARN of the IAM role in your AWS account which Cognito will use to send SMS messages. SMS messages are subject to a [spending limit](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-email-phone-verification.html) ." + "SnsCallerArn": "The Amazon Resource Name (ARN) of the Amazon SNS caller. This is the ARN of the IAM role in your AWS account that Amazon Cognito will use to send SMS messages. SMS messages are subject to a [spending limit](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-email-phone-verification.html) .", + "SnsRegion": "" } }, "AWS::Cognito::UserPool.StringAttributeConstraints": { @@ -8416,7 +8587,7 @@ "attributes": {}, "description": "The `UsernameConfiguration` property type specifies case sensitivity on the username input for the selected sign-in option.", "properties": { - "CaseSensitive": "Specifies whether username case sensitivity will be applied for all users in the user pool through Cognito APIs.\n\nValid values include:\n\n- *`True`* : Enables case sensitivity for all username input. When this option is set to `True` , users must sign in using the exact capitalization of their given username. For example, \u201cUserName\u201d. This is the default value.\n- *`False`* : Enables case insensitivity for all username input. For example, when this option is set to `False` , users will be able to sign in using either \"username\" or \"Username\". This option also enables both `preferred_username` and `email` alias to be case insensitive, in addition to the `username` attribute." + "CaseSensitive": "Specifies whether username case sensitivity will be applied for all users in the user pool through Amazon Cognito APIs.\n\nValid values include:\n\n- *`True`* : Enables case sensitivity for all username input. When this option is set to `True` , users must sign in using the exact capitalization of their given username, such as \u201cUserName\u201d. This is the default value.\n- *`False`* : Enables case insensitivity for all username input. For example, when this option is set to `False` , users can sign in using either \"username\" or \"Username\". This option also enables both `preferred_username` and `email` alias to be case insensitive, in addition to the `username` attribute." } }, "AWS::Cognito::UserPool.VerificationMessageTemplate": { @@ -8439,34 +8610,34 @@ "properties": { "AccessTokenValidity": "The time limit, after which the access token is no longer valid and cannot be used.", "AllowedOAuthFlows": "The allowed OAuth flows.\n\nSet to `code` to initiate a code grant flow, which provides an authorization code as the response. This code can be exchanged for access tokens with the token endpoint.\n\nSet to `implicit` to specify that the client should get the access token (and, optionally, ID token, based on scopes) directly.\n\nSet to `client_credentials` to specify that the client should get the access token (and, optionally, ID token, based on scopes) from the token endpoint using a combination of client and client_secret.", - "AllowedOAuthFlowsUserPoolClient": "Set to true if the client is allowed to follow the OAuth protocol when interacting with Cognito user pools.", + "AllowedOAuthFlowsUserPoolClient": "Set to true if the client is allowed to follow the OAuth protocol when interacting with Amazon Cognito user pools.", "AllowedOAuthScopes": "The allowed OAuth scopes. Possible values provided by OAuth are: `phone` , `email` , `openid` , and `profile` . Possible values provided by AWS are: `aws.cognito.signin.user.admin` . Custom scopes created in Resource Servers are also supported.", - "AnalyticsConfiguration": "The Amazon Pinpoint analytics configuration for collecting metrics for this user pool.\n\n> In regions where Pinpoint is not available, Cognito User Pools only supports sending events to Amazon Pinpoint projects in us-east-1. In regions where Pinpoint is available, Cognito User Pools will support sending events to Amazon Pinpoint projects within that same region.", + "AnalyticsConfiguration": "The Amazon Pinpoint analytics configuration for collecting metrics for this user pool.\n\n> In AWS Regions where isn't available, User Pools only supports sending events to Amazon Pinpoint projects in AWS Region us-east-1. In Regions where is available, User Pools will support sending events to Amazon Pinpoint projects within that same Region.", "CallbackURLs": "A list of allowed redirect (callback) URLs for the identity providers.\n\nA redirect URI must:\n\n- Be an absolute URI.\n- Be registered with the authorization server.\n- Not include a fragment component.\n\nSee [OAuth 2.0 - Redirection Endpoint](https://docs.aws.amazon.com/https://tools.ietf.org/html/rfc6749#section-3.1.2) .\n\nAmazon Cognito requires HTTPS over HTTP except for http://localhost for testing purposes only.\n\nApp callback URLs such as myapp://example are also supported.", "ClientName": "The client name for the user pool client you would like to create.", "DefaultRedirectURI": "The default redirect URI. Must be in the `CallbackURLs` list.\n\nA redirect URI must:\n\n- Be an absolute URI.\n- Be registered with the authorization server.\n- Not include a fragment component.\n\nSee [OAuth 2.0 - Redirection Endpoint](https://docs.aws.amazon.com/https://tools.ietf.org/html/rfc6749#section-3.1.2) .\n\nAmazon Cognito requires HTTPS over HTTP except for http://localhost for testing purposes only.\n\nApp callback URLs such as myapp://example are also supported.", - "EnableTokenRevocation": "Enables or disables token revocation. For more information about revoking tokens, see [RevokeToken](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_RevokeToken.html) .\n\nIf you don't include this parameter, token revocation is automatically enabled for the new user pool client.", - "ExplicitAuthFlows": "The authentication flows that are supported by the user pool clients. Flow names without the `ALLOW_` prefix are deprecated in favor of new names with the `ALLOW_` prefix. Note that values with `ALLOW_` prefix cannot be used along with values without `ALLOW_` prefix.\n\nValid values include:\n\n- `ALLOW_ADMIN_USER_PASSWORD_AUTH` : Enable admin based user password authentication flow `ADMIN_USER_PASSWORD_AUTH` . This setting replaces the `ADMIN_NO_SRP_AUTH` setting. With this authentication flow, Cognito receives the password in the request instead of using the SRP (Secure Remote Password protocol) protocol to verify passwords.\n- `ALLOW_CUSTOM_AUTH` : Enable Lambda trigger based authentication.\n- `ALLOW_USER_PASSWORD_AUTH` : Enable user password-based authentication. In this flow, Cognito receives the password in the request instead of using the SRP protocol to verify passwords.\n- `ALLOW_USER_SRP_AUTH` : Enable SRP based authentication.\n- `ALLOW_REFRESH_TOKEN_AUTH` : Enable authflow to refresh tokens.", + "EnableTokenRevocation": "Activates or deactivates token revocation. For more information about revoking tokens, see [RevokeToken](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_RevokeToken.html) .\n\nIf you don't include this parameter, token revocation is automatically activated for the new user pool client.", + "ExplicitAuthFlows": "The authentication flows that are supported by the user pool clients. Flow names without the `ALLOW_` prefix are no longer supported, in favor of new names with the `ALLOW_` prefix. Note that values with `ALLOW_` prefix must be used only along with the `ALLOW_` prefix.\n\nValid values include:\n\n- `ALLOW_ADMIN_USER_PASSWORD_AUTH` : Enable admin based user password authentication flow `ADMIN_USER_PASSWORD_AUTH` . This setting replaces the `ADMIN_NO_SRP_AUTH` setting. With this authentication flow, Amazon Cognito receives the password in the request instead of using the Secure Remote Password (SRP) protocol to verify passwords.\n- `ALLOW_CUSTOM_AUTH` : Enable AWS Lambda trigger based authentication.\n- `ALLOW_USER_PASSWORD_AUTH` : Enable user password-based authentication. In this flow, Amazon Cognito receives the password in the request instead of using the SRP protocol to verify passwords.\n- `ALLOW_USER_SRP_AUTH` : Enable SRP-based authentication.\n- `ALLOW_REFRESH_TOKEN_AUTH` : Enable authflow to refresh tokens.", "GenerateSecret": "Boolean to specify whether you want to generate a secret for the user pool client being created.", "IdTokenValidity": "The time limit, after which the ID token is no longer valid and cannot be used.", "LogoutURLs": "A list of allowed logout URLs for the identity providers.", "PreventUserExistenceErrors": "Use this setting to choose which errors and responses are returned by Cognito APIs during authentication, account confirmation, and password recovery when the user does not exist in the user pool. When set to `ENABLED` and the user does not exist, authentication returns an error indicating either the username or password was incorrect, and account confirmation and password recovery return a response indicating a code was sent to a simulated destination. When set to `LEGACY` , those APIs will return a `UserNotFoundException` exception if the user does not exist in the user pool.", "ReadAttributes": "The read attributes.", - "RefreshTokenValidity": "The time limit, in days, after which the refresh token is no longer valid and cannot be used.", + "RefreshTokenValidity": "The time limit, in days, after which the refresh token is no longer valid and can't be used.", "SupportedIdentityProviders": "A list of provider names for the identity providers that are supported on this client. The following are supported: `COGNITO` , `Facebook` , `SignInWithApple` , `Google` and `LoginWithAmazon` .", "TokenValidityUnits": "The units in which the validity times are represented in. Default for RefreshToken is days, and default for ID and access tokens are hours.", "UserPoolId": "The user pool ID for the user pool where you want to create a user pool client.", - "WriteAttributes": "The user pool attributes that the app client can write to.\n\nIf your app client allows users to sign in through an identity provider, this array must include all attributes that are mapped to identity provider attributes. Amazon Cognito updates mapped attributes when users sign in to your application through an identity provider. If your app client lacks write access to a mapped attribute, Amazon Cognito throws an error when it attempts to update the attribute. For more information, see [Specifying Identity Provider Attribute Mappings for Your User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-specifying-attribute-mapping.html) ." + "WriteAttributes": "The user pool attributes that the app client can write to.\n\nIf your app client allows users to sign in through an identity provider, this array must include all attributes that are mapped to identity provider attributes. Amazon Cognito updates mapped attributes when users sign in to your application through an identity provider. If your app client lacks write access to a mapped attribute, Amazon Cognito throws an error when it tries to update the attribute. For more information, see [Specifying Identity Provider Attribute Mappings for Your User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-specifying-attribute-mapping.html) ." } }, "AWS::Cognito::UserPoolClient.AnalyticsConfiguration": { "attributes": {}, - "description": "The Amazon Pinpoint analytics configuration for collecting metrics for a user pool.\n\n> In regions where Pinpoint is not available, Cognito User Pools only supports sending events to Amazon Pinpoint projects in us-east-1. In regions where Pinpoint is available, Cognito User Pools will support sending events to Amazon Pinpoint projects within that same region.", + "description": "The Amazon Pinpoint analytics configuration for collecting metrics for a user pool.\n\n> In Regions where Pinpoint isn't available, User Pools only supports sending events to Amazon Pinpoint projects in us-east-1. In Regions where Pinpoint is available, User Pools will support sending events to Amazon Pinpoint projects within that same Region.", "properties": { - "ApplicationArn": "", + "ApplicationArn": "The Amazon Resource Name (ARN) of an Amazon Pinpoint project. You can use the Amazon Pinpoint project for integration with the chosen user pool client. Amazon Cognito publishes events to the Amazon Pinpoint project that the app ARN declares.", "ApplicationId": "The application ID for an Amazon Pinpoint application.", "ExternalId": "The external ID.", - "RoleArn": "The ARN of an IAM role that authorizes Amazon Cognito to publish events to Amazon Pinpoint analytics.", + "RoleArn": "The ARN of an AWS Identity and Access Management role that authorizes Amazon Cognito to publish events to Amazon Pinpoint analytics.", "UserDataShared": "If `UserDataShared` is `true` , Amazon Cognito will include user data in the events it publishes to Amazon Pinpoint analytics." } }, @@ -8505,8 +8676,8 @@ "properties": { "Description": "A string containing the description of the group.", "GroupName": "The name of the group. Must be unique.", - "Precedence": "A nonnegative integer value that specifies the precedence of this group relative to the other groups that a user can belong to in the user pool. Zero is the highest precedence value. Groups with lower `Precedence` values take precedence over groups with higher or null `Precedence` values. If a user belongs to two or more groups, it is the group with the lowest precedence value whose role ARN will be used in the `cognito:roles` and `cognito:preferred_role` claims in the user's tokens.\n\nTwo groups can have the same `Precedence` value. If this happens, neither group takes precedence over the other. If two groups with the same `Precedence` have the same role ARN, that role is used in the `cognito:preferred_role` claim in tokens for users in each group. If the two groups have different role ARNs, the `cognito:preferred_role` claim is not set in users' tokens.\n\nThe default `Precedence` value is null.", - "RoleArn": "The role ARN for the group.", + "Precedence": "A non-negative integer value that specifies the precedence of this group relative to the other groups that a user can belong to in the user pool. Zero is the highest precedence value. Groups with lower `Precedence` values take precedence over groups with higher ornull `Precedence` values. If a user belongs to two or more groups, it is the group with the lowest precedence value whose role ARN is given in the user's tokens for the `cognito:roles` and `cognito:preferred_role` claims.\n\nTwo groups can have the same `Precedence` value. If this happens, neither group takes precedence over the other. If two groups with the same `Precedence` have the same role ARN, that role is used in the `cognito:preferred_role` claim in tokens for users in each group. If the two groups have different role ARNs, the `cognito:preferred_role` claim isn't set in users' tokens.\n\nThe default `Precedence` value is null.", + "RoleArn": "The role Amazon Resource Name (ARN) for the group.", "UserPoolId": "The user pool ID for the user pool." } }, @@ -8518,7 +8689,7 @@ "properties": { "AttributeMapping": "A mapping of identity provider attributes to standard and custom user pool attributes.", "IdpIdentifiers": "A list of identity provider identifiers.", - "ProviderDetails": "The identity provider details. The following list describes the provider detail keys for each identity provider type.\n\n- For Google and Login with Amazon:\n\n- client_id\n- client_secret\n- authorize_scopes\n- For Facebook:\n\n- client_id\n- client_secret\n- authorize_scopes\n- api_version\n- For Sign in with Apple:\n\n- client_id\n- team_id\n- key_id\n- private_key\n- authorize_scopes\n- For OIDC providers:\n\n- client_id\n- client_secret\n- attributes_request_method\n- oidc_issuer\n- authorize_scopes\n- authorize_url *if not available from discovery URL specified by oidc_issuer key*\n- token_url *if not available from discovery URL specified by oidc_issuer key*\n- attributes_url *if not available from discovery URL specified by oidc_issuer key*\n- jwks_uri *if not available from discovery URL specified by oidc_issuer key*\n- For SAML providers:\n\n- MetadataFile OR MetadataURL\n- IDPSignout *optional*", + "ProviderDetails": "The identity provider details. The following list describes the provider detail keys for each identity provider type.\n\n- For Google and Login with Amazon:\n\n- client_id\n- client_secret\n- authorize_scopes\n- For Facebook:\n\n- client_id\n- client_secret\n- authorize_scopes\n- api_version\n- For Sign in with Apple:\n\n- client_id\n- team_id\n- key_id\n- private_key\n- authorize_scopes\n- For OpenID Connect (OIDC) providers:\n\n- client_id\n- client_secret\n- attributes_request_method\n- oidc_issuer\n- authorize_scopes\n- authorize_url *if not available from discovery URL specified by oidc_issuer key*\n- token_url *if not available from discovery URL specified by oidc_issuer key*\n- attributes_url *if not available from discovery URL specified by oidc_issuer key*\n- jwks_uri *if not available from discovery URL specified by oidc_issuer key*\n- attributes_url_add_attributes *a read-only property that is set automatically*\n- For SAML providers:\n\n- MetadataFile OR MetadataURL\n- IDPSignout (optional)", "ProviderName": "The identity provider name.", "ProviderType": "The identity provider type.", "UserPoolId": "The user pool ID." @@ -8550,9 +8721,9 @@ }, "description": "The `AWS::Cognito::UserPoolRiskConfigurationAttachment` resource sets the risk configuration that is used for Amazon Cognito advanced security features.\n\nYou can specify risk configuration for a single client (with a specific `clientId` ) or for all clients (by setting the `clientId` to `ALL` ). If you specify `ALL` , the default configuration is used for every client that has had no risk configuration set previously. If you specify risk configuration for a particular client, it no longer falls back to the `ALL` configuration.", "properties": { - "AccountTakeoverRiskConfiguration": "The account takeover risk configuration object including the `NotifyConfiguration` object and `Actions` to take in the case of an account takeover.", + "AccountTakeoverRiskConfiguration": "The account takeover risk configuration object, including the `NotifyConfiguration` object and `Actions` to take if there is an account takeover.", "ClientId": "The app client ID. You can specify the risk configuration for a single client (with a specific ClientId) or for all clients (by setting the ClientId to `ALL` ).", - "CompromisedCredentialsRiskConfiguration": "The compromised credentials risk configuration object including the `EventFilter` and the `EventAction`", + "CompromisedCredentialsRiskConfiguration": "The compromised credentials risk configuration object, including the `EventFilter` and the `EventAction` .", "RiskExceptionConfiguration": "The configuration to override the risk decision.", "UserPoolId": "The user pool ID." } @@ -8561,7 +8732,7 @@ "attributes": {}, "description": "Account takeover action type.", "properties": { - "EventAction": "The event action.\n\n- `BLOCK` Choosing this action will block the request.\n- `MFA_IF_CONFIGURED` Throw MFA challenge if user has configured it, else allow the request.\n- `MFA_REQUIRED` Throw MFA challenge if user has configured it, else block the request.\n- `NO_ACTION` Allow the user sign-in.", + "EventAction": "The event action.\n\n- `BLOCK` Choosing this action will block the request.\n- `MFA_IF_CONFIGURED` Present an MFA challenge if user has configured it, else allow the request.\n- `MFA_REQUIRED` Present an MFA challenge if user has configured it, else block the request.\n- `NO_ACTION` Allow the user to sign in.", "Notify": "Flag specifying whether to send a notification." } }, @@ -8578,13 +8749,13 @@ "attributes": {}, "description": "Configuration for mitigation actions and notification for different levels of risk detected for a potential account takeover.", "properties": { - "Actions": "Account takeover risk configuration actions", + "Actions": "Account takeover risk configuration actions.", "NotifyConfiguration": "The notify configuration used to construct email notifications." } }, "AWS::Cognito::UserPoolRiskConfigurationAttachment.CompromisedCredentialsActionsType": { "attributes": {}, - "description": "The compromised credentials actions type", + "description": "The compromised credentials actions type.", "properties": { "EventAction": "The event action." } @@ -8602,28 +8773,28 @@ "description": "The notify configuration type.", "properties": { "BlockEmail": "Email template used when a detected risk event is blocked.", - "From": "The email address that is sending the email. It must be either individually verified with Amazon SES, or from a domain that has been verified with Amazon SES.", - "MfaEmail": "The MFA email template used when MFA is challenged as part of a detected risk.", + "From": "The email address that is sending the email. The address must be either individually verified with Amazon Simple Email Service, or from a domain that has been verified with Amazon SES.", + "MfaEmail": "The multi-factor authentication (MFA) email template used when MFA is challenged as part of a detected risk.", "NoActionEmail": "The email template used when a detected risk event is allowed.", "ReplyTo": "The destination to which the receiver of an email should reply to.", - "SourceArn": "The Amazon Resource Name (ARN) of the identity that is associated with the sending authorization policy. It permits Amazon Cognito to send for the email address specified in the `From` parameter." + "SourceArn": "The Amazon Resource Name (ARN) of the identity that is associated with the sending authorization policy. This identity permits Amazon Cognito to send for the email address specified in the `From` parameter." } }, "AWS::Cognito::UserPoolRiskConfigurationAttachment.NotifyEmailType": { "attributes": {}, "description": "The notify email type.", "properties": { - "HtmlBody": "The HTML body.", - "Subject": "The subject.", - "TextBody": "The text body." + "HtmlBody": "The email HTML body.", + "Subject": "The email subject.", + "TextBody": "The email text body." } }, "AWS::Cognito::UserPoolRiskConfigurationAttachment.RiskExceptionConfigurationType": { "attributes": {}, "description": "The type of the configuration to override the risk decision.", "properties": { - "BlockedIPRangeList": "Overrides the risk decision to always block the pre-authentication requests. The IP range is in CIDR notation: a compact representation of an IP address and its associated routing prefix.", - "SkippedIPRangeList": "Risk detection is not performed on the IP addresses in the range list. The IP range is in CIDR notation." + "BlockedIPRangeList": "Overrides the risk decision to always block the pre-authentication requests. The IP range is in CIDR notation, a compact representation of an IP address and its routing prefix.", + "SkippedIPRangeList": "Risk detection isn't performed on the IP addresses in this range list. The IP range is in CIDR notation." } }, "AWS::Cognito::UserPoolUICustomizationAttachment": { @@ -8644,13 +8815,13 @@ "description": "The `AWS::Cognito::UserPoolUser` resource creates an Amazon Cognito user pool user.", "properties": { "ClientMetadata": "A map of custom key-value pairs that you can provide as input for the custom workflow that is invoked by the *pre sign-up* trigger.\n\nYou create custom workflows by assigning AWS Lambda functions to user pool triggers. When you create a `UserPoolUser` resource and include the `ClientMetadata` property, Amazon Cognito invokes the function that is assigned to the *pre sign-up* trigger. When Amazon Cognito invokes this function, it passes a JSON payload, which the function receives as input. This payload contains a `clientMetadata` attribute, which provides the data that you assigned to the ClientMetadata property. In your function code in AWS Lambda , you can process the `clientMetadata` value to enhance your workflow for your specific needs.\n\nFor more information, see [Customizing User Pool Workflows with Lambda Triggers](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html) in the *Amazon Cognito Developer Guide* .\n\n> Take the following limitations into consideration when you use the ClientMetadata parameter:\n> \n> - Amazon Cognito does not store the ClientMetadata value. This data is available only to AWS Lambda triggers that are assigned to a user pool to support custom workflows. If your user pool configuration does not include triggers, the ClientMetadata parameter serves no purpose.\n> - Amazon Cognito does not validate the ClientMetadata value.\n> - Amazon Cognito does not encrypt the the ClientMetadata value, so don't use it to provide sensitive information.", - "DesiredDeliveryMediums": "Specify `\"EMAIL\"` if email will be used to send the welcome message. Specify `\"SMS\"` if the phone number will be used. The default value is `\"SMS\"` . More than one value can be specified.", - "ForceAliasCreation": "This parameter is only used if the `phone_number_verified` or `email_verified` attribute is set to `True` . Otherwise, it is ignored.\n\nIf this parameter is set to `True` and the phone number or email address specified in the UserAttributes parameter already exists as an alias with a different user, the API call will migrate the alias from the previous user to the newly created user. The previous user will no longer be able to log in using that alias.\n\nIf this parameter is set to `False` , the API throws an `AliasExistsException` error if the alias already exists. The default value is `False` .", - "MessageAction": "Set to `\"RESEND\"` to resend the invitation message to a user that already exists and reset the expiration limit on the user's account. Set to `\"SUPPRESS\"` to suppress sending the message. Only one value can be specified.", + "DesiredDeliveryMediums": "Specify `\"EMAIL\"` if email will be used to send the welcome message. Specify `\"SMS\"` if the phone number will be used. The default value is `\"SMS\"` . You can specify more than one value.", + "ForceAliasCreation": "This parameter is used only if the `phone_number_verified` or `email_verified` attribute is set to `True` . Otherwise, it is ignored.\n\nIf this parameter is set to `True` and the phone number or email address specified in the UserAttributes parameter already exists as an alias with a different user, the API call will migrate the alias from the previous user to the newly created user. The previous user will no longer be able to log in using that alias.\n\nIf this parameter is set to `False` , the API throws an `AliasExistsException` error if the alias already exists. The default value is `False` .", + "MessageAction": "Set to `RESEND` to resend the invitation message to a user that already exists and reset the expiration limit on the user's account. Set to `SUPPRESS` to suppress sending the message. You can specify only one value.", "UserAttributes": "The user attributes and attribute values to be set for the user to be created. These are name-value pairs You can create a user without specifying any attributes other than `Username` . However, any attributes that you specify as required (in [](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_CreateUserPool.html) or in the *Attributes* tab of the console) must be supplied either by you (in your call to `AdminCreateUser` ) or by the user (when they sign up in response to your welcome message).\n\nFor custom attributes, you must prepend the `custom:` prefix to the attribute name.\n\nTo send a message inviting the user to sign up, you must specify the user's email address or phone number. This can be done in your call to AdminCreateUser or in the *Users* tab of the Amazon Cognito console for managing your user pools.\n\nIn your call to `AdminCreateUser` , you can set the `email_verified` attribute to `True` , and you can set the `phone_number_verified` attribute to `True` . (You can also do this by calling [](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminUpdateUserAttributes.html) .)\n\n- *email* : The email address of the user to whom the message that contains the code and user name will be sent. Required if the `email_verified` attribute is set to `True` , or if `\"EMAIL\"` is specified in the `DesiredDeliveryMediums` parameter.\n- *phone_number* : The phone number of the user to whom the message that contains the code and user name will be sent. Required if the `phone_number_verified` attribute is set to `True` , or if `\"SMS\"` is specified in the `DesiredDeliveryMediums` parameter.", "UserPoolId": "The user pool ID for the user pool where the user will be created.", - "Username": "The username for the user. Must be unique within the user pool. Must be a UTF-8 string between 1 and 128 characters. After the user is created, the username cannot be changed.", - "ValidationData": "The user's validation data. This is an array of name-value pairs that contain user attributes and attribute values that you can use for custom validation, such as restricting the types of user accounts that can be registered. For example, you might choose to allow or disallow user sign-up based on the user's domain.\n\nTo configure custom validation, you must create a Pre Sign-up Lambda trigger for the user pool as described in the Amazon Cognito Developer Guide. The Lambda trigger receives the validation data and uses it in the validation process.\n\nThe user's validation data is not persisted." + "Username": "The username for the user. Must be unique within the user pool. Must be a UTF-8 string between 1 and 128 characters. After the user is created, the username can't be changed.", + "ValidationData": "The user's validation data. This is an array of name-value pairs that contain user attributes and attribute values that you can use for custom validation, such as restricting the types of user accounts that can be registered. For example, you might choose to allow or disallow user sign-up based on the user's domain.\n\nTo configure custom validation, you must create a Pre Sign-up AWS Lambda trigger for the user pool as described in the Amazon Cognito Developer Guide. The Lambda trigger receives the validation data and uses it in the validation process.\n\nThe user's validation data isn't persisted." } }, "AWS::Cognito::UserPoolUser.AttributeType": { @@ -8674,7 +8845,7 @@ }, "AWS::Config::AggregationAuthorization": { "attributes": { - "AggregationAuthorizationArn": "", + "AggregationAuthorizationArn": "The Amazon Resource Name (ARN) of the aggregation object.", "Ref": "`Ref` returns the ARN of the AggregationAuthorization, such as `arn:aws:config:us-east-1:123456789012:aggregation-authorization/987654321012/us-west-2` ." }, "description": "An object that represents the authorizations granted to aggregator accounts and regions.", @@ -8731,7 +8902,7 @@ }, "AWS::Config::ConfigurationAggregator": { "attributes": { - "ConfigurationAggregatorArn": "", + "ConfigurationAggregatorArn": "The Amazon Resource Name (ARN) of the aggregator.", "Ref": "`Ref` returns the ConfigurationAggregatorName, such as `myConfigurationAggregator` ." }, "description": "The details about the configuration aggregator, including information about source accounts, regions, and metadata of the aggregator.", @@ -8827,14 +8998,32 @@ "attributes": { "Ref": "`Ref` returns the OrganizationConfigRuleName." }, - "description": "An organization config rule that has information about config rules that AWS Config creates in member accounts. Only a master account can create or update an organization config rule.\n\n`OrganizationConfigRule` resource enables organization service access through `EnableAWSServiceAccess` action and creates a service linked role in the master account of your organization. The service linked role is created only when the role does not exist in the master account. AWS Config verifies the existence of role with `GetRole` action.\n\nWhen creating custom organization config rules using a centralized Lambda function, you will need to allow Lambda permissions to sub-accounts and you will need to create an IAM role will to pass to the Lambda function. For more information, see [How to Centrally Manage AWS Config Rules across Multiple AWS Accounts](https://docs.aws.amazon.com/devops/how-to-centrally-manage-aws-config-rules-across-multiple-aws-accounts/) .", + "description": "An organization config rule that has information about config rules that AWS Config creates in member accounts. Only a master account and a delegated administrator can create or update an organization config rule.\n\n`OrganizationConfigRule` resource enables organization service access through `EnableAWSServiceAccess` action and creates a service linked role in the master account of your organization. The service linked role is created only when the role does not exist in the master account. AWS Config verifies the existence of role with `GetRole` action.\n\nWhen creating custom organization config rules using a centralized Lambda function, you will need to allow Lambda permissions to sub-accounts and you will need to create an IAM role will to pass to the Lambda function. For more information, see [How to Centrally Manage AWS Config Rules across Multiple AWS Accounts](https://docs.aws.amazon.com/devops/how-to-centrally-manage-aws-config-rules-across-multiple-aws-accounts/) .", "properties": { "ExcludedAccounts": "A comma-separated list of accounts excluded from organization config rule.", "OrganizationConfigRuleName": "The name that you assign to organization config rule.", + "OrganizationCustomCodeRuleMetadata": "", "OrganizationCustomRuleMetadata": "An `OrganizationCustomRuleMetadata` object.", "OrganizationManagedRuleMetadata": "An `OrganizationManagedRuleMetadata` object." } }, + "AWS::Config::OrganizationConfigRule.OrganizationCustomCodeRuleMetadata": { + "attributes": {}, + "description": "", + "properties": { + "CodeText": "", + "DebugLogDeliveryAccounts": "", + "Description": "", + "InputParameters": "", + "MaximumExecutionFrequency": "", + "OrganizationConfigRuleTriggerTypes": "", + "ResourceIdScope": "", + "ResourceTypesScope": "", + "Runtime": "", + "TagKeyScope": "", + "TagValueScope": "" + } + }, "AWS::Config::OrganizationConfigRule.OrganizationCustomRuleMetadata": { "attributes": {}, "description": "An object that specifies organization custom rule metadata such as resource type, resource ID of AWS resource, Lambda function ARN, and organization trigger types that trigger AWS Config to evaluate your AWS resources against a rule. It also provides the frequency with which you want AWS Config to run evaluations for the rule if the trigger type is periodic.", @@ -8944,8 +9133,8 @@ }, "AWS::Config::StoredQuery": { "attributes": { - "QueryArn": "", - "QueryId": "", + "QueryArn": "Amazon Resource Name (ARN) of the query. For example, arn:partition:service:region:account-id:resource-type/resource-name/resource-id.", + "QueryId": "The ID of the query.", "Ref": "" }, "description": "Provides the details of a stored query.", @@ -9143,6 +9332,7 @@ "DomainName": "The unique name of the domain.", "FlowDefinition": "", "ObjectTypeName": "The name of the profile object type mapping to use.", + "ObjectTypeNames": "", "Tags": "The tags used to organize, track, or control access for this resource.", "Uri": "The URI of the S3 bucket or any other type of data source." } @@ -9184,6 +9374,14 @@ "Object": "" } }, + "AWS::CustomerProfiles::Integration.ObjectTypeMapping": { + "attributes": {}, + "description": "", + "properties": { + "Key": "", + "Value": "" + } + }, "AWS::CustomerProfiles::Integration.S3SourceProperties": { "attributes": {}, "description": "", @@ -9580,7 +9778,7 @@ "description": "The `AWS::DMS::Endpoint` resource creates an AWS DMS endpoint.\n\nCurrently, the only endpoint setting types that AWS CloudFormation supports are *DynamoDBSettings* , *ElasticSearchSettings* , and *NeptuneSettings* .", "properties": { "CertificateArn": "The Amazon Resource Name (ARN) for the certificate.", - "DatabaseName": "The name of the endpoint database. For a MySQL source or target endpoint, do not specify DatabaseName.", + "DatabaseName": "The name of the endpoint database. For a MySQL source or target endpoint, do not specify DatabaseName. To migrate to a specific database, use this setting and `targetDbType` .", "DocDbSettings": "Settings in JSON format for the source DocumentDB endpoint. For more information about the available settings, see the configuration properties section in [Using DocumentDB as a Target for AWS Database Migration Service](https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Source.DocumentDB.html) in the *AWS Database Migration Service User Guide.*", "DynamoDbSettings": "Settings in JSON format for the target Amazon DynamoDB endpoint. For information about other available settings, see [Using Object Mapping to Migrate Data to DynamoDB](https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Target.DynamoDB.html#CHAP_Target.DynamoDB.ObjectMapping) in the *AWS Database Migration Service User Guide.*", "ElasticsearchSettings": "Settings in JSON format for the target OpenSearch endpoint. For more information about the available settings, see [Extra Connection Attributes When Using OpenSearch as a Target for AWS DMS](https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Target.Elasticsearch.html#CHAP_Target.Elasticsearch.Configuration) in the *AWS Database Migration Service User Guide* .", @@ -9588,6 +9786,7 @@ "EndpointType": "The type of endpoint. Valid values are `source` and `target` .", "EngineName": "The type of engine for the endpoint. Valid values, depending on the `EndpointType` value, include `\"mysql\"` , `\"oracle\"` , `\"postgres\"` , `\"mariadb\"` , `\"aurora\"` , `\"aurora-postgresql\"` , `\"opensearch\"` , `\"redshift\"` , `\"s3\"` , `\"db2\"` , `\"azuredb\"` , `\"sybase\"` , `\"dynamodb\"` , `\"mongodb\"` , `\"kinesis\"` , `\"kafka\"` , `\"elasticsearch\"` , `\"docdb\"` , `\"sqlserver\"` , and `\"neptune\"` .", "ExtraConnectionAttributes": "Additional attributes associated with the connection. Each attribute is specified as a name-value pair associated by an equal sign (=). Multiple attributes are separated by a semicolon (;) with no additional white space. For information on the attributes available for connecting your source or target endpoint, see [Working with AWS DMS Endpoints](https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Endpoints.html) in the *AWS Database Migration Service User Guide.*", + "GcpMySQLSettings": "Settings in JSON format for the source GCP MySQL endpoint.", "IbmDb2Settings": "Not currently supported by AWS CloudFormation .", "KafkaSettings": "Settings in JSON format for the target Apache Kafka endpoint. For more information about the available settings, see [Using object mapping to migrate data to a Kafka topic](https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Target.Kafka.html#CHAP_Target.Kafka.ObjectMapping) in the *AWS Database Migration Service User Guide.*", "KinesisSettings": "Settings in JSON format for the target endpoint for Amazon Kinesis Data Streams. For more information about the available settings, see [Using Amazon Kinesis Data Streams as a Target for AWS Database Migration Service](https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Target.Kinesis.html) in the *AWS Database Migration Service User Guide.*", @@ -9636,6 +9835,25 @@ "ServiceAccessRoleArn": "The Amazon Resource Name (ARN) used by the service to access the IAM role. The role must allow the `iam:PassRole` action." } }, + "AWS::DMS::Endpoint.GcpMySQLSettings": { + "attributes": {}, + "description": "Settings in JSON format for the source GCP MySQL endpoint.", + "properties": { + "AfterConnectScript": "Specifies a script to run immediately after AWS DMS connects to the endpoint. The migration task continues running regardless if the SQL statement succeeds or fails.\n\nFor this parameter, provide the code of the script itself, not the name of a file containing the script.", + "CleanSourceMetadataOnMismatch": "Adjusts the behavior of AWS DMS when migrating from an SQL Server source database that is hosted as part of an Always On availability group cluster. If you need AWS DMS to poll all the nodes in the Always On cluster for transaction backups, set this attribute to `false` .", + "DatabaseName": "Database name for the endpoint. For a MySQL source or target endpoint, don't explicitly specify the database using the `DatabaseName` request parameter on either the `CreateEndpoint` or `ModifyEndpoint` API call. Specifying `DatabaseName` when you create or modify a MySQL endpoint replicates all the task tables to this single database. For MySQL endpoints, you specify the database only when you specify the schema in the table-mapping rules of the AWS DMS task.", + "EventsPollInterval": "Specifies how often to check the binary log for new changes/events when the database is idle. The default is five seconds.\n\nExample: `eventsPollInterval=5;`\n\nIn the example, AWS DMS checks for changes in the binary logs every five seconds.", + "MaxFileSize": "Specifies the maximum size (in KB) of any .csv file used to transfer data to a MySQL-compatible database.\n\nExample: `maxFileSize=512`", + "ParallelLoadThreads": "Improves performance when loading data into the MySQL-compatible target database. Specifies how many threads to use to load the data into the MySQL-compatible target database. Setting a large number of threads can have an adverse effect on database performance, because a separate connection is required for each thread. The default is one.\n\nExample: `parallelLoadThreads=1`", + "Password": "Endpoint connection password.", + "Port": "", + "SecretsManagerAccessRoleArn": "The full Amazon Resource Name (ARN) of the IAM role that specifies AWS DMS as the trusted entity and grants the required permissions to access the value in `SecretsManagerSecret.` The role must allow the `iam:PassRole` action. `SecretsManagerSecret` has the value of the AWS Secrets Manager secret that allows access to the MySQL endpoint.\n\n> You can specify one of two sets of values for these permissions. You can specify the values for this setting and `SecretsManagerSecretId` . Or you can specify clear-text values for `UserName` , `Password` , `ServerName` , and `Port` . You can't specify both. For more information on creating this `SecretsManagerSecret` and the `SecretsManagerAccessRoleArn` and `SecretsManagerSecretId` required to access it, see [Using secrets to access AWS Database Migration Service resources](https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Security.html#security-iam-secretsmanager) in the AWS Database Migration Service User Guide.", + "SecretsManagerSecretId": "The full ARN, partial ARN, or friendly name of the `SecretsManagerSecret` that contains the MySQL endpoint connection details.", + "ServerName": "Endpoint TCP port.", + "ServerTimezone": "Specifies the time zone for the source MySQL database.\n\nExample: `serverTimezone=US/Pacific;`\n\nNote: Do not enclose time zones in single quotes.", + "Username": "Endpoint connection user name." + } + }, "AWS::DMS::Endpoint.IbmDb2Settings": { "attributes": {}, "description": "Not currently supported by AWS CloudFormation .", @@ -9771,13 +9989,44 @@ "attributes": {}, "description": "Not currently supported by AWS CloudFormation .", "properties": { + "AddColumnName": "An optional parameter that, when set to `true` or `y` , you can use to add column name information to the .csv output file.\n\nThe default value is `false` . Valid values are `true` , `false` , `y` , and `n` .", "BucketFolder": "Not currently supported by AWS CloudFormation .", "BucketName": "Not currently supported by AWS CloudFormation .", + "CannedAclForObjects": "A value that enables AWS DMS to specify a predefined (canned) access control list for objects created in an Amazon S3 bucket as .csv or .parquet files. For more information about Amazon S3 canned ACLs, see [Canned ACL](https://docs.aws.amazon.com/http://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl) in the *Amazon S3 Developer Guide.*\n\nThe default value is NONE. Valid values include NONE, PRIVATE, PUBLIC_READ, PUBLIC_READ_WRITE, AUTHENTICATED_READ, AWS_EXEC_READ, BUCKET_OWNER_READ, and BUCKET_OWNER_FULL_CONTROL.", + "CdcInsertsAndUpdates": "A value that enables a change data capture (CDC) load to write INSERT and UPDATE operations to .csv or .parquet (columnar storage) output files. The default setting is `false` , but when `CdcInsertsAndUpdates` is set to `true` or `y` , only INSERTs and UPDATEs from the source database are migrated to the .csv or .parquet file.\n\nFor .csv file format only, how these INSERTs and UPDATEs are recorded depends on the value of the `IncludeOpForFullLoad` parameter. If `IncludeOpForFullLoad` is set to `true` , the first field of every CDC record is set to either `I` or `U` to indicate INSERT and UPDATE operations at the source. But if `IncludeOpForFullLoad` is set to `false` , CDC records are written without an indication of INSERT or UPDATE operations at the source. For more information about how these settings work together, see [Indicating Source DB Operations in Migrated S3 Data](https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Target.S3.html#CHAP_Target.S3.Configuring.InsertOps) in the *AWS Database Migration Service User Guide.* .\n\n> AWS DMS supports the use of the `CdcInsertsAndUpdates` parameter in versions 3.3.1 and later.\n> \n> `CdcInsertsOnly` and `CdcInsertsAndUpdates` can't both be set to `true` for the same endpoint. Set either `CdcInsertsOnly` or `CdcInsertsAndUpdates` to `true` for the same endpoint, but not both.", + "CdcInsertsOnly": "A value that enables a change data capture (CDC) load to write only INSERT operations to .csv or columnar storage (.parquet) output files. By default (the `false` setting), the first field in a .csv or .parquet record contains the letter I (INSERT), U (UPDATE), or D (DELETE). These values indicate whether the row was inserted, updated, or deleted at the source database for a CDC load to the target.\n\nIf `CdcInsertsOnly` is set to `true` or `y` , only INSERTs from the source database are migrated to the .csv or .parquet file. For .csv format only, how these INSERTs are recorded depends on the value of `IncludeOpForFullLoad` . If `IncludeOpForFullLoad` is set to `true` , the first field of every CDC record is set to I to indicate the INSERT operation at the source. If `IncludeOpForFullLoad` is set to `false` , every CDC record is written without a first field to indicate the INSERT operation at the source. For more information about how these settings work together, see [Indicating Source DB Operations in Migrated S3 Data](https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Target.S3.html#CHAP_Target.S3.Configuring.InsertOps) in the *AWS Database Migration Service User Guide.* .\n\n> AWS DMS supports the interaction described preceding between the `CdcInsertsOnly` and `IncludeOpForFullLoad` parameters in versions 3.1.4 and later.\n> \n> `CdcInsertsOnly` and `CdcInsertsAndUpdates` can't both be set to `true` for the same endpoint. Set either `CdcInsertsOnly` or `CdcInsertsAndUpdates` to `true` for the same endpoint, but not both.", + "CdcMaxBatchInterval": "Maximum length of the interval, defined in seconds, after which to output a file to Amazon S3.\n\nWhen `CdcMaxBatchInterval` and `CdcMinFileSize` are both specified, the file write is triggered by whichever parameter condition is met first within an AWS DMS CloudFormation template.\n\nThe default value is 60 seconds.", + "CdcMinFileSize": "Minimum file size, defined in megabytes, to reach for a file output to Amazon S3.\n\nWhen `CdcMinFileSize` and `CdcMaxBatchInterval` are both specified, the file write is triggered by whichever parameter condition is met first within an AWS DMS CloudFormation template.\n\nThe default value is 32 MB.", + "CdcPath": "Specifies the folder path of CDC files. For an S3 source, this setting is required if a task captures change data; otherwise, it's optional. If `CdcPath` is set, AWS DMS reads CDC files from this path and replicates the data changes to the target endpoint. For an S3 target if you set [`PreserveTransactions`](https://docs.aws.amazon.com/dms/latest/APIReference/API_S3Settings.html#DMS-Type-S3Settings-PreserveTransactions) to `true` , AWS DMS verifies that you have set this parameter to a folder path on your S3 target where AWS DMS can save the transaction order for the CDC load. AWS DMS creates this CDC folder path in either your S3 target working directory or the S3 target location specified by [`BucketFolder`](https://docs.aws.amazon.com/dms/latest/APIReference/API_S3Settings.html#DMS-Type-S3Settings-BucketFolder) and [`BucketName`](https://docs.aws.amazon.com/dms/latest/APIReference/API_S3Settings.html#DMS-Type-S3Settings-BucketName) .\n\nFor example, if you specify `CdcPath` as `MyChangedData` , and you specify `BucketName` as `MyTargetBucket` but do not specify `BucketFolder` , AWS DMS creates the CDC folder path following: `MyTargetBucket/MyChangedData` .\n\nIf you specify the same `CdcPath` , and you specify `BucketName` as `MyTargetBucket` and `BucketFolder` as `MyTargetData` , AWS DMS creates the CDC folder path following: `MyTargetBucket/MyTargetData/MyChangedData` .\n\nFor more information on CDC including transaction order on an S3 target, see [Capturing data changes (CDC) including transaction order on the S3 target](https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Target.S3.html#CHAP_Target.S3.EndpointSettings.CdcPath) .\n\n> This setting is supported in AWS DMS versions 3.4.2 and later.", "CompressionType": "Not currently supported by AWS CloudFormation .", "CsvDelimiter": "Not currently supported by AWS CloudFormation .", + "CsvNoSupValue": "This setting only applies if your Amazon S3 output files during a change data capture (CDC) load are written in .csv format. If [`UseCsvNoSupValue`](https://docs.aws.amazon.com/dms/latest/APIReference/API_S3Settings.html#DMS-Type-S3Settings-UseCsvNoSupValue) is set to true, specify a string value that you want AWS DMS to use for all columns not included in the supplemental log. If you do not specify a string value, AWS DMS uses the null value for these columns regardless of the `UseCsvNoSupValue` setting.\n\n> This setting is supported in AWS DMS versions 3.4.1 and later.", + "CsvNullValue": "An optional parameter that specifies how AWS DMS treats null values. While handling the null value, you can use this parameter to pass a user-defined string as null when writing to the target. For example, when target columns are not nullable, you can use this option to differentiate between the empty string value and the null value. So, if you set this parameter value to the empty string (\"\" or ''), AWS DMS treats the empty string as the null value instead of `NULL` .\n\nThe default value is `NULL` . Valid values include any valid string.", "CsvRowDelimiter": "Not currently supported by AWS CloudFormation .", + "DataFormat": "The format of the data that you want to use for output. You can choose one of the following:\n\n- `csv` : This is a row-based file format with comma-separated values (.csv).\n- `parquet` : Apache Parquet (.parquet) is a columnar storage file format that features efficient compression and provides faster query response.", + "DataPageSize": "The size of one data page in bytes. This parameter defaults to 1024 * 1024 bytes (1 MiB). This number is used for .parquet file format only.", + "DatePartitionDelimiter": "Specifies a date separating delimiter to use during folder partitioning. The default value is `SLASH` . Use this parameter when `DatePartitionedEnabled` is set to `true` .", + "DatePartitionEnabled": "When set to `true` , this parameter partitions S3 bucket folders based on transaction commit dates. The default value is `false` . For more information about date-based folder partitioning, see [Using date-based folder partitioning](https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Target.S3.html#CHAP_Target.S3.DatePartitioning) .", + "DatePartitionSequence": "Identifies the sequence of the date format to use during folder partitioning. The default value is `YYYYMMDD` . Use this parameter when `DatePartitionedEnabled` is set to `true` .", + "DatePartitionTimezone": "When creating an S3 target endpoint, set `DatePartitionTimezone` to convert the current UTC time into a specified time zone. The conversion occurs when a date partition folder is created and a CDC filename is generated. The time zone format is Area/Location. Use this parameter when `DatePartitionedEnabled` is set to `true` , as shown in the following example.\n\n`s3-settings='{\"DatePartitionEnabled\": true, \"DatePartitionSequence\": \"YYYYMMDDHH\", \"DatePartitionDelimiter\": \"SLASH\", \"DatePartitionTimezone\":\" *Asia/Seoul* \", \"BucketName\": \"dms-nattarat-test\"}'`", + "DictPageSizeLimit": "The maximum size of an encoded dictionary page of a column. If the dictionary page exceeds this, this column is stored using an encoding type of `PLAIN` . This parameter defaults to 1024 * 1024 bytes (1 MiB), the maximum size of a dictionary page before it reverts to `PLAIN` encoding. This size is used for .parquet file format only.", + "EnableStatistics": "A value that enables statistics for Parquet pages and row groups. Choose `true` to enable statistics, `false` to disable. Statistics include `NULL` , `DISTINCT` , `MAX` , and `MIN` values. This parameter defaults to `true` . This value is used for .parquet file format only.", + "EncodingType": "The type of encoding you are using:\n\n- `RLE_DICTIONARY` uses a combination of bit-packing and run-length encoding to store repeated values more efficiently. This is the default.\n- `PLAIN` doesn't use encoding at all. Values are stored as they are.\n- `PLAIN_DICTIONARY` builds a dictionary of the values encountered in a given column. The dictionary is stored in a dictionary page for each column chunk.", + "EncryptionMode": "The type of server-side encryption that you want to use for your data. This encryption type is part of the endpoint settings or the extra connections attributes for Amazon S3. You can choose either `SSE_S3` (the default) or `SSE_KMS` .\n\n> For the `ModifyEndpoint` operation, you can change the existing value of the `EncryptionMode` parameter from `SSE_KMS` to `SSE_S3` . But you can\u2019t change the existing value from `SSE_S3` to `SSE_KMS` . \n\nTo use `SSE_S3` , you need an AWS Identity and Access Management (IAM) role with permission to allow `\"arn:aws:s3:::dms-*\"` to use the following actions:\n\n- `s3:CreateBucket`\n- `s3:ListBucket`\n- `s3:DeleteBucket`\n- `s3:GetBucketLocation`\n- `s3:GetObject`\n- `s3:PutObject`\n- `s3:DeleteObject`\n- `s3:GetObjectVersion`\n- `s3:GetBucketPolicy`\n- `s3:PutBucketPolicy`\n- `s3:DeleteBucketPolicy`", "ExternalTableDefinition": "Not currently supported by AWS CloudFormation .", - "ServiceAccessRoleArn": "Not currently supported by AWS CloudFormation ." + "IgnoreHeaderRows": "When this value is set to 1, AWS DMS ignores the first row header in a .csv file. A value of 1 turns on the feature; a value of 0 turns off the feature.\n\nThe default is 0.", + "IncludeOpForFullLoad": "A value that enables a full load to write INSERT operations to the comma-separated value (.csv) output files only to indicate how the rows were added to the source database.\n\n> AWS DMS supports the `IncludeOpForFullLoad` parameter in versions 3.1.4 and later. \n\nFor full load, records can only be inserted. By default (the `false` setting), no information is recorded in these output files for a full load to indicate that the rows were inserted at the source database. If `IncludeOpForFullLoad` is set to `true` or `y` , the INSERT is recorded as an I annotation in the first field of the .csv file. This allows the format of your target records from a full load to be consistent with the target records from a CDC load.\n\n> This setting works together with the `CdcInsertsOnly` and the `CdcInsertsAndUpdates` parameters for output to .csv files only. For more information about how these settings work together, see [Indicating Source DB Operations in Migrated S3 Data](https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Target.S3.html#CHAP_Target.S3.Configuring.InsertOps) in the *AWS Database Migration Service User Guide.* .", + "MaxFileSize": "A value that specifies the maximum size (in KB) of any .csv file to be created while migrating to an S3 target during full load.\n\nThe default value is 1,048,576 KB (1 GB). Valid values include 1 to 1,048,576.", + "ParquetTimestampInMillisecond": "A value that specifies the precision of any `TIMESTAMP` column values that are written to an Amazon S3 object file in .parquet format.\n\n> AWS DMS supports the `ParquetTimestampInMillisecond` parameter in versions 3.1.4 and later. \n\nWhen `ParquetTimestampInMillisecond` is set to `true` or `y` , AWS DMS writes all `TIMESTAMP` columns in a .parquet formatted file with millisecond precision. Otherwise, DMS writes them with microsecond precision.\n\nCurrently, Amazon Athena and AWS Glue can handle only millisecond precision for `TIMESTAMP` values. Set this parameter to `true` for S3 endpoint object files that are .parquet formatted only if you plan to query or process the data with Athena or AWS Glue .\n\n> AWS DMS writes any `TIMESTAMP` column values written to an S3 file in .csv format with microsecond precision.\n> \n> Setting `ParquetTimestampInMillisecond` has no effect on the string format of the timestamp column value that is inserted by setting the `TimestampColumnName` parameter.", + "ParquetVersion": "The version of the Apache Parquet format that you want to use: `parquet_1_0` (the default) or `parquet_2_0` .", + "PreserveTransactions": "If set to `true` , AWS DMS saves the transaction order for a change data capture (CDC) load on the Amazon S3 target specified by [`CdcPath`](https://docs.aws.amazon.com/dms/latest/APIReference/API_S3Settings.html#DMS-Type-S3Settings-CdcPath) . For more information, see [Capturing data changes (CDC) including transaction order on the S3 target](https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Target.S3.html#CHAP_Target.S3.EndpointSettings.CdcPath) .\n\n> This setting is supported in AWS DMS versions 3.4.2 and later.", + "Rfc4180": "For an S3 source, when this value is set to `true` or `y` , each leading double quotation mark has to be followed by an ending double quotation mark. This formatting complies with RFC 4180. When this value is set to `false` or `n` , string literals are copied to the target as is. In this case, a delimiter (row or column) signals the end of the field. Thus, you can't use a delimiter as part of the string, because it signals the end of the value.\n\nFor an S3 target, an optional parameter used to set behavior to comply with RFC 4180 for data migrated to Amazon S3 using .csv file format only. When this value is set to `true` or `y` using Amazon S3 as a target, if the data has quotation marks or newline characters in it, AWS DMS encloses the entire column with an additional pair of double quotation marks (\"). Every quotation mark within the data is repeated twice.\n\nThe default value is `true` . Valid values include `true` , `false` , `y` , and `n` .", + "RowGroupLength": "The number of rows in a row group. A smaller row group size provides faster reads. But as the number of row groups grows, the slower writes become. This parameter defaults to 10,000 rows. This number is used for .parquet file format only.\n\nIf you choose a value larger than the maximum, `RowGroupLength` is set to the max row group length in bytes (64 * 1024 * 1024).", + "ServerSideEncryptionKmsKeyId": "If you are using `SSE_KMS` for the `EncryptionMode` , provide the AWS KMS key ID. The key that you use needs an attached policy that enables AWS Identity and Access Management (IAM) user permissions and allows use of the key.\n\nHere is a CLI example: `aws dms create-endpoint --endpoint-identifier *value* --endpoint-type target --engine-name s3 --s3-settings ServiceAccessRoleArn= *value* ,BucketFolder= *value* ,BucketName= *value* ,EncryptionMode=SSE_KMS,ServerSideEncryptionKmsKeyId= *value*`", + "ServiceAccessRoleArn": "Not currently supported by AWS CloudFormation .", + "TimestampColumnName": "A value that when nonblank causes AWS DMS to add a column with timestamp information to the endpoint data for an Amazon S3 target.\n\n> AWS DMS supports the `TimestampColumnName` parameter in versions 3.1.4 and later. \n\nDMS includes an additional `STRING` column in the .csv or .parquet object files of your migrated data when you set `TimestampColumnName` to a nonblank value.\n\nFor a full load, each row of this timestamp column contains a timestamp for when the data was transferred from the source to the target by DMS.\n\nFor a change data capture (CDC) load, each row of the timestamp column contains the timestamp for the commit of that row in the source database.\n\nThe string format for this timestamp column value is `yyyy-MM-dd HH:mm:ss.SSSSSS` . By default, the precision of this value is in microseconds. For a CDC load, the rounding of the precision depends on the commit timestamp supported by DMS for the source database.\n\nWhen the `AddColumnName` parameter is set to `true` , DMS also includes a name for the timestamp column that you set with `TimestampColumnName` .", + "UseCsvNoSupValue": "This setting applies if the S3 output files during a change data capture (CDC) load are written in .csv format. If set to `true` for columns not included in the supplemental log, AWS DMS uses the value specified by [`CsvNoSupValue`](https://docs.aws.amazon.com/dms/latest/APIReference/API_S3Settings.html#DMS-Type-S3Settings-CsvNoSupValue) . If not set or set to `false` , AWS DMS uses the null value for these columns.\n\n> This setting is supported in AWS DMS versions 3.4.1 and later.", + "UseTaskStartTimeForFullLoadTimestamp": "When set to true, this parameter uses the task start time as the timestamp column value instead of the time data is written to target. For full load, when `useTaskStartTimeForFullLoadTimestamp` is set to `true` , each row of the timestamp column contains the task start time. For CDC loads, each row of the timestamp column contains the transaction commit time.\n\nWhen `useTaskStartTimeForFullLoadTimestamp` is set to `false` , the full load timestamp in the timestamp column increments with the time data arrives at the target." } }, "AWS::DMS::Endpoint.SybaseSettings": { @@ -10085,9 +10334,9 @@ }, "AWS::DataSync::LocationS3.S3Config": { "attributes": {}, - "description": "The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that is used to access an Amazon S3 bucket.\n\nFor detailed information about using such a role, see [Creating a Location for Amazon S3](https://docs.aws.amazon.com/datasync/latest/userguide/working-with-locations.html#create-s3-location) in the *AWS DataSync User Guide* .", + "description": "The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role used to access an Amazon S3 bucket.\n\nFor detailed information about using such a role, see [Creating a Location for Amazon S3](https://docs.aws.amazon.com/datasync/latest/userguide/working-with-locations.html#create-s3-location) in the *AWS DataSync User Guide* .", "properties": { - "BucketAccessRoleArn": "The Amazon S3 bucket to access. This bucket is used as a parameter in the [CreateLocationS3](https://docs.aws.amazon.com/datasync/latest/userguide/API_CreateLocationS3.html) operation." + "BucketAccessRoleArn": "The ARN of the IAM role for accessing the S3 bucket." } }, "AWS::DataSync::LocationSMB": { @@ -10414,6 +10663,7 @@ "properties": { "AvailabilityZones": "A list of Amazon EC2 Availability Zones that instances in the cluster can be created in.", "BackupRetentionPeriod": "The number of days for which automated backups are retained. You must specify a minimum value of 1.\n\nDefault: 1\n\nConstraints:\n\n- Must be a value from 1 to 35.", + "CopyTagsToSnapshot": "", "DBClusterIdentifier": "The cluster identifier. This parameter is stored as a lowercase string.\n\nConstraints:\n\n- Must contain from 1 to 63 letters, numbers, or hyphens.\n- The first character must be a letter.\n- Cannot end with a hyphen or contain two consecutive hyphens.\n\nExample: `my-cluster`", "DBClusterParameterGroupName": "The name of the cluster parameter group to associate with this cluster.", "DBSubnetGroupName": "A subnet group to associate with this cluster.\n\nConstraints: Must match the name of an existing `DBSubnetGroup` . Must not be default.\n\nExample: `mySubnetgroup`", @@ -10481,7 +10731,7 @@ "StreamArn": "The ARN of the DynamoDB stream, such as `arn:aws:dynamodb:us-east-1:123456789012:table/testddbstack-myDynamoDBTable-012A1SL7SMP5Q/stream/2015-11-30T20:10:00.000` . The `StreamArn` returned is that of the replica in the region the stack is deployed to.\n\n> You must specify the `StreamSpecification` property to use this attribute.", "TableId": "Unique identifier for the table, such as `a123b456-01ab-23cd-123a-111222aaabbb` . The `TableId` returned is that of the replica in the region the stack is deployed to." }, - "description": "The `AWS::DynamoDB::GlobalTable` resource enables you to create and manage a Version 2019.11.21 global table. This resource cannot be used to create or manage a Version 2017.11.29 global table.\n\n> You cannot convert a resource of type `AWS::DynamoDB::Table` into a resource of type `AWS::DynamoDB::GlobalTable` by changing its type in your template. *Doing so might result in the deletion of your DynamoDB table.* \n\nYou should be aware of the following behaviors when working with DynamoDB global tables.\n\n- The IAM Principal executing the stack operation must have the permissions listed below in all regions where you plan to have a global table replica. The IAM Principal's permissions should not have restrictions based on IP source address. Some global tables operations (for example, adding a replica) are asynchronous, and require that the IAM Principal is valid until they complete. You should not delete the Principal (user or IAM role) until CloudFormation has finished updating your stack.\n\n- `dynamodb:CreateTable`\n- `dynamodb:UpdateTable`\n- `dynamodb:DeleteTable`\n- `dynamodb:DescribeContinuousBackups`\n- `dynamodb:DescribeContributorInsights`\n- `dynamodb:DescribeTable`\n- `dynamodb:DescribeTableReplicaAutoScaling`\n- `dynamodb:DescribeTimeToLive`\n- `dynamodb:ListTables`\n- `dynamodb:UpdateTimeToLive`\n- `dynamodb:UpdateContributorInsights`\n- `dynamodb:UpdateContinuousBackups`\n- `dynamodb:ListTagsOfResource`\n- `dynamodb:TagResource`\n- `dynamodb:UntagResource`\n- `dynamodb:BatchWriteItem`\n- `dynamodb:CreateTableReplica`\n- `dynamodb:DeleteItem`\n- `dynamodb:DeleteTableReplica`\n- `dynamodb:DisableKinesisStreamingDestination`\n- `dynamodb:EnableKinesisStreamingDestination`\n- `dynamodb:GetItem`\n- `dynamodb:PutItem`\n- `dynamodb:Query`\n- `dynamodb:Scan`\n- `dynamodb:UpdateItem`\n- `dynamodb:DescribeTableReplicaAutoScaling`\n- `dynamodb:UpdateTableReplicaAutoScaling`\n- `iam:CreateServiceLinkedRole`\n- `kms:CreateGrant`\n- `kms:DescribeKey`\n- `application-autoscaling:DeleteScalingPolicy`\n- `application-autoscaling:DeleteScheduledAction`\n- `application-autoscaling:DeregisterScalableTarget`\n- `application-autoscaling:DescribeScalingPolicies`\n- `application-autoscaling:DescribeScalableTargets`\n- `application-autoscaling:PutScalingPolicy`\n- `application-autoscaling:PutScheduledAction`\n- `application-autoscaling:RegisterScalableTarget`\n- When using provisioned billing mode, CloudFormation will create an auto scaling policy on each of your replicas to control their write capacities. You must configure this policy using the `WriteProvisionedThroughputSettings` property. CloudFormation will ensure that all replicas have the same write capacity auto scaling property. You cannot directly specify a value for write capacity for a global table.\n- If your table uses provisioned capacity, you must configure auto scaling directly in the `AWS::DynamoDB::GlobalTable` resource. You should not configure additional auto scaling policies on any of the table replicas or global secondary indexes, either via API or via `AWS::ApplicationAutoScaling::ScalableTarget` or `AWS::ApplicationAutoScaling::ScalingPolicy` . Doing so might result in unexpected behavior and is unsupported.\n- In AWS CloudFormation , each global table is controlled by a single stack, in a single region, regardless of the number of replicas. When you deploy your template, CloudFormation will create/update all replicas as part of a single stack operation. You should not deploy the same `AWS::DynamoDB::GlobalTable` resource in multiple regions. Doing so will result in errors, and is unsupported. If you deploy your application template in multiple regions, you can use conditions to only create the resource in a single region. Alternatively, you can choose to define your `AWS::DynamoDB::GlobalTable` resources in a stack separate from your application stack, and make sure it is only deployed to a single region.", + "description": "The `AWS::DynamoDB::GlobalTable` resource enables you to create and manage a Version 2019.11.21 global table. This resource cannot be used to create or manage a Version 2017.11.29 global table.\n\n> You cannot convert a resource of type `AWS::DynamoDB::Table` into a resource of type `AWS::DynamoDB::GlobalTable` by changing its type in your template. *Doing so might result in the deletion of your DynamoDB table.*\n> \n> You can instead use the GlobalTable resource to create a new table in a single Region. This will be billed the same as a single Region table. If you later update the stack to add other Regions then Global Tables pricing will apply. \n\nYou should be aware of the following behaviors when working with DynamoDB global tables.\n\n- The IAM Principal executing the stack operation must have the permissions listed below in all regions where you plan to have a global table replica. The IAM Principal's permissions should not have restrictions based on IP source address. Some global tables operations (for example, adding a replica) are asynchronous, and require that the IAM Principal is valid until they complete. You should not delete the Principal (user or IAM role) until CloudFormation has finished updating your stack.\n\n- `dynamodb:CreateTable`\n- `dynamodb:UpdateTable`\n- `dynamodb:DeleteTable`\n- `dynamodb:DescribeContinuousBackups`\n- `dynamodb:DescribeContributorInsights`\n- `dynamodb:DescribeTable`\n- `dynamodb:DescribeTableReplicaAutoScaling`\n- `dynamodb:DescribeTimeToLive`\n- `dynamodb:ListTables`\n- `dynamodb:UpdateTimeToLive`\n- `dynamodb:UpdateContributorInsights`\n- `dynamodb:UpdateContinuousBackups`\n- `dynamodb:ListTagsOfResource`\n- `dynamodb:TagResource`\n- `dynamodb:UntagResource`\n- `dynamodb:BatchWriteItem`\n- `dynamodb:CreateTableReplica`\n- `dynamodb:DeleteItem`\n- `dynamodb:DeleteTableReplica`\n- `dynamodb:DisableKinesisStreamingDestination`\n- `dynamodb:EnableKinesisStreamingDestination`\n- `dynamodb:GetItem`\n- `dynamodb:PutItem`\n- `dynamodb:Query`\n- `dynamodb:Scan`\n- `dynamodb:UpdateItem`\n- `dynamodb:DescribeTableReplicaAutoScaling`\n- `dynamodb:UpdateTableReplicaAutoScaling`\n- `iam:CreateServiceLinkedRole`\n- `kms:CreateGrant`\n- `kms:DescribeKey`\n- `application-autoscaling:DeleteScalingPolicy`\n- `application-autoscaling:DeleteScheduledAction`\n- `application-autoscaling:DeregisterScalableTarget`\n- `application-autoscaling:DescribeScalingPolicies`\n- `application-autoscaling:DescribeScalableTargets`\n- `application-autoscaling:PutScalingPolicy`\n- `application-autoscaling:PutScheduledAction`\n- `application-autoscaling:RegisterScalableTarget`\n- When using provisioned billing mode, CloudFormation will create an auto scaling policy on each of your replicas to control their write capacities. You must configure this policy using the `WriteProvisionedThroughputSettings` property. CloudFormation will ensure that all replicas have the same write capacity auto scaling property. You cannot directly specify a value for write capacity for a global table.\n- If your table uses provisioned capacity, you must configure auto scaling directly in the `AWS::DynamoDB::GlobalTable` resource. You should not configure additional auto scaling policies on any of the table replicas or global secondary indexes, either via API or via `AWS::ApplicationAutoScaling::ScalableTarget` or `AWS::ApplicationAutoScaling::ScalingPolicy` . Doing so might result in unexpected behavior and is unsupported.\n- In AWS CloudFormation , each global table is controlled by a single stack, in a single region, regardless of the number of replicas. When you deploy your template, CloudFormation will create/update all replicas as part of a single stack operation. You should not deploy the same `AWS::DynamoDB::GlobalTable` resource in multiple regions. Doing so will result in errors, and is unsupported. If you deploy your application template in multiple regions, you can use conditions to only create the resource in a single region. Alternatively, you can choose to define your `AWS::DynamoDB::GlobalTable` resources in a stack separate from your application stack, and make sure it is only deployed to a single region.", "properties": { "AttributeDefinitions": "A list of attributes that describe the key schema for the global table and indexes.", "BillingMode": "Specifies how you are charged for read and write throughput and how you manage capacity. Valid values are:\n\n- `PAY_PER_REQUEST`\n- `PROVISIONED`\n\nAll replicas in your global table will have the same billing mode. If you use `PROVISIONED` billing mode, you must provide an auto scaling configuration via the `WriteProvisionedThroughputSettings` property. The default value of this property is `PROVISIONED` .", @@ -10492,7 +10742,7 @@ "SSESpecification": "Specifies the settings to enable server-side encryption. These settings will be applied to all replicas. If you plan to use customer-managed KMS keys, you must provide a key for each replica using the `ReplicaSpecification.ReplicaSSESpecification` property.", "StreamSpecification": "Specifies the streams settings on your global table. You must provide a value for this property if your global table contains more than one replica. You can only change the streams settings if your global table has only one replica.", "TableName": "A name for the global table. If you don't specify a name, AWS CloudFormation generates a unique ID and uses that ID as the table name. For more information, see [Name type](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-name.html) .\n\n> If you specify a name, you cannot perform updates that require replacement of this resource. You can perform updates that require no or some interruption. If you must replace the resource, specify a new name.", - "TimeToLiveSpecification": "Specifies the Time to Live (TTL) settings for the table. This setting will be applied to all replicas.\n\n> For detailed information about the TTL feature of DynamoDB, see [Expiring Items with Time to Live](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html) in the Amazon DynamoDB Developer Guide.", + "TimeToLiveSpecification": "Specifies the time to live (TTL) settings for the table. This setting will be applied to all replicas.", "WriteProvisionedThroughputSettings": "Specifies an auto scaling policy for write capacity. This policy will be applied to all replicas. This setting must be specified if `BillingMode` is set to `PROVISIONED` ." } }, @@ -10597,6 +10847,7 @@ "ReadProvisionedThroughputSettings": "Defines read capacity settings for the replica table.", "Region": "The region in which this replica exists.", "SSESpecification": "Allows you to specify a customer-managed key for the replica. When using customer-managed keys for server-side encryption, this property must have a value in all replicas.", + "TableClass": "", "Tags": "An array of key-value pairs to apply to this replica.\n\nFor more information, see [Tag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) ." } }, @@ -10662,7 +10913,7 @@ "TableClass": "The table class of the new table. Valid values are `STANDARD` and `STANDARD_INFREQUENT_ACCESS` .", "TableName": "A name for the table. If you don't specify a name, AWS CloudFormation generates a unique physical ID and uses that ID for the table name. For more information, see [Name Type](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-name.html) .\n\n> If you specify a name, you cannot perform updates that require replacement of this resource. You can perform updates that require no or some interruption. If you must replace the resource, specify a new name.", "Tags": "An array of key-value pairs to apply to this resource.\n\nFor more information, see [Tag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) .", - "TimeToLiveSpecification": "Specifies the Time to Live (TTL) settings for the table.\n\n> For detailed information about the TTL feature of DynamoDB, see [Expiring Items with Time to Live](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html) in the Amazon DynamoDB Developer Guide." + "TimeToLiveSpecification": "Specifies the Time to Live (TTL) settings for the table.\n\n> For detailed information about the limits in DynamoDB, see [Limits in Amazon DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html) in the Amazon DynamoDB Developer Guide." } }, "AWS::DynamoDB::Table.AttributeDefinition": { @@ -10732,7 +10983,7 @@ }, "AWS::DynamoDB::Table.ProvisionedThroughput": { "attributes": {}, - "description": "Throughput for the specified table, which consists of values for `ReadCapacityUnits` and `WriteCapacityUnits` .", + "description": "Throughput for the specified table, which consists of values for `ReadCapacityUnits` and `WriteCapacityUnits` . For more information about the contents of a provisioned throughput structure, see [Amazon DynamoDB Table ProvisionedThroughput](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-provisionedthroughput.html) .", "properties": { "ReadCapacityUnits": "The maximum number of strongly consistent reads consumed per second before DynamoDB returns a `ThrottlingException` . For more information, see [Specifying Read and Write Requirements](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithTables.html#ProvisionedThroughput) in the *Amazon DynamoDB Developer Guide* .\n\nIf read/write capacity mode is `PAY_PER_REQUEST` the value is set to 0.", "WriteCapacityUnits": "The maximum number of writes consumed per second before DynamoDB returns a `ThrottlingException` . For more information, see [Specifying Read and Write Requirements](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithTables.html#ProvisionedThroughput) in the *Amazon DynamoDB Developer Guide* .\n\nIf read/write capacity mode is `PAY_PER_REQUEST` the value is set to 0." @@ -10782,8 +11033,8 @@ "InstanceMatchCriteria": "Indicates the type of instance launches that the Capacity Reservation accepts. The options include:\n\n- `open` - The Capacity Reservation automatically matches all instances that have matching attributes (instance type, platform, and Availability Zone). Instances that have matching attributes run in the Capacity Reservation automatically without specifying any additional parameters.\n- `targeted` - The Capacity Reservation only accepts instances that have matching attributes (instance type, platform, and Availability Zone), and explicitly target the Capacity Reservation. This ensures that only permitted instances can use the reserved capacity.\n\nDefault: `open`", "InstancePlatform": "The type of operating system for which to reserve capacity.", "InstanceType": "The instance type for which to reserve capacity. For more information, see [Instance types](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html) in the *Amazon EC2 User Guide* .", - "OutPostArn": "The Amazon Resource Name (ARN) of the Outpost on which the Capacity Reservation was created.", - "PlacementGroupArn": "", + "OutPostArn": "The Amazon Resource Name (ARN) of the Outpost on which to create the Capacity Reservation.", + "PlacementGroupArn": "The Amazon Resource Name (ARN) of the cluster placement group in which to create the Capacity Reservation. For more information, see [Capacity Reservations for cluster placement groups](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/cr-cpg.html) in the *Amazon EC2 User Guide* .", "TagSpecifications": "The tags to apply to the Capacity Reservation during launch.", "Tenancy": "Indicates the tenancy of the Capacity Reservation. A Capacity Reservation can have one of the following tenancy settings:\n\n- `default` - The Capacity Reservation is created on hardware that is shared with other AWS accounts .\n- `dedicated` - The Capacity Reservation is created on single-tenant hardware that is dedicated to a single AWS account ." } @@ -10868,12 +11119,14 @@ "AuthenticationOptions": "Information about the authentication method to be used to authenticate clients.", "ClientCidrBlock": "The IPv4 address range, in CIDR notation, from which to assign client IP addresses. The address range cannot overlap with the local CIDR of the VPC in which the associated subnet is located, or the routes that you add manually. The address range cannot be changed after the Client VPN endpoint has been created. The CIDR block should be /22 or greater.", "ClientConnectOptions": "The options for managing connection authorization for new client connections.", + "ClientLoginBannerOptions": "Options for enabling a customizable text banner that will be displayed on AWS provided clients when a VPN session is established.", "ConnectionLogOptions": "Information about the client connection logging options.\n\nIf you enable client connection logging, data about client connections is sent to a Cloudwatch Logs log stream. The following information is logged:\n\n- Client connection requests\n- Client connection results (successful and unsuccessful)\n- Reasons for unsuccessful client connection requests\n- Client connection termination time", "Description": "A brief description of the Client VPN endpoint.", "DnsServers": "Information about the DNS servers to be used for DNS resolution. A Client VPN endpoint can have up to two DNS servers. If no DNS server is specified, the DNS address configured on the device is used for the DNS server.", "SecurityGroupIds": "The IDs of one or more security groups to apply to the target network. You must also specify the ID of the VPC that contains the security groups.", "SelfServicePortal": "Specify whether to enable the self-service portal for the Client VPN endpoint.\n\nDefault Value: `enabled`", "ServerCertificateArn": "The ARN of the server certificate. For more information, see the [AWS Certificate Manager User Guide](https://docs.aws.amazon.com/acm/latest/userguide/) .", + "SessionTimeoutHours": "The maximum VPN session duration time in hours.\n\nValid values: `8 | 10 | 12 | 24`\n\nDefault value: `24`", "SplitTunnel": "Indicates whether split-tunnel is enabled on the AWS Client VPN endpoint.\n\nBy default, split-tunnel on a VPN endpoint is disabled.\n\nFor information about split-tunnel VPN endpoints, see [Split-tunnel AWS Client VPN endpoint](https://docs.aws.amazon.com/vpn/latest/clientvpn-admin/split-tunnel-vpn.html) in the *AWS Client VPN Administrator Guide* .", "TagSpecifications": "The tags to apply to the Client VPN endpoint during creation.", "TransportProtocol": "The transport protocol to be used by the VPN session.\n\nDefault value: `udp`", @@ -10906,6 +11159,14 @@ "LambdaFunctionArn": "The Amazon Resource Name (ARN) of the AWS Lambda function used for connection authorization." } }, + "AWS::EC2::ClientVpnEndpoint.ClientLoginBannerOptions": { + "attributes": {}, + "description": "Options for enabling a customizable text banner that will be displayed on AWS provided clients when a VPN session is established.", + "properties": { + "BannerText": "Customizable text that will be displayed in a banner on AWS provided clients when a VPN session is established. UTF-8 encoded characters only. Maximum of 1400 characters.", + "Enabled": "Enable or disable a customizable text banner that will be displayed on AWS provided clients when a VPN session is established.\n\nValid values: `true | false`\n\nDefault value: `false`" + } + }, "AWS::EC2::ClientVpnEndpoint.ConnectionLogOptions": { "attributes": {}, "description": "Describes the client connection logging options for the Client VPN endpoint.", @@ -10978,7 +11239,7 @@ "description": "Specifies a set of DHCP options for your VPC.\n\nYou must specify at least one of the following properties: `DomainNameServers` , `NetbiosNameServers` , `NtpServers` . If you specify `NetbiosNameServers` , you must specify `NetbiosNodeType` .", "properties": { "DomainName": "This value is used to complete unqualified DNS hostnames. If you're using AmazonProvidedDNS in `us-east-1` , specify `ec2.internal` . If you're using AmazonProvidedDNS in another Region, specify *region* . `compute.internal` (for example, `ap-northeast-1.compute.internal` ). Otherwise, specify a domain name (for example, *MyCompany.com* ).", - "DomainNameServers": "The IPv4 addresses of up to four domain name servers, or AmazonProvidedDNS. The default DHCP option set specifies `AmazonProvidedDNS` . If specifying more than one domain name server, specify the IP addresses in a single parameter, separated by commas. To have your instance to receive a custom DNS hostname as specified in `DomainName` , you must set this to a custom DNS server.", + "DomainNameServers": "The IPv4 addresses of up to four domain name servers, or `AmazonProvidedDNS` . The default is `AmazonProvidedDNS` . To have your instance receive a custom DNS hostname as specified in `DomainName` , you must set this property to a custom DNS server.", "NetbiosNameServers": "The IPv4 addresses of up to four NetBIOS name servers.", "NetbiosNodeType": "The NetBIOS node type (1, 2, 4, or 8). We recommend that you specify 2 (broadcast and multicast are not currently supported).", "NtpServers": "The IPv4 addresses of up to four Network Time Protocol (NTP) servers.", @@ -11001,7 +11262,7 @@ "TagSpecifications": "The key-value pair for tagging the EC2 Fleet request on creation. For more information, see [Tagging your resources](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#tag-resources) .\n\nIf the fleet type is `instant` , specify a resource type of `fleet` to tag the fleet or `instance` to tag the instances at launch.\n\nIf the fleet type is `maintain` or `request` , specify a resource type of `fleet` to tag the fleet. You cannot specify a resource type of `instance` . To tag instances at launch, specify the tags in a [launch template](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-launch-templates.html#create-launch-template) .", "TargetCapacitySpecification": "The number of units to request.", "TerminateInstancesWithExpiration": "Indicates whether running instances should be terminated when the EC2 Fleet expires.", - "Type": "The fleet type. The default value is `maintain` .\n\n- `maintain` - The EC2 Fleet places an asynchronous request for your desired capacity, and continues to maintain your desired Spot capacity by replenishing interrupted Spot Instances.\n- `request` - The EC2 Fleet places an asynchronous one-time request for your desired capacity, but does submit Spot requests in alternative capacity pools if Spot capacity is unavailable, and does not maintain Spot capacity if Spot Instances are interrupted.\n- `instant` - The EC2 Fleet places a synchronous one-time request for your desired capacity, and returns errors for any instances that could not be launched.\n\nFor more information, see [EC2 Fleet request types](https://docs.aws.amazon.com/https://docs.aws.amazon.com/ec2-fleet-request-type.html) in the *Amazon EC2 User Guide* .", + "Type": "The fleet type. The default value is `maintain` .\n\n- `maintain` - The EC2 Fleet places an asynchronous request for your desired capacity, and continues to maintain your desired Spot capacity by replenishing interrupted Spot Instances.\n- `request` - The EC2 Fleet places an asynchronous one-time request for your desired capacity, but does submit Spot requests in alternative capacity pools if Spot capacity is unavailable, and does not maintain Spot capacity if Spot Instances are interrupted.\n- `instant` - The EC2 Fleet places a synchronous one-time request for your desired capacity, and returns errors for any instances that could not be launched.\n\nFor more information, see [EC2 Fleet request types](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-fleet-request-type.html) in the *Amazon EC2 User Guide* .", "ValidFrom": "The start date and time of the request, in UTC format (for example, *YYYY* - *MM* - *DD* T *HH* : *MM* : *SS* Z). The default is to start fulfilling the request immediately.", "ValidUntil": "The end date and time of the request, in UTC format (for example, *YYYY* - *MM* - *DD* T *HH* : *MM* : *SS* Z). At this point, no new EC2 Fleet requests are placed or able to fulfill the request. If no value is specified, the request remains until you cancel it." } @@ -11096,9 +11357,9 @@ "MemoryGiBPerVCpu": "The minimum and maximum amount of memory per vCPU, in GiB.\n\nDefault: No minimum or maximum limits", "MemoryMiB": "The minimum and maximum amount of memory, in MiB.", "NetworkInterfaceCount": "The minimum and maximum number of network interfaces.\n\nDefault: No minimum or maximum limits", - "OnDemandMaxPricePercentageOverLowestPrice": "The price protection threshold for On-Demand Instances. This is the maximum you\u2019ll pay for an On-Demand Instance, expressed as a percentage above the cheapest M, C, or R instance type with your specified attributes. When Amazon EC2 selects instance types with your attributes, it excludes instance types priced above your threshold.\n\nThe parameter accepts an integer, which Amazon EC2 interprets as a percentage.\n\nTo turn off price protection, specify a high value, such as `999999` .\n\nThis parameter is not supported for [GetSpotPlacementScores](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetSpotPlacementScores.html) and [GetInstanceTypesFromInstanceRequirements](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetInstanceTypesFromInstanceRequirements.html) .\n\nDefault: `20`", + "OnDemandMaxPricePercentageOverLowestPrice": "The price protection threshold for On-Demand Instances. This is the maximum you\u2019ll pay for an On-Demand Instance, expressed as a percentage above the cheapest M, C, or R instance type with your specified attributes. When Amazon EC2 selects instance types with your attributes, it excludes instance types priced above your threshold.\n\nThe parameter accepts an integer, which Amazon EC2 interprets as a percentage.\n\nTo turn off price protection, specify a high value, such as `999999` .\n\nThis parameter is not supported for [GetSpotPlacementScores](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetSpotPlacementScores.html) and [GetInstanceTypesFromInstanceRequirements](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetInstanceTypesFromInstanceRequirements.html) .\n\n> If you set `TargetCapacityUnitType` to `vcpu` or `memory-mib` , the price protection threshold is applied based on the per-vCPU or per-memory price instead of the per-instance price. \n\nDefault: `20`", "RequireHibernateSupport": "Indicates whether instance types must support hibernation for On-Demand Instances.\n\nThis parameter is not supported for [GetSpotPlacementScores](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetSpotPlacementScores.html) .\n\nDefault: `false`", - "SpotMaxPricePercentageOverLowestPrice": "The price protection threshold for Spot Instance. This is the maximum you\u2019ll pay for an Spot Instance, expressed as a percentage above the cheapest M, C, or R instance type with your specified attributes. When Amazon EC2 selects instance types with your attributes, it excludes instance types priced above your threshold.\n\nThe parameter accepts an integer, which Amazon EC2 interprets as a percentage.\n\nTo turn off price protection, specify a high value, such as `999999` .\n\nThis parameter is not supported for [GetSpotPlacementScores](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetSpotPlacementScores.html) and [GetInstanceTypesFromInstanceRequirements](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetInstanceTypesFromInstanceRequirements.html) .\n\nDefault: `100`", + "SpotMaxPricePercentageOverLowestPrice": "The price protection threshold for Spot Instance. This is the maximum you\u2019ll pay for an Spot Instance, expressed as a percentage above the cheapest M, C, or R instance type with your specified attributes. When Amazon EC2 selects instance types with your attributes, it excludes instance types priced above your threshold.\n\nThe parameter accepts an integer, which Amazon EC2 interprets as a percentage.\n\nTo turn off price protection, specify a high value, such as `999999` .\n\nThis parameter is not supported for [GetSpotPlacementScores](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetSpotPlacementScores.html) and [GetInstanceTypesFromInstanceRequirements](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetInstanceTypesFromInstanceRequirements.html) .\n\n> If you set `TargetCapacityUnitType` to `vcpu` or `memory-mib` , the price protection threshold is applied based on the per-vCPU or per-memory price instead of the per-instance price. \n\nDefault: `100`", "TotalLocalStorageGB": "The minimum and maximum amount of total local storage, in GB.\n\nDefault: No minimum or maximum limits", "VCpuCount": "The minimum and maximum number of vCPUs." } @@ -11122,8 +11383,8 @@ "attributes": {}, "description": "The minimum and maximum amount of memory, in MiB.", "properties": { - "Max": "", - "Min": "" + "Max": "The maximum amount of memory, in MiB. To specify no maximum limit, omit this parameter.", + "Min": "The minimum amount of memory, in MiB. To specify no minimum limit, specify `0` ." } }, "AWS::EC2::EC2Fleet.NetworkInterfaceCountRequest": { @@ -11266,6 +11527,7 @@ "description": "Specifies a VPC flow log that captures IP traffic for a specified network interface, subnet, or VPC. To view the log data, use Amazon CloudWatch Logs (CloudWatch Logs) to help troubleshoot connection issues. For example, you can use a flow log to investigate why certain traffic isn't reaching an instance, which can help you diagnose overly restrictive security group rules. For more information, see [VPC Flow Logs](https://docs.aws.amazon.com/vpc/latest/userguide/flow-logs.html) in the *Amazon VPC User Guide* .", "properties": { "DeliverLogsPermissionArn": "The ARN for the IAM role that permits Amazon EC2 to publish flow logs to a CloudWatch Logs log group in your account.\n\nIf you specify `LogDestinationType` as `s3` , do not specify `DeliverLogsPermissionArn` or `LogGroupName` .", + "DestinationOptions": "The destination options. The following options are supported:\n\n- `FileFormat` - The format for the flow log ( `plain-text` | `parquet` ). The default is `plain-text` .\n- `HiveCompatiblePartitions` - Indicates whether to use Hive-compatible prefixes for flow logs stored in Amazon S3 ( `true` | `false` ). The default is `false` .\n- `PerHourPartition` - Indicates whether to partition the flow log per hour ( `true` | `false` ). The default is `false` .", "LogDestination": "The destination to which the flow log data is to be published. Flow log data can be published to a CloudWatch Logs log group or an Amazon S3 bucket. The value specified for this parameter depends on the value specified for `LogDestinationType` .\n\nIf `LogDestinationType` is not specified or `cloud-watch-logs` , specify the Amazon Resource Name (ARN) of the CloudWatch Logs log group. For example, to publish to a log group called `my-logs` , specify `arn:aws:logs:us-east-1:123456789012:log-group:my-logs` . Alternatively, use `LogGroupName` instead.\n\nIf LogDestinationType is `s3` , specify the ARN of the Amazon S3 bucket. You can also specify a subfolder in the bucket. To specify a subfolder in the bucket, use the following ARN format: `bucket_ARN/subfolder_name/` . For example, to specify a subfolder named `my-logs` in a bucket named `my-bucket` , use the following ARN: `arn:aws:s3:::my-bucket/my-logs/` . You cannot use `AWSLogs` as a subfolder name. This is a reserved term.", "LogDestinationType": "The type of destination to which the flow log data is to be published. Flow log data can be published to CloudWatch Logs or Amazon S3. To publish flow log data to CloudWatch Logs, specify `cloud-watch-logs` . To publish flow log data to Amazon S3, specify `s3` .\n\nIf you specify `LogDestinationType` as `s3` , do not specify `DeliverLogsPermissionArn` or `LogGroupName` .\n\nDefault: `cloud-watch-logs`", "LogFormat": "The fields to include in the flow log record, in the order in which they should appear. For a list of available fields, see [Flow Log Records](https://docs.aws.amazon.com/vpc/latest/userguide/flow-logs.html#flow-log-records) . If you omit this parameter, the flow log is created using the default format. If you specify this parameter, you must specify at least one field.\n\nSpecify the fields using the `${field-id}` format, separated by spaces.", @@ -11273,7 +11535,7 @@ "MaxAggregationInterval": "The maximum interval of time during which a flow of packets is captured and aggregated into a flow log record. You can specify 60 seconds (1 minute) or 600 seconds (10 minutes).\n\nWhen a network interface is attached to a [Nitro-based instance](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html#ec2-nitro-instances) , the aggregation interval is always 60 seconds or less, regardless of the value that you specify.\n\nDefault: 600", "ResourceId": "The ID of the subnet, network interface, or VPC for which you want to create a flow log.", "ResourceType": "The type of resource for which to create the flow log. For example, if you specified a VPC ID for the `ResourceId` property, specify `VPC` for this property.", - "Tags": "The tags for the flow log.", + "Tags": "The tags to apply to the flow logs.", "TrafficType": "The type of traffic to log. You can log traffic that the resource accepts or rejects, or all traffic." } }, @@ -11290,6 +11552,7 @@ }, "AWS::EC2::Host": { "attributes": { + "HostId": "The ID of the host.", "Ref": "`Ref` returns the host ID, such as `h-0ab123c45d67ef89` ." }, "description": "Allocates a fully dedicated physical server for launching EC2 instances. Because the host is fully dedicated for your use, it can help you address compliance requirements and reduce costs by allowing you to use your existing server-bound software licenses. For more information, see [Dedicated Hosts](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/dedicated-hosts-overview.html) in the *Amazon EC2 User Guide for Linux Instances* .", @@ -11377,6 +11640,7 @@ "Arn": "The ARN of the scope.", "IpamArn": "The ARN of an IPAM.", "IpamScopeId": "The ID of an IPAM scope.", + "IpamScopeType": "The type of the scope.", "IsDefault": "Defines if the scope is the default scope or not.", "PoolCount": "The number of pools in a scope.", "Ref": "`Ref` returns the IPAM scope ID." @@ -11385,13 +11649,13 @@ "properties": { "Description": "The description of the scope.", "IpamId": "The ID of the IPAM for which you're creating this scope.", - "IpamScopeType": "The type of the scope.", "Tags": "The key/value combination of a tag assigned to the resource. Use the tag key in the filter name and the tag value as the filter value. For example, to find all resources that have a tag with the key `Owner` and the value `TeamA` , specify `tag:Owner` for the filter name and `TeamA` for the filter value." } }, "AWS::EC2::Instance": { "attributes": { "AvailabilityZone": "The Availability Zone where the specified instance is launched. For example: `us-east-1b` .\n\nYou can retrieve a list of all Availability Zones for a Region by using the [Fn::GetAZs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getavailabilityzones.html) intrinsic function.", + "PrivateDnsName": "The private DNS name of the specified instance. For example: `ip-10-24-34-0.ec2.internal` .", "PrivateIp": "The private IP address of the specified instance. For example: `10.24.34.0` .", "PublicDnsName": "The public DNS name of the specified instance. For example: `ec2-107-20-50-45.compute-1.amazonaws.com` .", "PublicIp": "The public IP address of the specified instance. For example: `192.0.2.0` .", @@ -11426,8 +11690,9 @@ "Monitoring": "Specifies whether detailed monitoring is enabled for the instance.", "NetworkInterfaces": "The network interfaces to associate with the instance.\n\n> If you use this property to point to a network interface, you must terminate the original interface before attaching a new one to allow the update of the instance to succeed.\n> \n> If this resource has a public IP address and is also in a VPC that is defined in the same template, you must use the [DependsOn Attribute](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-dependson.html) to declare a dependency on the VPC-gateway attachment.", "PlacementGroupName": "The name of an existing placement group that you want to launch the instance into (cluster | partition | spread).", + "PrivateDnsNameOptions": "The options for the instance hostname.", "PrivateIpAddress": "[EC2-VPC] The primary IPv4 address. You must specify a value from the IPv4 address range of the subnet.\n\nOnly one private IP address can be designated as primary. You can't specify this option if you've specified the option to designate a private IP address as the primary IP address in a network interface specification. You cannot specify this option if you're launching more than one instance in the request.\n\nYou cannot specify this option and the network interfaces option in the same request.\n\nIf you make an update to an instance that requires replacement, you must assign a new private IP address. During a replacement, AWS CloudFormation creates a new instance but doesn't delete the old instance until the stack has successfully updated. If the stack update fails, AWS CloudFormation uses the old instance to roll back the stack to the previous working state. The old and new instances cannot have the same private IP address.", - "PropagateTagsToVolumeOnCreation": "Whether to propagate the EC2 instance tags to the EBS volumes.", + "PropagateTagsToVolumeOnCreation": "Indicates whether to assign the tags from the instance to all of the volumes attached to the instance at launch. If you specify `true` and you assign tags to the instance, those tags are automatically assigned to all of the volumes that you attach to the instance at launch. If you specify `false` , those tags are not assigned to the attached volumes.", "RamdiskId": "The ID of the RAM disk to select. Some kernels require additional drivers at launch. Check the kernel requirements for information about whether you need to specify a RAM disk. To find kernel requirements, go to the AWS Resource Center and search for the kernel ID.\n\n> We recommend that you use PV-GRUB instead of kernels and RAM disks. For more information, see [PV-GRUB](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/UserProvidedkernels.html) in the *Amazon EC2 User Guide* .", "SecurityGroupIds": "The IDs of the security groups. You can create a security group using [CreateSecurityGroup](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateSecurityGroup.html) .\n\nIf you specify a network interface, you must specify any security groups as part of the network interface.", "SecurityGroups": "[EC2-Classic, default VPC] The names of the security groups. For a nondefault VPC, you must use security group IDs instead.\n\nYou cannot specify this option and the network interfaces option in the same request. The list can contain both the name of existing Amazon EC2 security groups or references to AWS::EC2::SecurityGroup resources created in the template.\n\nDefault: Amazon EC2 uses the default security group.", @@ -11562,6 +11827,15 @@ "description": "Suppresses the specified device included in the block device mapping of the AMI. To suppress a device, specify an empty string.\n\n`NoDevice` is a property of the [Amazon EC2 BlockDeviceMapping](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-blockdev-mapping.html) property.", "properties": {} }, + "AWS::EC2::Instance.PrivateDnsNameOptions": { + "attributes": {}, + "description": "The type of hostnames to assign to instances in the subnet at launch. For IPv4 only subnets, an instance DNS name must be based on the instance IPv4 address. For IPv6 only subnets, an instance DNS name must be based on the instance ID. For dual-stack subnets, you can specify whether DNS names use the instance IPv4 address or the instance ID. For more information, see [Amazon EC2 instance hostname types](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-naming.html) in the *Amazon Elastic Compute Cloud User Guide* .", + "properties": { + "EnableResourceNameDnsAAAARecord": "Indicates whether to respond to DNS queries for instance hostnames with DNS AAAA records. For more information, see [Amazon EC2 instance hostname types](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-naming.html) in the *Amazon Elastic Compute Cloud User Guide* .", + "EnableResourceNameDnsARecord": "Indicates whether to respond to DNS queries for instance hostnames with DNS A records. For more information, see [Amazon EC2 instance hostname types](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-naming.html) in the *Amazon Elastic Compute Cloud User Guide* .", + "HostnameType": "The type of hostnames to assign to instances in the subnet at launch. For IPv4 only subnets, an instance DNS name must be based on the instance IPv4 address. For IPv6 only subnets, an instance DNS name must be based on the instance ID. For dual-stack subnets, you can specify whether DNS names use the instance IPv4 address or the instance ID. For more information, see [Amazon EC2 instance hostname types](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-naming.html) in the *Amazon Elastic Compute Cloud User Guide* ." + } + }, "AWS::EC2::Instance.PrivateIpAddressSpecification": { "attributes": {}, "description": "Specifies a secondary private IPv4 address for a network interface.\n\n`PrivateIpAddressSpecification` is a property of the [AWS::EC2::NetworkInterface](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html) resource.", @@ -11725,6 +11999,33 @@ "SpotOptions": "The options for Spot Instances." } }, + "AWS::EC2::LaunchTemplate.InstanceRequirements": { + "attributes": {}, + "description": "The attributes for the instance types. When you specify instance attributes, Amazon EC2 will identify instance types with these attributes.\n\nWhen you specify multiple parameters, you get instance types that satisfy all of the specified parameters. If you specify multiple values for a parameter, you get instance types that satisfy any of the specified values.\n\n> You must specify `VCpuCount` and `MemoryMiB` . All other parameters are optional. Any unspecified optional parameter is set to its default. \n\nFor more information, see [Attribute-based instance type selection for EC2 Fleet](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-fleet-attribute-based-instance-type-selection.html) , [Attribute-based instance type selection for Spot Fleet](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-fleet-attribute-based-instance-type-selection.html) , and [Spot placement score](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-placement-score.html) in the *Amazon EC2 User Guide* .", + "properties": { + "AcceleratorCount": "The minimum and maximum number of accelerators (GPUs, FPGAs, or AWS Inferentia chips) on an instance.\n\nTo exclude accelerator-enabled instance types, set `Max` to `0` .\n\nDefault: No minimum or maximum limits", + "AcceleratorManufacturers": "Indicates whether instance types must have accelerators by specific manufacturers.\n\n- For instance types with NVIDIA devices, specify `nvidia` .\n- For instance types with AMD devices, specify `amd` .\n- For instance types with AWS devices, specify `amazon-web-services` .\n- For instance types with Xilinx devices, specify `xilinx` .\n\nDefault: Any manufacturer", + "AcceleratorNames": "The accelerators that must be on the instance type.\n\n- For instance types with NVIDIA A100 GPUs, specify `a100` .\n- For instance types with NVIDIA V100 GPUs, specify `v100` .\n- For instance types with NVIDIA K80 GPUs, specify `k80` .\n- For instance types with NVIDIA T4 GPUs, specify `t4` .\n- For instance types with NVIDIA M60 GPUs, specify `m60` .\n- For instance types with AMD Radeon Pro V520 GPUs, specify `radeon-pro-v520` .\n- For instance types with Xilinx VU9P FPGAs, specify `vu9p` .\n\nDefault: Any accelerator", + "AcceleratorTotalMemoryMiB": "The minimum and maximum amount of total accelerator memory, in MiB.\n\nDefault: No minimum or maximum limits", + "AcceleratorTypes": "The accelerator types that must be on the instance type.\n\n- For instance types with GPU accelerators, specify `gpu` .\n- For instance types with FPGA accelerators, specify `fpga` .\n- For instance types with inference accelerators, specify `inference` .\n\nDefault: Any accelerator type", + "BareMetal": "Indicates whether bare metal instance types must be included, excluded, or required.\n\n- To include bare metal instance types, specify `included` .\n- To require only bare metal instance types, specify `required` .\n- To exclude bare metal instance types, specify `excluded` .\n\nDefault: `excluded`", + "BaselineEbsBandwidthMbps": "The minimum and maximum baseline bandwidth to Amazon EBS, in Mbps. For more information, see [Amazon EBS\u2013optimized instances](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-optimized.html) in the *Amazon EC2 User Guide* .\n\nDefault: No minimum or maximum limits", + "BurstablePerformance": "Indicates whether burstable performance T instance types are included, excluded, or required. For more information, see [Burstable performance instances](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/burstable-performance-instances.html) .\n\n- To include burstable performance instance types, specify `included` .\n- To require only burstable performance instance types, specify `required` .\n- To exclude burstable performance instance types, specify `excluded` .\n\nDefault: `excluded`", + "CpuManufacturers": "The CPU manufacturers to include.\n\n- For instance types with Intel CPUs, specify `intel` .\n- For instance types with AMD CPUs, specify `amd` .\n- For instance types with AWS CPUs, specify `amazon-web-services` .\n\n> Don't confuse the CPU manufacturer with the CPU architecture. Instances will be launched with a compatible CPU architecture based on the Amazon Machine Image (AMI) that you specify in your launch template. \n\nDefault: Any manufacturer", + "ExcludedInstanceTypes": "The instance types to exclude. You can use strings with one or more wild cards, represented by an asterisk ( `*` ), to exclude an instance type, size, or generation. The following are examples: `m5.8xlarge` , `c5*.*` , `m5a.*` , `r*` , `*3*` .\n\nFor example, if you specify `c5*` ,Amazon EC2 will exclude the entire C5 instance family, which includes all C5a and C5n instance types. If you specify `m5a.*` , Amazon EC2 will exclude all the M5a instance types, but not the M5n instance types.\n\nDefault: No excluded instance types", + "InstanceGenerations": "Indicates whether current or previous generation instance types are included. The current generation instance types are recommended for use. Current generation instance types are typically the latest two to three generations in each instance family. For more information, see [Instance types](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html) in the *Amazon EC2 User Guide* .\n\nFor current generation instance types, specify `current` .\n\nFor previous generation instance types, specify `previous` .\n\nDefault: Current and previous generation instance types", + "LocalStorage": "Indicates whether instance types with instance store volumes are included, excluded, or required. For more information, [Amazon EC2 instance store](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html) in the *Amazon EC2 User Guide* .\n\n- To include instance types with instance store volumes, specify `included` .\n- To require only instance types with instance store volumes, specify `required` .\n- To exclude instance types with instance store volumes, specify `excluded` .\n\nDefault: `included`", + "LocalStorageTypes": "The type of local storage that is required.\n\n- For instance types with hard disk drive (HDD) storage, specify `hdd` .\n- For instance types with solid state drive (SDD) storage, specify `sdd` .\n\nDefault: `hdd` and `sdd`", + "MemoryGiBPerVCpu": "The minimum and maximum amount of memory per vCPU, in GiB.\n\nDefault: No minimum or maximum limits", + "MemoryMiB": "The minimum and maximum amount of memory, in MiB.", + "NetworkInterfaceCount": "The minimum and maximum number of network interfaces.\n\nDefault: No minimum or maximum limits", + "OnDemandMaxPricePercentageOverLowestPrice": "The price protection threshold for On-Demand Instances. This is the maximum you\u2019ll pay for an On-Demand Instance, expressed as a percentage above the cheapest M, C, or R instance type with your specified attributes. When Amazon EC2 selects instance types with your attributes, it excludes instance types priced above your threshold.\n\nThe parameter accepts an integer, which Amazon EC2 interprets as a percentage.\n\nTo turn off price protection, specify a high value, such as `999999` .\n\nThis parameter is not supported for [GetSpotPlacementScores](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetSpotPlacementScores.html) and [GetInstanceTypesFromInstanceRequirements](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetInstanceTypesFromInstanceRequirements.html) .\n\n> If you set `TargetCapacityUnitType` to `vcpu` or `memory-mib` , the price protection threshold is applied based on the per-vCPU or per-memory price instead of the per-instance price. \n\nDefault: `20`", + "RequireHibernateSupport": "Indicates whether instance types must support hibernation for On-Demand Instances.\n\nThis parameter is not supported for [GetSpotPlacementScores](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetSpotPlacementScores.html) .\n\nDefault: `false`", + "SpotMaxPricePercentageOverLowestPrice": "The price protection threshold for Spot Instances. This is the maximum you\u2019ll pay for a Spot Instance, expressed as a percentage above the cheapest M, C, or R instance type with your specified attributes. When Amazon EC2 selects instance types with your attributes, it excludes instance types priced above your threshold.\n\nThe parameter accepts an integer, which Amazon EC2 interprets as a percentage.\n\nTo turn off price protection, specify a high value, such as `999999` .\n\nThis parameter is not supported for [GetSpotPlacementScores](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetSpotPlacementScores.html) and [GetInstanceTypesFromInstanceRequirements](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetInstanceTypesFromInstanceRequirements.html) .\n\n> If you set `TargetCapacityUnitType` to `vcpu` or `memory-mib` , the price protection threshold is applied based on the per-vCPU or per-memory price instead of the per-instance price. \n\nDefault: `100`", + "TotalLocalStorageGB": "The minimum and maximum amount of total local storage, in GB.\n\nDefault: No minimum or maximum limits", + "VCpuCount": "The minimum and maximum number of vCPUs." + } + }, "AWS::EC2::LaunchTemplate.Ipv6Add": { "attributes": {}, "description": "Specifies an IPv6 address in an Amazon EC2 launch template.\n\n`Ipv6Add` is a property of [AWS::EC2::LaunchTemplate NetworkInterface](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-networkinterface.html) .", @@ -11750,6 +12051,7 @@ "ImageId": "The ID of the AMI.", "InstanceInitiatedShutdownBehavior": "Indicates whether an instance stops or terminates when you initiate shutdown from the instance (using the operating system command for system shutdown).\n\nDefault: `stop`", "InstanceMarketOptions": "The market (purchasing) option for the instances.", + "InstanceRequirements": "", "InstanceType": "The instance type. For more information, see [Instance Types](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html) in the *Amazon Elastic Compute Cloud User Guide* .\n\nIf you specify `InstanceTypes` , you can't specify `InstanceRequirements` .", "KernelId": "The ID of the kernel.\n\nWe recommend that you use PV-GRUB instead of kernels and RAM disks. For more information, see [User Provided Kernels](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/UserProvidedkernels.html) in the *Amazon EC2 User Guide* .", "KeyName": "The name of the key pair. You can create a key pair using [CreateKeyPair](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateKeyPair.html) or [ImportKeyPair](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_ImportKeyPair.html) .\n\n> If you do not specify a key pair, you can't connect to the instance unless you choose an AMI that is configured to allow users another way to log in.", @@ -11758,6 +12060,7 @@ "Monitoring": "The monitoring for the instance.", "NetworkInterfaces": "One or more network interfaces. If you specify a network interface, you must specify any security groups and subnets as part of the network interface.", "Placement": "The placement for the instance.", + "PrivateDnsNameOptions": "The options for the instance hostname. The default values are inherited from the subnet.", "RamDiskId": "The ID of the RAM disk.\n\n> We recommend that you use PV-GRUB instead of kernels and RAM disks. For more information, see [User Provided Kernels](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/UserProvidedkernels.html) in the *Amazon Elastic Compute Cloud User Guide* .", "SecurityGroupIds": "One or more security group IDs. You can create a security group using [CreateSecurityGroup](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateSecurityGroup.html) . You cannot specify both a security group ID and security name in the same request.", "SecurityGroups": "[EC2-Classic, default VPC] One or more security group names. For a nondefault VPC, you must use security group IDs instead. You cannot specify both a security group ID and security name in the same request.", @@ -11811,7 +12114,8 @@ "HttpEndpoint": "Enables or disables the HTTP metadata endpoint on your instances. If the parameter is not specified, the default state is `enabled` .\n\n> If you specify a value of `disabled` , you will not be able to access your instance metadata.", "HttpProtocolIpv6": "Enables or disables the IPv6 endpoint for the instance metadata service.\n\nDefault: `disabled`", "HttpPutResponseHopLimit": "The desired HTTP PUT response hop limit for instance metadata requests. The larger the number, the further instance metadata requests can travel.\n\nDefault: 1\n\nPossible values: Integers from 1 to 64", - "HttpTokens": "The state of token usage for your instance metadata requests. If the parameter is not specified in the request, the default state is `optional` .\n\nIf the state is `optional` , you can choose to retrieve instance metadata with or without a signed token header on your request. If you retrieve the IAM role credentials without a token, the version 1.0 role credentials are returned. If you retrieve the IAM role credentials using a valid signed token, the version 2.0 role credentials are returned.\n\nIf the state is `required` , you must send a signed token header with any instance metadata retrieval requests. In this state, retrieving the IAM role credentials always returns the version 2.0 credentials; the version 1.0 credentials are not available." + "HttpTokens": "The state of token usage for your instance metadata requests. If the parameter is not specified in the request, the default state is `optional` .\n\nIf the state is `optional` , you can choose to retrieve instance metadata with or without a signed token header on your request. If you retrieve the IAM role credentials without a token, the version 1.0 role credentials are returned. If you retrieve the IAM role credentials using a valid signed token, the version 2.0 role credentials are returned.\n\nIf the state is `required` , you must send a signed token header with any instance metadata retrieval requests. In this state, retrieving the IAM role credentials always returns the version 2.0 credentials; the version 1.0 credentials are not available.", + "InstanceMetadataTags": "Set to `enabled` to allow access to instance tags from the instance metadata. Set to `disabled` to turn off access to instance tags from the instance metadata. For more information, see [Work with instance tags using the instance metadata](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#work-with-tags-in-IMDS) .\n\nDefault: `disabled`" } }, "AWS::EC2::LaunchTemplate.Monitoring": { @@ -11864,6 +12168,15 @@ "Tenancy": "The tenancy of the instance (if the instance is running in a VPC). An instance with a tenancy of dedicated runs on single-tenant hardware." } }, + "AWS::EC2::LaunchTemplate.PrivateDnsNameOptions": { + "attributes": {}, + "description": "Describes the options for instance hostnames.", + "properties": { + "EnableResourceNameDnsAAAARecord": "Indicates whether to respond to DNS queries for instance hostnames with DNS AAAA records.", + "EnableResourceNameDnsARecord": "Indicates whether to respond to DNS queries for instance hostnames with DNS A records.", + "HostnameType": "The type of hostname for EC2 instances. For IPv4 only subnets, an instance DNS name must be based on the instance IPv4 address. For IPv6 only subnets, an instance DNS name must be based on the instance ID. For dual-stack subnets, you can specify whether DNS names use the instance IPv4 address or the instance ID. For more information, see [Amazon EC2 instance hostname types](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-naming.html) in the *Amazon Elastic Compute Cloud User Guide* ." + } + }, "AWS::EC2::LaunchTemplate.PrivateIpAdd": { "attributes": {}, "description": "Specifies a secondary private IPv4 address for a network interface.\n\n`PrivateIpAdd` is a property of [AWS::EC2::LaunchTemplate NetworkInterface](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-networkinterface.html) .", @@ -11993,13 +12306,13 @@ }, "AWS::EC2::NetworkInsightsAccessScope": { "attributes": { - "CreatedDate": "", - "NetworkInsightsAccessScopeArn": "", - "NetworkInsightsAccessScopeId": "", - "Ref": "", - "UpdatedDate": "" + "CreatedDate": "The creation date.", + "NetworkInsightsAccessScopeArn": "The ARN of the Network Access Scope.", + "NetworkInsightsAccessScopeId": "The ID of the Network Access Scope.", + "Ref": "`Ref` returns the ID of the network insights scope.", + "UpdatedDate": "The last updated date." }, - "description": "Describes a Network Access Scope.", + "description": "Describes a Network Access Scope. A Network Access Scope defines outbound (egress) and inbound (ingress) traffic patterns, including sources, destinations, paths, and traffic types.\n\nNetwork Access Analyzer identifies unintended network access to your resources on AWS . When you start an analysis on a Network Access Scope, Network Access Analyzer produces findings. For more information, see the [Network Access Analyzer User Guide](https://docs.aws.amazon.com/vpc/latest/network-access-analyzer/) .", "properties": { "ExcludePaths": "The paths to exclude.", "MatchPaths": "The paths to match.", @@ -12053,15 +12366,15 @@ }, "AWS::EC2::NetworkInsightsAccessScopeAnalysis": { "attributes": { - "AnalyzedEniCount": "", - "EndDate": "", - "FindingsFound": "", - "NetworkInsightsAccessScopeAnalysisArn": "", - "NetworkInsightsAccessScopeAnalysisId": "", - "Ref": "", - "StartDate": "", - "Status": "", - "StatusMessage": "" + "AnalyzedEniCount": "The number of network interfaces analyzed.", + "EndDate": "The end date of the analysis.", + "FindingsFound": "Indicates whether there are findings (true | false | unknown).", + "NetworkInsightsAccessScopeAnalysisArn": "The ARN of the Network Access Scope analysis.", + "NetworkInsightsAccessScopeAnalysisId": "The ID of the Network Access Scope analysis.", + "Ref": "`Ref` returns the ID of the network insights analysis.", + "StartDate": "The start date of the analysis.", + "Status": "The status of the analysis (running | succeeded | failed).", + "StatusMessage": "The status message." }, "description": "Describes a Network Access Scope analysis.", "properties": { @@ -12280,7 +12593,7 @@ "properties": { "Description": "A description for the network interface.", "GroupSet": "The security group IDs associated with this network interface.", - "InterfaceType": "Indicates the type of network interface. To create an Elastic Fabric Adapter (EFA), specify `efa` . For more information, see [Elastic Fabric Adapter](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/efa.html) in the *Amazon Elastic Compute Cloud User Guide* . To create a trunk network interface, specify `trunk` .", + "InterfaceType": "The type of network interface. The default is `interface` . The supported values are `efa` and `trunk` .", "Ipv6AddressCount": "The number of IPv6 addresses to assign to a network interface. Amazon EC2 automatically selects the IPv6 addresses from the subnet range. To specify specific IPv6 addresses, use the `Ipv6Addresses` property and don't specify this property.", "Ipv6Addresses": "One or more specific IPv6 addresses from the IPv6 CIDR block range of your subnet to associate with the network interface. If you're specifying a number of IPv6 addresses, use the `Ipv6AddressCount` property and don't specify this property.", "PrivateIpAddress": "Assigns a single private IP address to the network interface, which is used as the primary private IP address. If you want to specify multiple private IP address, use the `PrivateIpAddresses` property.", @@ -12613,9 +12926,9 @@ "MemoryGiBPerVCpu": "The minimum and maximum amount of memory per vCPU, in GiB.\n\nDefault: No minimum or maximum limits", "MemoryMiB": "The minimum and maximum amount of memory, in MiB.", "NetworkInterfaceCount": "The minimum and maximum number of network interfaces.\n\nDefault: No minimum or maximum limits", - "OnDemandMaxPricePercentageOverLowestPrice": "The price protection threshold for On-Demand Instances. This is the maximum you\u2019ll pay for an On-Demand Instance, expressed as a percentage above the cheapest M, C, or R instance type with your specified attributes. When Amazon EC2 selects instance types with your attributes, it excludes instance types priced above your threshold.\n\nThe parameter accepts an integer, which Amazon EC2 interprets as a percentage.\n\nTo turn off price protection, specify a high value, such as `999999` .\n\nThis parameter is not supported for [GetSpotPlacementScores](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetSpotPlacementScores.html) and [GetInstanceTypesFromInstanceRequirements](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetInstanceTypesFromInstanceRequirements.html) .\n\nDefault: `20`", + "OnDemandMaxPricePercentageOverLowestPrice": "The price protection threshold for On-Demand Instances. This is the maximum you\u2019ll pay for an On-Demand Instance, expressed as a percentage above the cheapest M, C, or R instance type with your specified attributes. When Amazon EC2 selects instance types with your attributes, it excludes instance types priced above your threshold.\n\nThe parameter accepts an integer, which Amazon EC2 interprets as a percentage.\n\nTo turn off price protection, specify a high value, such as `999999` .\n\nThis parameter is not supported for [GetSpotPlacementScores](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetSpotPlacementScores.html) and [GetInstanceTypesFromInstanceRequirements](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetInstanceTypesFromInstanceRequirements.html) .\n\n> If you set `TargetCapacityUnitType` to `vcpu` or `memory-mib` , the price protection threshold is applied based on the per-vCPU or per-memory price instead of the per-instance price. \n\nDefault: `20`", "RequireHibernateSupport": "Indicates whether instance types must support hibernation for On-Demand Instances.\n\nThis parameter is not supported for [GetSpotPlacementScores](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetSpotPlacementScores.html) .\n\nDefault: `false`", - "SpotMaxPricePercentageOverLowestPrice": "The price protection threshold for Spot Instance. This is the maximum you\u2019ll pay for an Spot Instance, expressed as a percentage above the cheapest M, C, or R instance type with your specified attributes. When Amazon EC2 selects instance types with your attributes, it excludes instance types priced above your threshold.\n\nThe parameter accepts an integer, which Amazon EC2 interprets as a percentage.\n\nTo turn off price protection, specify a high value, such as `999999` .\n\nThis parameter is not supported for [GetSpotPlacementScores](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetSpotPlacementScores.html) and [GetInstanceTypesFromInstanceRequirements](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetInstanceTypesFromInstanceRequirements.html) .\n\nDefault: `100`", + "SpotMaxPricePercentageOverLowestPrice": "The price protection threshold for Spot Instance. This is the maximum you\u2019ll pay for an Spot Instance, expressed as a percentage above the cheapest M, C, or R instance type with your specified attributes. When Amazon EC2 selects instance types with your attributes, it excludes instance types priced above your threshold.\n\nThe parameter accepts an integer, which Amazon EC2 interprets as a percentage.\n\nTo turn off price protection, specify a high value, such as `999999` .\n\nThis parameter is not supported for [GetSpotPlacementScores](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetSpotPlacementScores.html) and [GetInstanceTypesFromInstanceRequirements](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetInstanceTypesFromInstanceRequirements.html) .\n\n> If you set `TargetCapacityUnitType` to `vcpu` or `memory-mib` , the price protection threshold is applied based on the per-vCPU or per-memory price instead of the per-instance price. \n\nDefault: `100`", "TotalLocalStorageGB": "The minimum and maximum amount of total local storage, in GB.\n\nDefault: No minimum or maximum limits", "VCpuCount": "The minimum and maximum number of vCPUs." } @@ -12808,20 +13121,34 @@ "NetworkAclAssociationId": "The ID of the network ACL that is associated with the subnet's VPC, such as `acl-5fb85d36` .", "OutpostArn": "The Amazon Resource Name (ARN) of the Outpost.", "Ref": "`Ref` returns the ID of the subnet.", + "SubnetId": "The ID of the subnet.", "VpcId": "The ID of the subnet's VPC, such as `vpc-11ad4878` ." }, "description": "Specifies a subnet for a VPC.\n\nWhen you create each subnet, you provide the VPC ID and IPv4 CIDR block for the subnet. After you create a subnet, you can't change its CIDR block. The size of the subnet's IPv4 CIDR block can be the same as a VPC's IPv4 CIDR block, or a subset of a VPC's IPv4 CIDR block. If you create more than one subnet in a VPC, the subnets' CIDR blocks must not overlap. The smallest IPv4 subnet (and VPC) you can create uses a /28 netmask (16 IPv4 addresses), and the largest uses a /16 netmask (65,536 IPv4 addresses).\n\nIf you've associated an IPv6 CIDR block with your VPC, you can create a subnet with an IPv6 CIDR block that uses a /64 prefix length.", "properties": { "AssignIpv6AddressOnCreation": "Indicates whether a network interface created in this subnet receives an IPv6 address. The default value is `false` .\n\nIf you specify `AssignIpv6AddressOnCreation` , you must also specify `Ipv6CidrBlock` .\n\nIf you specify `AssignIpv6AddressOnCreation` , you cannot specify `MapPublicIpOnLaunch` .", "AvailabilityZone": "The Availability Zone of the subnet.\n\nIf you update this property, you must also update the `CidrBlock` property.", + "AvailabilityZoneId": "The AZ ID of the subnet.", "CidrBlock": "The IPv4 CIDR block assigned to the subnet.\n\nIf you update this property, we create a new subnet, and then delete the existing one.", + "EnableDns64": "Indicates whether DNS queries made to the Amazon-provided DNS Resolver in this subnet should return synthetic IPv6 addresses for IPv4-only destinations. For more information, see [DNS64 and NAT64](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html#nat-gateway-nat64-dns64) in the *Amazon Virtual Private Cloud User Guide* .", "Ipv6CidrBlock": "The IPv6 CIDR block.\n\nIf you specify `AssignIpv6AddressOnCreation` , you must also specify `Ipv6CidrBlock` .", + "Ipv6Native": "Indicates whether this is an IPv6 only subnet. For more information, see [Subnet basics](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Subnets.html#subnet-basics) in the *Amazon Virtual Private Cloud User Guide* .", "MapPublicIpOnLaunch": "Indicates whether instances launched in this subnet receive a public IPv4 address. The default value is `false` .\n\nIf you specify `MapPublicIpOnLaunch` , you cannot specify `AssignIpv6AddressOnCreation` .", "OutpostArn": "The Amazon Resource Name (ARN) of the Outpost.", + "PrivateDnsNameOptionsOnLaunch": "The hostname type for EC2 instances launched into this subnet and how DNS A and AAAA record queries to the instances should be handled. For more information, see [Amazon EC2 instance hostname types](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-naming.html) in the *Amazon Elastic Compute Cloud User Guide* .", "Tags": "Any tags assigned to the subnet.", "VpcId": "The ID of the VPC the subnet is in.\n\nIf you update this property, you must also update the `CidrBlock` property." } }, + "AWS::EC2::Subnet.PrivateDnsNameOptionsOnLaunch": { + "attributes": {}, + "description": "Describes the options for instance hostnames.", + "properties": { + "EnableResourceNameDnsAAAARecord": "Indicates whether to respond to DNS queries for instance hostname with DNS AAAA records.", + "EnableResourceNameDnsARecord": "Indicates whether to respond to DNS queries for instance hostnames with DNS A records.", + "HostnameType": "The type of hostname for EC2 instances. For IPv4 only subnets, an instance DNS name must be based on the instance IPv4 address. For IPv6 only subnets, an instance DNS name must be based on the instance ID. For dual-stack subnets, you can specify whether DNS names use the instance IPv4 address or the instance ID." + } + }, "AWS::EC2::SubnetCidrBlock": { "attributes": { "Ref": "`Ref` returns the association ID for the subnet\u2019s IPv6 CIDR block." @@ -12845,7 +13172,7 @@ }, "AWS::EC2::SubnetRouteTableAssociation": { "attributes": { - "Id": "", + "Id": "The ID of the subnet route table association.", "Ref": "`Ref` returns the ID of the subnet route table association." }, "description": "Associates a subnet with a route table. The subnet and route table must be in the same VPC. This association causes traffic originating from the subnet to be routed according to the routes in the route table. A route table can be associated with multiple subnets. If you want to associate a route table with a VPC, see [AWS::EC2::RouteTable](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-route-table.html) .", @@ -13115,7 +13442,7 @@ "description": "Specifies a VPC attachment.", "properties": { "AddSubnetIds": "The IDs of one or more subnets to add. You can specify at most one subnet per Availability Zone.", - "Options": "The VPC attachment options.\n\n- DnsSupport (enable | disable)\n- Ipv6Support (enable| disable)\n- ApplianceModeSupport (enable | disable)", + "Options": "The VPC attachment options in JSON or YAML.\n\n- DnsSupport (enable | disable)\n- Ipv6Support (enable| disable)\n- ApplianceModeSupport (enable | disable)", "RemoveSubnetIds": "The IDs of one or more subnets to remove.", "SubnetIds": "The IDs of the subnets.", "Tags": "The tags for the VPC attachment.", @@ -13132,11 +13459,11 @@ "Ipv6CidrBlocks": "The IPv6 CIDR blocks that are associated with the VPC, such as `[ 2001:db8:1234:1a00::/56 ]` .", "Ref": "`Ref` returns the ID of the VPC." }, - "description": "Specifies a VPC with the specified IPv4 CIDR block. The smallest VPC you can create uses a /28 netmask (16 IPv4 addresses), and the largest uses a /16 netmask (65,536 IPv4 addresses). For more information about how large to make your VPC, see [Your VPC and Subnets](https://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Subnets.html) in the *Amazon Virtual Private Cloud User Guide* .", + "description": "Specifies a VPC with the specified IPv4 CIDR block. The smallest VPC you can create uses a /28 netmask (16 IPv4 addresses), and the largest uses a /16 netmask (65,536 IPv4 addresses). For more information about how large to make your VPC, see [Overview of VPCs and subnets](https://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Subnets.html) in the *Amazon Virtual Private Cloud User Guide* .", "properties": { "CidrBlock": "The primary IPv4 CIDR block for the VPC.", - "EnableDnsHostnames": "Indicates whether the instances launched in the VPC get DNS hostnames. If enabled, instances in the VPC get DNS hostnames; otherwise, they do not. Disabled by default for nondefault VPCs. For more information, see [DNS Support in Your VPC](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-dns.html#vpc-dns-support) .\n\nYou can only enable DNS hostnames if you've enabled DNS support.", - "EnableDnsSupport": "Indicates whether the DNS resolution is supported for the VPC. If enabled, queries to the Amazon provided DNS server at the 169.254.169.253 IP address, or the reserved IP address at the base of the VPC network range \"plus two\" succeed. If disabled, the Amazon provided DNS service in the VPC that resolves public DNS hostnames to IP addresses is not enabled. Enabled by default. For more information, see [DNS Support in Your VPC](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-dns.html#vpc-dns-support) .", + "EnableDnsHostnames": "Indicates whether the instances launched in the VPC get DNS hostnames. If enabled, instances in the VPC get DNS hostnames; otherwise, they do not. Disabled by default for nondefault VPCs. For more information, see [DNS attributes in your VPC](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-dns.html#vpc-dns-support) .\n\nYou can only enable DNS hostnames if you've enabled DNS support.", + "EnableDnsSupport": "Indicates whether the DNS resolution is supported for the VPC. If enabled, queries to the Amazon provided DNS server at the 169.254.169.253 IP address, or the reserved IP address at the base of the VPC network range \"plus two\" succeed. If disabled, the Amazon provided DNS service in the VPC that resolves public DNS hostnames to IP addresses is not enabled. Enabled by default. For more information, see [DNS attributes in your VPC](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-dns.html#vpc-dns-support) .", "InstanceTenancy": "The allowed tenancy of instances launched into the VPC.\n\n- `\"default\"` : An instance launched into the VPC runs on shared hardware by default, unless you explicitly specify a different tenancy during instance launch.\n- `\"dedicated\"` : An instance launched into the VPC is a Dedicated Instance by default, unless you explicitly specify a tenancy of host during instance launch. You cannot specify a tenancy of default during instance launch.\n\nUpdating `InstanceTenancy` requires no replacement only if you are updating its value from `\"dedicated\"` to `\"default\"` . Updating `InstanceTenancy` from `\"default\"` to `\"dedicated\"` requires replacement.", "Ipv4IpamPoolId": "The ID of an IPv4 IPAM pool you want to use for allocating this VPC's CIDR. For more information, see [What is IPAM?](https://docs.aws.amazon.com//vpc/latest/ipam/what-is-it-ipam.html) in the *Amazon VPC IPAM User Guide* .", "Ipv4NetmaskLength": "The netmask length of the IPv4 CIDR you want to allocate to this VPC from an Amazon VPC IP Address Manager (IPAM) pool. For more information about IPAM, see [What is IPAM?](https://docs.aws.amazon.com//vpc/latest/ipam/what-is-it-ipam.html) in the *Amazon VPC IPAM User Guide* .", @@ -13162,7 +13489,7 @@ }, "AWS::EC2::VPCDHCPOptionsAssociation": { "attributes": { - "Id": "", + "Id": "The ID of the DHCP options set.", "Ref": "`Ref` returns the ID of the DHCP options association." }, "description": "Associates a set of DHCP options with a VPC, or associates no DHCP options with the VPC.\n\nAfter you associate the options with the VPC, any existing instances and all new instances that you launch in that VPC use the options. You don't need to restart or relaunch the instances. They automatically pick up the changes within a few hours, depending on how frequently the instance renews its DHCP lease. You can explicitly renew the lease using the operating system on the instance.", @@ -13174,19 +13501,18 @@ "AWS::EC2::VPCEndpoint": { "attributes": { "CreationTimestamp": "The date and time the VPC endpoint was created. For example: `Fri Sep 28 23:34:36 UTC 2018.`", - "DnsEntries": "(Interface endpoint) The DNS entries for the endpoint. Each entry is a combination of the hosted zone ID and the DNS name. The entries are ordered as follows: regional public DNS, zonal public DNS, private DNS, and wildcard DNS. This order is not enforced for AWS Marketplace services.\n\nThe following is an example. In the first entry, the hosted zone ID is Z1HUB23UULQXV and the DNS name is vpce-01abc23456de78f9g-12abccd3.ec2.us-east-1.vpce.amazonaws.com.\n\n[\"Z1HUB23UULQXV:vpce-01abc23456de78f9g-12abccd3.ec2.us-east-1.vpce.amazonaws.com\", \"Z1HUB23UULQXV:vpce-01abc23456de78f9g-12abccd3-us-east-1a.ec2.us-east-1.vpce.amazonaws.com\", \"Z1C12344VYDITB0:ec2.us-east-1.amazonaws.com\"]\n\nIf you update the `PrivateDnsEnabled` or `SubnetIds` properties, the DNS entries in the list will change.", - "Id": "", - "NetworkInterfaceIds": "(Interface endpoint) One or more network interface IDs. If you update the `PrivateDnsEnabled` or `SubnetIds` properties, the items in this list might change.", + "DnsEntries": "(Interface endpoints) The DNS entries for the endpoint. Each entry is a combination of the hosted zone ID and the DNS name. The entries are ordered as follows: regional public DNS, zonal public DNS, private DNS, and wildcard DNS. This order is not enforced for AWS Marketplace services.\n\nThe following is an example. In the first entry, the hosted zone ID is Z1HUB23UULQXV and the DNS name is vpce-01abc23456de78f9g-12abccd3.ec2.us-east-1.vpce.amazonaws.com.\n\n[\"Z1HUB23UULQXV:vpce-01abc23456de78f9g-12abccd3.ec2.us-east-1.vpce.amazonaws.com\", \"Z1HUB23UULQXV:vpce-01abc23456de78f9g-12abccd3-us-east-1a.ec2.us-east-1.vpce.amazonaws.com\", \"Z1C12344VYDITB0:ec2.us-east-1.amazonaws.com\"]\n\nIf you update the `PrivateDnsEnabled` or `SubnetIds` properties, the DNS entries in the list will change.", + "NetworkInterfaceIds": "(Interface endpoints) One or more network interface IDs. If you update the `PrivateDnsEnabled` or `SubnetIds` properties, the items in this list might change.", "Ref": "`Ref` returns the ID of the VPC endpoint." }, - "description": "Specifies a VPC endpoint for a service. An endpoint enables you to create a private connection between your VPC and the service. The service may be provided by AWS , an AWS Marketplace Partner, or another AWS account. For more information, see [VPC Endpoints](https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints.html) in the *AWS PrivateLink User Guide* .\n\nA `gateway` endpoint serves as a target for a route in your route table for traffic destined for the AWS service. You can specify an endpoint policy to attach to the endpoint, which controls access to the service from your VPC. You can also specify the VPC route tables that use the endpoint.\n\nFor information about connectivity when you use a gateway endpoint to connect to an Amazon S3 bucket from an EC2 instance, see [Why can\u2019t I connect to an S3 bucket using a gateway VPC endpoint](https://docs.aws.amazon.com/premiumsupport/knowledge-center/connect-s3-vpc-endpoint) .\n\nAn `interface` endpoint is a network interface in your subnet that serves as an endpoint for communicating with the specified service. You can specify the subnets in which to create an endpoint, and the security groups to associate with the endpoint network interface.\n\nA `GatewayLoadBalancer` endpoint is a network interface in your subnet that serves an endpoint for communicating with a Gateway Load Balancer that you've configured as a VPC endpoint service.", + "description": "Specifies a VPC endpoint for a service. An endpoint enables you to create a private connection between your VPC and the service. The service may be provided by AWS , an AWS Marketplace Partner, or another AWS account. For more information, see the [AWS PrivateLink User Guide](https://docs.aws.amazon.com/vpc/latest/privatelink/) .\n\nAn interface endpoint establishes connections between the subnets in your VPC and an AWS service, your own service, or a service hosted by another AWS account . You can specify the subnets in which to create the endpoint and the security groups to associate with the endpoint network interface.\n\nA gateway endpoint serves as a target for a route in your route table for traffic destined for Amazon S3 or Amazon DynamoDB. You can specify an endpoint policy for the endpoint, which controls access to the service from your VPC. You can also specify the VPC route tables that use the endpoint. For information about connectivity to Amazon S3, see [Why can\u2019t I connect to an S3 bucket using a gateway VPC endpoint?](https://docs.aws.amazon.com/premiumsupport/knowledge-center/connect-s3-vpc-endpoint)\n\nA Gateway Load Balancer endpoint provides private connectivity between your VPC and virtual appliances from a service provider.", "properties": { - "PolicyDocument": "(Interface and gateway endpoints) A policy to attach to the endpoint that controls access to the service. If this parameter is not specified, we attach a default policy that allows full access to the service.\n\nFor CloudFormation templates in YAML, you can provide the policy in JSON or YAML format. AWS CloudFormation converts YAML policies to JSON format before calling the API to create or modify the VPC endpoint.", - "PrivateDnsEnabled": "(Interface endpoint) Indicate whether to associate a private hosted zone with the specified VPC. The private hosted zone contains a record set for the default public DNS name for the service for the Region (for example, `kinesis.us-east-1.amazonaws.com` ) which resolves to the private IP addresses of the endpoint network interfaces in the VPC. This enables you to make requests to the default public DNS name for the service instead of the public DNS names that are automatically generated by the VPC endpoint service.\n\nTo use a private hosted zone, you must set the following VPC attributes to `true` : `enableDnsHostnames` and `enableDnsSupport` .\n\nDefault: `false`", - "RouteTableIds": "(Gateway endpoint) One or more route table IDs.", - "SecurityGroupIds": "(Interface endpoint) The ID of one or more security groups to associate with the endpoint network interface.", - "ServiceName": "The service name. To get a list of available services, use the [DescribeVpcEndpointServices](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeVpcEndpointServices.html) request, or get the name from the service provider.", - "SubnetIds": "(Interface and Gateway Load Balancer endpoints) The ID of one or more subnets in which to create an endpoint network interface. For a Gateway Load Balancer endpoint, you can specify one subnet only.", + "PolicyDocument": "A policy that controls access to the service from the VPC. If this parameter is not specified, the default policy allows full access to the service. Endpoint policies are supported only for gateway and interface endpoints.\n\nFor CloudFormation templates in YAML, you can provide the policy in JSON or YAML format. AWS CloudFormation converts YAML policies to JSON format before calling the API to create or modify the VPC endpoint.", + "PrivateDnsEnabled": "Indicate whether to associate a private hosted zone with the specified VPC. The private hosted zone contains a record set for the default public DNS name for the service for the Region (for example, `kinesis.us-east-1.amazonaws.com` ), which resolves to the private IP addresses of the endpoint network interfaces in the VPC. This enables you to make requests to the default public DNS name for the service instead of the public DNS names that are automatically generated by the VPC endpoint service.\n\nTo use a private hosted zone, you must set the following VPC attributes to `true` : `enableDnsHostnames` and `enableDnsSupport` .\n\nThis property is supported only for interface endpoints.\n\nDefault: `false`", + "RouteTableIds": "The route table IDs. Routing is supported only for gateway endpoints.", + "SecurityGroupIds": "The IDs of the security groups to associate with the endpoint network interface. Security groups are supported only for interface endpoints.", + "ServiceName": "The service name. To list the available services, use [DescribeVpcEndpointServices](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeVpcEndpointServices.html) . Otherwise, get the name from the service provider.", + "SubnetIds": "The ID of the subnets in which to create an endpoint network interface. You must specify this property for an interface endpoints or a Gateway Load Balancer endpoint. You can't specify this property for a gateway endpoint. For a Gateway Load Balancer endpoint, you can specify only one subnet.", "VpcEndpointType": "The type of endpoint.\n\nDefault: Gateway", "VpcId": "The ID of the VPC in which the endpoint will be used." } @@ -13211,7 +13537,8 @@ "properties": { "AcceptanceRequired": "Indicates whether requests from service consumers to create an endpoint to your service must be accepted.", "GatewayLoadBalancerArns": "The Amazon Resource Names (ARNs) of one or more Gateway Load Balancers.", - "NetworkLoadBalancerArns": "The Amazon Resource Names (ARNs) of one or more Network Load Balancers for your service." + "NetworkLoadBalancerArns": "The Amazon Resource Names (ARNs) of one or more Network Load Balancers for your service.", + "PayerResponsibility": "The entity that is responsible for the endpoint costs. The default is the endpoint owner. If you set the payer responsibility to the service owner, you cannot set it back to the endpoint owner." } }, "AWS::EC2::VPCEndpointServicePermissions": { @@ -13295,6 +13622,7 @@ }, "AWS::EC2::VPNGatewayRoutePropagation": { "attributes": { + "Id": "The ID of the VPN gateway.", "Ref": "`Ref` returns the ID of the VPN gateway." }, "description": "Enables a virtual private gateway (VGW) to propagate routes to the specified route table of a VPC.\n\nIf you reference a VPN gateway that is in the same template as your VPN gateway route propagation, you must explicitly declare a dependency on the VPN gateway attachment. The `AWS::EC2::VPNGatewayRoutePropagation` resource cannot use the VPN gateway until it has successfully attached to the VPC. Add a [DependsOn Attribute](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-dependson.html) in the `AWS::EC2::VPNGatewayRoutePropagation` resource to explicitly declare a dependency on the VPN gateway attachment.", @@ -13345,6 +13673,16 @@ "Tags": "An array of key-value pairs to apply to this resource." } }, + "AWS::ECR::PullThroughCacheRule": { + "attributes": { + "RegistryId": "The account ID of the private registry." + }, + "description": "Creates a pull through cache rule. A pull through cache rule provides a way to cache images from an external public registry in your Amazon ECR private registry.", + "properties": { + "EcrRepositoryPrefix": "The Amazon ECR repository prefix associated with the pull through cache rule.", + "UpstreamRegistryUrl": "The upstream registry URL associated with the pull through cache rule." + } + }, "AWS::ECR::RegistryPolicy": { "attributes": { "RegistryId": "The account ID of the private registry the policy is associated with." @@ -13451,7 +13789,7 @@ "properties": { "AutoScalingGroupArn": "The Amazon Resource Name (ARN) or short name that identifies the Auto Scaling group.", "ManagedScaling": "The managed scaling settings for the Auto Scaling group capacity provider.", - "ManagedTerminationProtection": "The managed termination protection setting to use for the Auto Scaling group capacity provider. This determines whether the Auto Scaling group has managed termination protection.\n\n> When using managed termination protection, managed scaling must also be used otherwise managed termination protection doesn't work. \n\nWhen managed termination protection is enabled, Amazon ECS prevents the Amazon EC2 instances in an Auto Scaling group that contain tasks from being terminated during a scale-in action. The Auto Scaling group and each instance in the Auto Scaling group must have instance protection from scale-in actions enabled as well. For more information, see [Instance Protection](https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-instance-termination.html#instance-protection) in the *AWS Auto Scaling User Guide* .\n\nWhen managed termination protection is disabled, your Amazon EC2 instances aren't protected from termination when the Auto Scaling group scales in." + "ManagedTerminationProtection": "The managed termination protection setting to use for the Auto Scaling group capacity provider. This determines whether the Auto Scaling group has managed termination protection. The default is disabled.\n\n> When using managed termination protection, managed scaling must also be used otherwise managed termination protection doesn't work. \n\nWhen managed termination protection is enabled, Amazon ECS prevents the Amazon EC2 instances in an Auto Scaling group that contain tasks from being terminated during a scale-in action. The Auto Scaling group and each instance in the Auto Scaling group must have instance protection from scale-in actions enabled as well. For more information, see [Instance Protection](https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-instance-termination.html#instance-protection) in the *AWS Auto Scaling User Guide* .\n\nWhen managed termination protection is disabled, your Amazon EC2 instances aren't protected from termination when the Auto Scaling group scales in." } }, "AWS::ECS::CapacityProvider.ManagedScaling": { @@ -13570,7 +13908,7 @@ "DesiredCount": "The number of instantiations of the specified task definition to place and keep running on your cluster.\n\nFor new services, if a desired count is not specified, a default value of `1` is used. When using the `DAEMON` scheduling strategy, the desired count is not required.\n\nFor existing services, if a desired count is not specified, it is omitted from the operation.", "EnableECSManagedTags": "Specifies whether to enable Amazon ECS managed tags for the tasks within the service. For more information, see [Tagging Your Amazon ECS Resources](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-using-tags.html) in the *Amazon Elastic Container Service Developer Guide* .", "EnableExecuteCommand": "Determines whether the execute command functionality is enabled for the service. If `true` , the execute command functionality is enabled for all containers in tasks as part of the service.", - "HealthCheckGracePeriodSeconds": "The period of time, in seconds, that the Amazon ECS service scheduler ignores unhealthy Elastic Load Balancing target health checks after a task has first started. This is only used when your service is configured to use a load balancer. If your service has a load balancer defined and you don't specify a health check grace period value, the default value of `0` is used.\n\nIf your service's tasks take a while to start and respond to Elastic Load Balancing health checks, you can specify a health check grace period of up to 2,147,483,647 seconds (about 69 years). During that time, the Amazon ECS service scheduler ignores health check status. This grace period can prevent the service scheduler from marking tasks as unhealthy and stopping them before they have time to come up.", + "HealthCheckGracePeriodSeconds": "The period of time, in seconds, that the Amazon ECS service scheduler ignores unhealthy Elastic Load Balancing target health checks after a task has first started. This is only used when your service is configured to use a load balancer. If your service has a load balancer defined and you don't specify a health check grace period value, the default value of `0` is used.\n\nIf you do not use an Elastic Load Balancing, we recomend that you use the `startPeriod` in the task definition healtch check parameters. For more information, see [Health check](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_HealthCheck.html) .\n\nIf your service's tasks take a while to start and respond to Elastic Load Balancing health checks, you can specify a health check grace period of up to 2,147,483,647 seconds (about 69 years). During that time, the Amazon ECS service scheduler ignores health check status. This grace period can prevent the service scheduler from marking tasks as unhealthy and stopping them before they have time to come up.", "LaunchType": "The launch type on which to run your service. For more information, see [Amazon ECS Launch Types](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/launch_types.html) in the *Amazon Elastic Container Service Developer Guide* .", "LoadBalancers": "A list of load balancer objects to associate with the service. If you specify the `Role` property, `LoadBalancers` must be specified as well. For information about the number of load balancers that you can specify per service, see [Service Load Balancing](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-load-balancing.html) in the *Amazon Elastic Container Service Developer Guide* .", "NetworkConfiguration": "The network configuration for the service. This parameter is required for task definitions that use the `awsvpc` network mode to receive their own elastic network interface, and it is not supported for other network modes. For more information, see [Task Networking](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-networking.html) in the *Amazon Elastic Container Service Developer Guide* .", @@ -13690,7 +14028,7 @@ "PlacementConstraints": "An array of placement constraint objects to use for tasks.\n\n> This parameter isn't supported for tasks run on AWS Fargate .", "ProxyConfiguration": "The `ProxyConfiguration` property specifies the configuration details for the App Mesh proxy.\n\nYour Amazon ECS container instances require at least version 1.26.0 of the container agent and at least version 1.26.0-1 of the `ecs-init` package to enable a proxy configuration. If your container instances are launched from the Amazon ECS-optimized AMI version `20190301` or later, then they contain the required versions of the container agent and `ecs-init` . For more information, see [Amazon ECS-optimized Linux AMI](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-optimized_AMI.html) in the *Amazon Elastic Container Service Developer Guide* .", "RequiresCompatibilities": "The task launch types the task definition was validated against. To determine which task launch types the task definition is validated for, see the `TaskDefinition$compatibilities` parameter.", - "RuntimePlatform": "The operating system that your task definitions are running on. A platform family is specified only for tasks using the Fargate launch type.\n\nWhen you specify a task in a service, this value must match the `runtimePlatform` value of the service.", + "RuntimePlatform": "The operating system that your tasks definitions run on. A platform family is specified only for tasks using the Fargate launch type.\n\nWhen you specify a task definition in a service, this value must match the `runtimePlatform` value of the service.", "Tags": "The metadata that you apply to the task definition to help you categorize and organize them. Each tag consists of a key and an optional value. You define both of them.\n\nThe following basic restrictions apply to tags:\n\n- Maximum number of tags per resource - 50\n- For each resource, each tag key must be unique, and each tag key can have only one value.\n- Maximum key length - 128 Unicode characters in UTF-8\n- Maximum value length - 256 Unicode characters in UTF-8\n- If your tagging schema is used across multiple services and resources, remember that other services may have restrictions on allowed characters. Generally allowed characters are: letters, numbers, and spaces representable in UTF-8, and the following characters: + - = . _ : / @.\n- Tag keys and values are case-sensitive.\n- Do not use `aws:` , `AWS:` , or any upper or lowercase combination of such as a prefix for either keys or values as it is reserved for AWS use. You cannot edit or delete tag keys or values with this prefix. Tags with this prefix do not count against your tags per resource limit.", "TaskRoleArn": "The short name or full Amazon Resource Name (ARN) of the AWS Identity and Access Management role that grants containers in the task permission to call AWS APIs on your behalf. For more information, see [Amazon ECS Task Role](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html) in the *Amazon Elastic Container Service Developer Guide* .\n\nIAM roles for tasks on Windows require that the `-EnableTaskIAMRole` option is set when you launch the Amazon ECS-optimized Windows AMI. Your containers must also run some configuration code to use the feature. For more information, see [Windows IAM roles for tasks](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/windows_task_IAM_roles.html) in the *Amazon Elastic Container Service Developer Guide* .", "Volumes": "The list of data volume definitions for the task. For more information, see [Using data volumes in tasks](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/using_data_volumes.html) in the *Amazon Elastic Container Service Developer Guide* .\n\n> The `host` and `sourcePath` parameters aren't supported for tasks run on AWS Fargate ." @@ -13729,7 +14067,7 @@ "Links": "The `links` parameter allows containers to communicate with each other without the need for port mappings. This parameter is only supported if the network mode of a task definition is `bridge` . The `name:internalName` construct is analogous to `name:alias` in Docker links. Up to 255 letters (uppercase and lowercase), numbers, underscores, and hyphens are allowed. For more information about linking Docker containers, go to [Legacy container links](https://docs.aws.amazon.com/https://docs.docker.com/network/links/) in the Docker documentation. This parameter maps to `Links` in the [Create a container](https://docs.aws.amazon.com/https://docs.docker.com/engine/api/v1.35/#operation/ContainerCreate) section of the [Docker Remote API](https://docs.aws.amazon.com/https://docs.docker.com/engine/api/v1.35/) and the `--link` option to [docker run](https://docs.aws.amazon.com/https://docs.docker.com/engine/reference/run/#security-configuration) .\n\n> This parameter is not supported for Windows containers. > Containers that are collocated on a single container instance may be able to communicate with each other without requiring links or host port mappings. Network isolation is achieved on the container instance using security groups and VPC settings.", "LinuxParameters": "Linux-specific modifications that are applied to the container, such as Linux kernel capabilities. For more information see [KernelCapabilities](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_KernelCapabilities.html) .\n\n> This parameter is not supported for Windows containers.", "LogConfiguration": "The log configuration specification for the container.\n\nThis parameter maps to `LogConfig` in the [Create a container](https://docs.aws.amazon.com/https://docs.docker.com/engine/api/v1.35/#operation/ContainerCreate) section of the [Docker Remote API](https://docs.aws.amazon.com/https://docs.docker.com/engine/api/v1.35/) and the `--log-driver` option to [docker run](https://docs.aws.amazon.com/https://docs.docker.com/engine/reference/run/) . By default, containers use the same logging driver that the Docker daemon uses. However, the container may use a different logging driver than the Docker daemon by specifying a log driver with this parameter in the container definition. To use a different logging driver for a container, the log system must be configured properly on the container instance (or on a different log server for remote logging options). For more information on the options for different supported log drivers, see [Configure logging drivers](https://docs.aws.amazon.com/https://docs.docker.com/engine/admin/logging/overview/) in the Docker documentation.\n\n> Amazon ECS currently supports a subset of the logging drivers available to the Docker daemon (shown in the [LogConfiguration](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_LogConfiguration.html) data type). Additional log drivers may be available in future releases of the Amazon ECS container agent. \n\nThis parameter requires version 1.18 of the Docker Remote API or greater on your container instance. To check the Docker Remote API version on your container instance, log in to your container instance and run the following command: `sudo docker version --format '{{.Server.APIVersion}}'`\n\n> The Amazon ECS container agent running on a container instance must register the logging drivers available on that instance with the `ECS_AVAILABLE_LOGGING_DRIVERS` environment variable before containers placed on that instance can use these log configuration options. For more information, see [Amazon ECS Container Agent Configuration](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-agent-config.html) in the *Amazon Elastic Container Service Developer Guide* .", - "Memory": "The amount (in MiB) of memory to present to the container. If your container attempts to exceed the memory specified here, the container is killed. The total amount of memory reserved for all containers within a task must be lower than the task `memory` value, if one is specified. This parameter maps to `Memory` in the [Create a container](https://docs.aws.amazon.com/https://docs.docker.com/engine/api/v1.35/#operation/ContainerCreate) section of the [Docker Remote API](https://docs.aws.amazon.com/https://docs.docker.com/engine/api/v1.35/) and the `--memory` option to [docker run](https://docs.aws.amazon.com/https://docs.docker.com/engine/reference/run/#security-configuration) .\n\nIf using the Fargate launch type, this parameter is optional.\n\nIf using the EC2 launch type, you must specify either a task-level memory value or a container-level memory value. If you specify both a container-level `memory` and `memoryReservation` value, `memory` must be greater than `memoryReservation` . If you specify `memoryReservation` , then that value is subtracted from the available memory resources for the container instance where the container is placed. Otherwise, the value of `memory` is used.\n\nThe Docker daemon reserves a minimum of 4 MiB of memory for a container. Therefore, we recommend that you specify fewer than 4 MiB of memory for your containers.", + "Memory": "The amount (in MiB) of memory to present to the container. If your container attempts to exceed the memory specified here, the container is killed. The total amount of memory reserved for all containers within a task must be lower than the task `memory` value, if one is specified. This parameter maps to `Memory` in the [Create a container](https://docs.aws.amazon.com/https://docs.docker.com/engine/api/v1.35/#operation/ContainerCreate) section of the [Docker Remote API](https://docs.aws.amazon.com/https://docs.docker.com/engine/api/v1.35/) and the `--memory` option to [docker run](https://docs.aws.amazon.com/https://docs.docker.com/engine/reference/run/#security-configuration) .\n\nIf using the Fargate launch type, this parameter is optional.\n\nIf using the EC2 launch type, you must specify either a task-level memory value or a container-level memory value. If you specify both a container-level `memory` and `memoryReservation` value, `memory` must be greater than `memoryReservation` . If you specify `memoryReservation` , then that value is subtracted from the available memory resources for the container instance where the container is placed. Otherwise, the value of `memory` is used.\n\nThe Docker 20.10.0 or later daemon reserves a minimum of 6 MiB of memory for a container, so you should not specify fewer than 6 MiB of memory for your containers.\n\nThe Docker 19.03.13-ce or earlier daemon reserves a minimum of 4 MiB of memory for a container, so you should not specify fewer than 4 MiB of memory for your containers.", "MemoryReservation": "The soft limit (in MiB) of memory to reserve for the container. When system memory is under heavy contention, Docker attempts to keep the container memory to this soft limit. However, your container can consume more memory when it needs to, up to either the hard limit specified with the `memory` parameter (if applicable), or all of the available memory on the container instance, whichever comes first. This parameter maps to `MemoryReservation` in the [Create a container](https://docs.aws.amazon.com/https://docs.docker.com/engine/api/v1.35/#operation/ContainerCreate) section of the [Docker Remote API](https://docs.aws.amazon.com/https://docs.docker.com/engine/api/v1.35/) and the `--memory-reservation` option to [docker run](https://docs.aws.amazon.com/https://docs.docker.com/engine/reference/run/#security-configuration) .\n\nIf a task-level memory value is not specified, you must specify a non-zero integer for one or both of `memory` or `memoryReservation` in a container definition. If you specify both, `memory` must be greater than `memoryReservation` . If you specify `memoryReservation` , then that value is subtracted from the available memory resources for the container instance where the container is placed. Otherwise, the value of `memory` is used.\n\nFor example, if your container normally uses 128 MiB of memory, but occasionally bursts to 256 MiB of memory for short periods of time, you can set a `memoryReservation` of 128 MiB, and a `memory` hard limit of 300 MiB. This configuration would allow the container to only reserve 128 MiB of memory from the remaining resources on the container instance, but also allow the container to consume more memory resources when needed.\n\nThe Docker daemon reserves a minimum of 4 MiB of memory for a container. Therefore, we recommend that you specify fewer than 4 MiB of memory for your containers.", "MountPoints": "The mount points for data volumes in your container.\n\nThis parameter maps to `Volumes` in the [Create a container](https://docs.aws.amazon.com/https://docs.docker.com/engine/api/v1.35/#operation/ContainerCreate) section of the [Docker Remote API](https://docs.aws.amazon.com/https://docs.docker.com/engine/api/v1.35/) and the `--volume` option to [docker run](https://docs.aws.amazon.com/https://docs.docker.com/engine/reference/run/#security-configuration) .\n\nWindows containers can mount whole directories on the same drive as `$env:ProgramData` . Windows containers can't mount directories on a different drive, and mount point can't be across drives.", "Name": "The name of a container. If you're linking multiple containers together in a task definition, the `name` of one container can be entered in the `links` of another container to connect the containers. Up to 255 letters (uppercase and lowercase), numbers, underscores, and hyphens are allowed. This parameter maps to `name` in the [Create a container](https://docs.aws.amazon.com/https://docs.docker.com/engine/api/v1.35/#operation/ContainerCreate) section of the [Docker Remote API](https://docs.aws.amazon.com/https://docs.docker.com/engine/api/v1.35/) and the `--name` option to [docker run](https://docs.aws.amazon.com/https://docs.docker.com/engine/reference/run/#security-configuration) .", @@ -13798,7 +14136,7 @@ }, "AWS::ECS::TaskDefinition.EphemeralStorage": { "attributes": {}, - "description": "The amount of ephemeral storage to allocate for the task. This parameter is used to expand the total amount of ephemeral storage available, beyond the default amount, for tasks hosted on AWS Fargate . For more information, see [Fargate task storage](https://docs.aws.amazon.com/AmazonECS/latest/userguide/using_data_volumes.html) in the *Amazon ECS User Guide for AWS Fargate* .\n\n> This parameter is only supported for tasks hosted on Fargate using the following platform versions:\n> \n> - Linux platform version `1.4.0` or later.\n> - Windows platform version `1.0.0` or later.", + "description": "The amount of ephemeral storage to allocate for the task. This parameter is used to expand the total amount of ephemeral storage available, beyond the default amount, for tasks hosted on AWS Fargate . For more information, see [Fargate task storage](https://docs.aws.amazon.com/AmazonECS/latest/userguide/using_data_volumes.html) in the *Amazon ECS User Guide for AWS Fargate* .\n\n> This parameter is only supported for tasks hosted on Fargate using Linux platform version `1.4.0` or later. This parameter is not supported for Windows containers on Fargate.", "properties": { "SizeInGiB": "The total amount, in GiB, of ephemeral storage to set for the task. The minimum supported value is `21` GiB and the maximum supported value is `200` GiB." } @@ -13927,9 +14265,9 @@ }, "AWS::ECS::TaskDefinition.RuntimePlatform": { "attributes": {}, - "description": "Information about the platform for the Amazon ECS service or task.", + "description": "Information about the platform for the Amazon ECS service or task.\n\nFor more informataion about `RuntimePlatform` , see [RuntimePlatform](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#runtime-platform) in the *Amazon Elastic Container Service Developer Guide* .", "properties": { - "CpuArchitecture": "The CPU architecture.", + "CpuArchitecture": "The CPU architecture.\n\nYou can run your Linux tasks on an ARM-based platform by setting the value to `ARM64` . This option is avaiable for tasks that run on Linuc Amazon EC2 instance or Linux containers on Fargate.", "OperatingSystemFamily": "The operating system." } }, @@ -13938,7 +14276,7 @@ "description": "The `Secret` property specifies an object representing the secret to expose to your container. For more information, see [Specifying Sensitive Data](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/specifying-sensitive-data.html) in the *Amazon Elastic Container Service Developer Guide* .", "properties": { "Name": "The name of the secret.", - "ValueFrom": "The secret to expose to the container. The supported values are either the full ARN of the AWS Secrets Manager secret or the full ARN of the parameter in the SSM Parameter Store.\n\n> If the SSM Parameter Store parameter exists in the same Region as the task you're launching, then you can use either the full ARN or name of the parameter. If the parameter exists in a different Region, then the full ARN must be specified." + "ValueFrom": "The secret to expose to the container. The supported values are either the full ARN of the AWS Secrets Manager secret or the full ARN of the parameter in the SSM Parameter Store.\n\nFor information about the require AWS Identity and Access Management permissions, see [Required IAM permissions for Amazon ECS secrets](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/specifying-sensitive-data-secrets.html#secrets-iam) (for Secrets Manager) or [Required IAM permissions for Amazon ECS secrets](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/specifying-sensitive-data-parameters.html) (for Systems Manager Parameter store) in the *Amazon Elastic Container Service Developer Guide* .\n\n> If the SSM Parameter Store parameter exists in the same Region as the task you're launching, then you can use either the full ARN or name of the parameter. If the parameter exists in a different Region, then the full ARN must be specified." } }, "AWS::ECS::TaskDefinition.SystemControl": { @@ -14060,13 +14398,13 @@ "attributes": { "AccessPointId": "The ID of the EFS access point.", "Arn": "The Amazon Resource Name (ARN) of the access point.", - "Ref": "`Ref` returns the resource ID. For example:\n\n`{\"Ref\":\"fsap-0123456789abcdef0\"}` .\n\nRef returns the access point ID." + "Ref": "`Ref` returns the AccessPoint ID. For example:\n\n`{\"Ref\":\"access_point-logical_id\"}` returns\n\n`fsap-0123456789abcdef0`" }, "description": "The `AWS::EFS::AccessPoint` resource creates an EFS access point. An access point is an application-specific view into an EFS file system that applies an operating system user and group, and a file system path, to any file system request made through the access point. The operating system user and group override any identity information provided by the NFS client. The file system path is exposed as the access point's root directory. Applications using the access point can only access data in its own directory and below. To learn more, see [Mounting a file system using EFS access points](https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html) .\n\nThis operation requires permissions for the `elasticfilesystem:CreateAccessPoint` action.", "properties": { "AccessPointTags": "An array of key-value pairs to apply to this resource.\n\nFor more information, see [Tag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) .", "ClientToken": "The opaque string specified in the request to ensure idempotent creation.", - "FileSystemId": "The ID of the EFS file system that the access point applies to.", + "FileSystemId": "The ID of the EFS file system that the access point applies to. Accepts only the ID format for input when specifying a file system, for example `fs-0123456789abcedf2` .", "PosixUser": "The full POSIX identity, including the user ID, group ID, and secondary group IDs on the access point that is used for all file operations by NFS clients using the access point.", "RootDirectory": "The directory on the Amazon EFS file system that the access point exposes as the root directory to NFS clients using the access point." } @@ -14107,15 +14445,15 @@ }, "AWS::EFS::FileSystem": { "attributes": { - "Arn": "The Amazon Resource Name (ARN) of the EFS file system. For example: `arn:aws:elasticfilesystem:us-west-2:1111333322228888:file-system/fs-12345678`", + "Arn": "The Amazon Resource Name (ARN) of the EFS file system.\n\nExample: `arn:aws:elasticfilesystem:us-west-2:1111333322228888:file-system/fs-0123456789abcdef8`", "FileSystemId": "The ID of the EFS file system. For example: `fs-12345678`", - "Ref": "`Ref` returns the resource ID. For example:\n\n`{\"Ref\":\"fs-12345678\"}` .\n\nFor the Amazon EFS file system `fs-12345678` , Ref returns the file system ID." + "Ref": "`Ref` returns the FileSystem ID. For example:\n\n`{\"Ref\":\"file_system-logical_id\"}` returns\n\n`fs-0123456789abcdef2`" }, "description": "The `AWS::EFS::FileSystem` resource creates a new, empty file system in Amazon Elastic File System ( Amazon EFS ). You must create a mount target ( [AWS::EFS::MountTarget](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-efs-mounttarget.html) ) to mount your EFS file system on an Amazon EC2 or other AWS cloud compute resource.", "properties": { "AvailabilityZoneName": "Used to create a file system that uses One Zone storage classes. It specifies the AWS Availability Zone in which to create the file system. Use the format `us-east-1a` to specify the Availability Zone. For more information about One Zone storage classes, see [Using EFS storage classes](https://docs.aws.amazon.com/efs/latest/ug/storage-classes.html) in the *Amazon EFS User Guide* .\n\n> One Zone storage classes are not available in all Availability Zones in AWS Regions where Amazon EFS is available.", "BackupPolicy": "Use the `BackupPolicy` to turn automatic backups on or off for the file system.", - "BypassPolicyLockoutSafetyCheck": "", + "BypassPolicyLockoutSafetyCheck": "(Optional) Use this boolean to use or bypass the `FileSystemPolicy` lockout safety check. The policy lockout safety check determines if the `FileSystemPolicy` in the request will lock out the IAM principal making the request, preventing them from making future `PutFileSystemPolicy` requests on the file system. Set `BypassPolicyLockoutSafetyCheck` to `True` only when you intend to prevent the IAM principal that is making the request from making a subsequent `PutFileSystemPolicy` request on the file system. The default value is `False` .", "Encrypted": "A Boolean value that, if true, creates an encrypted file system. When creating an encrypted file system, you have the option of specifying a KmsKeyId for an existing AWS KMS key . If you don't specify a KMS key , then the default KMS key for Amazon EFS , `/aws/elasticfilesystem` , is used to protect the encrypted file system.", "FileSystemPolicy": "The `FileSystemPolicy` for the EFS file system. A file system policy is an IAM resource policy used to control NFS access to an EFS file system. For more information, see [Using IAM to control NFS access to Amazon EFS](https://docs.aws.amazon.com/efs/latest/ug/iam-access-control-nfs-efs.html) in the *Amazon EFS User Guide* .", "FileSystemTags": "Use to create one or more tags associated with the file system. Each tag is a user-defined key-value pair. Name your file system on creation by including a `\"Key\":\"Name\",\"Value\":\"{value}\"` key-value pair. Each key must be unique. For more information, see [Tagging AWS resources](https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html) in the *AWS General Reference Guide* .", @@ -14151,9 +14489,9 @@ }, "AWS::EFS::MountTarget": { "attributes": { - "Id": "", - "IpAddress": "The IPv4 address of the mount target.", - "Ref": "`Ref` returns the resource ID. For example:\n\n`{\"Ref\":\"fsmt-12345678\"}` .\n\nFor the Amazon EFS file system mount target `fsmt-12345678` , Ref returns the mount target ID." + "Id": "The ID of the Amazon EFS file system that the mount target provides access to.\n\nExample: `fs-0123456789111222a`", + "IpAddress": "The IPv4 address of the mount target.\n\nExample: 192.0.2.0", + "Ref": "`Ref` returns the MountTarget ID. For example:\n\n`{\"Ref\":\"logical_mount_target_id\"}` returns\n\n`fsmt-0123456789abcdef8` ." }, "description": "The `AWS::EFS::MountTarget` resource is an Amazon EFS resource that creates a mount target for an EFS file system. You can then mount the file system on Amazon EC2 instances or other resources by using the mount target.", "properties": { @@ -14185,7 +14523,7 @@ "ClusterSecurityGroupId": "The cluster security group that was created by Amazon EKS for the cluster. Managed node groups use this security group for control plane to data plane communication.\n\nThis parameter is only returned by Amazon EKS clusters that support managed node groups. For more information, see [Managed node groups](https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html) in the *Amazon EKS User Guide* .", "EncryptionConfigKeyArn": "Amazon Resource Name (ARN) or alias of the customer master key (CMK).", "Endpoint": "The endpoint for your Kubernetes API server, such as `https://5E1D0CEXAMPLEA591B746AFC5AB30262.yl4.us-west-2.eks.amazonaws.com` .", - "KubernetesNetworkConfig.ServiceIpv6Cidr": "", + "KubernetesNetworkConfig.ServiceIpv6Cidr": "The CIDR block that Kubernetes Service IP addresses are assigned from if you created a 1.21 or later cluster with version 1.10.1 or later of the Amazon VPC CNI add-on and specified `ipv6` for *ipFamily* when you created the cluster. Kubernetes assigns Service addresses from the unique local address range ( `fc00::/7` ) because you can't specify a custom IPv6 CIDR block when you create the cluster.", "OpenIdConnectIssuerUrl": "The issuer URL for the OIDC identity provider of the cluster, such as `https://oidc.eks.us-west-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E` . If you need to remove `https://` from this output value, you can include the following code in your template.\n\n`!Select [1, !Split [\"//\", !GetAtt EKSCluster.OpenIdConnectIssuerUrl]]`", "Ref": "`Ref` returns the resource name. For example:\n\n`{ \"Ref\": \"myCluster\" }`\n\nFor the Amazon EKS cluster `myCluster` , `Ref` returns the name of the cluster." }, @@ -14220,9 +14558,9 @@ "attributes": {}, "description": "The Kubernetes network configuration for the cluster.", "properties": { - "IpFamily": "", - "ServiceIpv4Cidr": "The CIDR block to assign Kubernetes service IP addresses from. If you don't specify a block, Kubernetes assigns addresses from either the 10.100.0.0/16 or 172.20.0.0/16 CIDR blocks. We recommend that you specify a block that does not overlap with resources in other networks that are peered or connected to your VPC. The block must meet the following requirements:\n\n- Within one of the following private IP address blocks: 10.0.0.0/8, 172.16.0.0/12, or 192.168.0.0/16.\n- Doesn't overlap with any CIDR block assigned to the VPC that you selected for VPC.\n- Between /24 and /12.\n\n> You can only specify a custom CIDR block when you create a cluster and can't change this value once the cluster is created.", - "ServiceIpv6Cidr": "" + "IpFamily": "Specify which IP family is used to assign Kubernetes pod and service IP addresses. If you don't specify a value, `ipv4` is used by default. You can only specify an IP family when you create a cluster and can't change this value once the cluster is created. If you specify `ipv6` , the VPC and subnets that you specify for cluster creation must have both IPv4 and IPv6 CIDR blocks assigned to them. You can't specify `ipv6` for clusters in China Regions.\n\nYou can only specify `ipv6` for 1.21 and later clusters that use version 1.10.1 or later of the Amazon VPC CNI add-on. If you specify `ipv6` , then ensure that your VPC meets the requirements listed in the considerations listed in [Assigning IPv6 addresses to pods and services](https://docs.aws.amazon.com/eks/latest/userguide/cni-ipv6.html) in the Amazon EKS User Guide. Kubernetes assigns services IPv6 addresses from the unique local address range (fc00::/7). You can't specify a custom IPv6 CIDR block. Pod addresses are assigned from the subnet's IPv6 CIDR.", + "ServiceIpv4Cidr": "Don't specify a value if you select `ipv6` for *ipFamily* . The CIDR block to assign Kubernetes service IP addresses from. If you don't specify a block, Kubernetes assigns addresses from either the 10.100.0.0/16 or 172.20.0.0/16 CIDR blocks. We recommend that you specify a block that does not overlap with resources in other networks that are peered or connected to your VPC. The block must meet the following requirements:\n\n- Within one of the following private IP address blocks: 10.0.0.0/8, 172.16.0.0/12, or 192.168.0.0/16.\n- Doesn't overlap with any CIDR block assigned to the VPC that you selected for VPC.\n- Between /24 and /12.\n\n> You can only specify a custom CIDR block when you create a cluster and can't change this value once the cluster is created.", + "ServiceIpv6Cidr": "The CIDR block that Kubernetes pod and service IP addresses are assigned from if you created a 1.21 or later cluster with version 1.10.1 or later of the Amazon VPC CNI add-on and specified `ipv6` for *ipFamily* when you created the cluster. Kubernetes assigns service addresses from the unique local address range ( `fc00::/7` ) because you can't specify a custom IPv6 CIDR block when you create the cluster." } }, "AWS::EKS::Cluster.Logging": { @@ -14285,12 +14623,13 @@ "attributes": { "Arn": "The Amazon Resource Name (ARN) associated with the managed node group.", "ClusterName": "The name of the cluster that the managed node group resides in.", + "Id": "", "NodegroupName": "The name associated with an Amazon EKS managed node group.", "Ref": "`Ref` returns the resource name. For example:\n\n`{ \"Ref\": \"myNodegroup\" }`\n\nFor the Amazon EKS node group `myNodegroup` , Ref returns the physical resource ID of the node group. For example, `/` ." }, - "description": "Creates a managed node group for an Amazon EKS cluster. You can only create a node group for your cluster that is equal to the current Kubernetes version for the cluster.\n\nAn Amazon EKS managed node group is an Amazon EC2 Auto Scaling group and associated Amazon EC2 instances that are managed by AWS for an Amazon EKS cluster. Each node group uses a version of the Amazon EKS optimized Amazon Linux 2 AMI. For more information, see [Managed Node Groups](https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html) in the *Amazon EKS User Guide* .", + "description": "Creates a managed node group for an Amazon EKS cluster. You can only create a node group for your cluster that is equal to the current Kubernetes version for the cluster. All node groups are created with the latest AMI release version for the respective minor Kubernetes version of the cluster, unless you deploy a custom AMI using a launch template. For more information about using launch templates, see [Launch template support](https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html) .\n\nAn Amazon EKS managed node group is an Amazon EC2 Auto Scaling group and associated Amazon EC2 instances that are managed by AWS for an Amazon EKS cluster. Each node group uses a version of the Amazon EKS optimized Amazon Linux 2 AMI. For more information, see [Managed Node Groups](https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html) in the *Amazon EKS User Guide* .", "properties": { - "AmiType": "The AMI type for your node group. The following values are examples:\n\n- `AL2_x86_64` \u2013 Use for Amazon Linux 2 non-GPU instances.\n- `AL2_x86_64_GPU` \u2013 Use for Amazon Linux 2 GPU instances.\n- `AL2_ARM_64` \u2013 Use for Amazon Linux 2 Arm instances.\n- `CUSTOM` \u2013 Use when specifying a custom AMI ID with a launch template. For more information, see [Specifying an AMI](https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html#launch-template-custom-ami) in the *Amazon EKS User Guide* .\n- `BOTTLEROCKET_ARM_64` \u2013 Use for Bottlerocket Arm instances.\n- `BOTTLEROCKET_x86_64` \u2013 Use for Bottlerocket x86_64 instances.\n\nIf you specify `launchTemplate` , and your launch template uses a custom AMI, then don't specify `amiType` , or the node group deployment will fail. For more information about using launch templates with Amazon EKS, see [Launch template support](https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html) in the *Amazon EKS User Guide* .", + "AmiType": "The AMI type for your node group. GPU instance types should use the `AL2_x86_64_GPU` AMI type. Non-GPU instances should use the `AL2_x86_64` AMI type. Arm instances should use the `AL2_ARM_64` AMI type. All types use the Amazon EKS optimized Amazon Linux 2 AMI. If you specify `launchTemplate` , and your launch template uses a custom AMI, then don't specify `amiType` , or the node group deployment will fail. For more information about using launch templates with Amazon EKS, see [Launch template support](https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html) in the *Amazon EKS User Guide* .", "CapacityType": "The capacity type of your managed node group.", "ClusterName": "The name of the cluster to create the node group in.", "DiskSize": "The root device disk size (in GiB) for your node group instances. The default disk size is 20 GiB. If you specify `launchTemplate` , then don't specify `diskSize` , or the node group deployment will fail. For more information about using launch templates with Amazon EKS, see [Launch template support](https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html) in the *Amazon EKS User Guide* .", @@ -14298,14 +14637,14 @@ "InstanceTypes": "Specify the instance types for a node group. If you specify a GPU instance type, be sure to specify `AL2_x86_64_GPU` with the `amiType` parameter. If you specify `launchTemplate` , then you can specify zero or one instance type in your launch template *or* you can specify 0-20 instance types for `instanceTypes` . If however, you specify an instance type in your launch template *and* specify any `instanceTypes` , the node group deployment will fail. If you don't specify an instance type in a launch template or for `instanceTypes` , then `t3.medium` is used, by default. If you specify `Spot` for `capacityType` , then we recommend specifying multiple values for `instanceTypes` . For more information, see [Managed node group capacity types](https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html#managed-node-group-capacity-types) and [Launch template support](https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html) in the *Amazon EKS User Guide* .", "Labels": "The Kubernetes labels to be applied to the nodes in the node group when they are created.", "LaunchTemplate": "An object representing a node group's launch template specification. If specified, then do not specify `instanceTypes` , `diskSize` , or `remoteAccess` and make sure that the launch template meets the requirements in `launchTemplateSpecification` .", - "NodeRole": "The Amazon Resource Name (ARN) of the IAM role to associate with your node group. The Amazon EKS worker node `kubelet` daemon makes calls to AWS APIs on your behalf. Nodes receive permissions for these API calls through an IAM instance profile and associated policies. Before you can launch nodes and register them into a cluster, you must create an IAM role for those nodes to use when they are launched. For more information, see [Amazon EKS node IAM role](https://docs.aws.amazon.com/eks/latest/userguide/worker_node_IAM_role.html) in the **Amazon EKS User Guide** . If you specify `launchTemplate` , then don't specify [`IamInstanceProfile`](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_IamInstanceProfile.html) in your launch template, or the node group deployment will fail. For more information about using launch templates with Amazon EKS, see [Launch template support](https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html) in the *Amazon EKS User Guide* .", + "NodeRole": "The Amazon Resource Name (ARN) of the IAM role to associate with your node group. The Amazon EKS worker node `kubelet` daemon makes calls to AWS APIs on your behalf. Nodes receive permissions for these API calls through an IAM instance profile and associated policies. Before you can launch nodes and register them into a cluster, you must create an IAM role for those nodes to use when they are launched. For more information, see [Amazon EKS node IAM role](https://docs.aws.amazon.com/eks/latest/userguide/create-node-role.html) in the **Amazon EKS User Guide** . If you specify `launchTemplate` , then don't specify [`IamInstanceProfile`](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_IamInstanceProfile.html) in your launch template, or the node group deployment will fail. For more information about using launch templates with Amazon EKS, see [Launch template support](https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html) in the *Amazon EKS User Guide* .", "NodegroupName": "The unique name to give your node group.", "ReleaseVersion": "The AMI version of the Amazon EKS optimized AMI to use with your node group (for example, `1.14.7- *YYYYMMDD*` ). By default, the latest available AMI version for the node group's current Kubernetes version is used. For more information, see [Amazon EKS optimized Linux AMI Versions](https://docs.aws.amazon.com/eks/latest/userguide/eks-linux-ami-versions.html) in the *Amazon EKS User Guide* .\n\n> Changing this value triggers an update of the node group if one is available. However, only the latest available AMI release version is valid as an input. You cannot roll back to a previous AMI release version.", "RemoteAccess": "The remote access (SSH) configuration to use with your node group. If you specify `launchTemplate` , then don't specify `remoteAccess` , or the node group deployment will fail. For more information about using launch templates with Amazon EKS, see [Launch template support](https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html) in the *Amazon EKS User Guide* .", "ScalingConfig": "The scaling configuration details for the Auto Scaling group that is created for your node group.", "Subnets": "The subnets to use for the Auto Scaling group that is created for your node group. If you specify `launchTemplate` , then don't specify [`SubnetId`](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateNetworkInterface.html) in your launch template, or the node group deployment will fail. For more information about using launch templates with Amazon EKS, see [Launch template support](https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html) in the *Amazon EKS User Guide* .", "Tags": "The metadata to apply to the node group to assist with categorization and organization. Each tag consists of a key and an optional value. You define both. Node group tags do not propagate to any other resources associated with the node group, such as the Amazon EC2 instances or subnets.", - "Taints": "The Kubernetes taints to be applied to the nodes in the node group when they are created. Effect is one of `No_Schedule` , `Prefer_No_Schedule` , or `No_Execute` . Kubernetes taints can be used together with tolerations to control how workloads are scheduled to your nodes.", + "Taints": "The Kubernetes taints to be applied to the nodes in the node group when they are created. Effect is one of `No_Schedule` , `Prefer_No_Schedule` , or `No_Execute` . Kubernetes taints can be used together with tolerations to control how workloads are scheduled to your nodes. For more information, see [Node taints on managed node groups](https://docs.aws.amazon.com/eks/latest/userguide/node-taints-managed-node-groups.html) .", "UpdateConfig": "The node group update configuration.", "Version": "The Kubernetes version to use for your managed nodes. By default, the Kubernetes version of the cluster is used, and this is the only accepted specified value. If you specify `launchTemplate` , and your launch template uses a custom AMI, then don't specify `version` , or the node group deployment will fail. For more information about using launch templates with Amazon EKS, see [Launch template support](https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html) in the *Amazon EKS User Guide* ." } @@ -14338,7 +14677,7 @@ }, "AWS::EKS::Nodegroup.Taint": { "attributes": {}, - "description": "A property that allows a node to repel a set of pods.", + "description": "A property that allows a node to repel a set of pods. For more information, see [Node taints on managed node groups](https://docs.aws.amazon.com/eks/latest/userguide/node-taints-managed-node-groups.html) .", "properties": { "Effect": "The effect of the taint.", "Key": "The key of the taint.", @@ -14663,9 +15002,9 @@ "attributes": {}, "description": "`VolumeSpecification` is a subproperty of the `EbsBlockDeviceConfig` property type. `VolumeSecification` determines the volume type, IOPS, and size (GiB) for EBS volumes attached to EC2 instances.", "properties": { - "Iops": "The number of I/O operations per second (IOPS) that the volume supports. IOPS parameters are supported for volumes: io1 and gp3. Among them, IOPS parameters are required for volumes io1 but optional for volumes gp3 which default to 3000 IOPS. IOPS parameters are not supported for volumes: gp2, standard, st1 and sc1.", + "Iops": "The number of I/O operations per second (IOPS) that the volume supports.", "SizeInGB": "The volume size, in gibibytes (GiB). This can be a number from 1 - 1024. If the volume type is EBS-optimized, the minimum value is 10.", - "VolumeType": "The volume type. Volume types supported are gp2, io1, standard sc1, st1 and gp3. For gp3, customer will be able to configure IOPs but not throughput. Throughput will default to 125 MiB/s." + "VolumeType": "The volume type. Volume types supported are gp2, io1, and standard." } }, "AWS::EMR::InstanceFleetConfig": { @@ -14750,9 +15089,9 @@ "attributes": {}, "description": "`VolumeSpecification` is a subproperty of the `EbsBlockDeviceConfig` property type. `VolumeSecification` determines the volume type, IOPS, and size (GiB) for EBS volumes attached to EC2 instances.", "properties": { - "Iops": "The number of I/O operations per second (IOPS) that the volume supports. IOPS parameters are supported for volumes: io1 and gp3. Among them, IOPS parameters are required for volumes io1 but optional for volumes gp3 which default to 3000 IOPS. IOPS parameters are not supported for volumes: gp2, standard, st1 and sc1.", + "Iops": "The number of I/O operations per second (IOPS) that the volume supports.", "SizeInGB": "The volume size, in gibibytes (GiB). This can be a number from 1 - 1024. If the volume type is EBS-optimized, the minimum value is 10.", - "VolumeType": "The volume type. Volume types supported are gp2, io1, standard sc1, st1 and gp3. For gp3, customer will be able to configure IOPs but not throughput. Throughput will default to 125 MiB/s." + "VolumeType": "The volume type. Volume types supported are gp2, io1, and standard." } }, "AWS::EMR::InstanceGroupConfig": { @@ -14876,9 +15215,9 @@ "attributes": {}, "description": "`VolumeSpecification` is a subproperty of the `EbsBlockDeviceConfig` property type. `VolumeSecification` determines the volume type, IOPS, and size (GiB) for EBS volumes attached to EC2 instances.", "properties": { - "Iops": "The number of I/O operations per second (IOPS) that the volume supports. IOPS parameters are supported for volumes: io1 and gp3. Among them, IOPS parameters are required for volumes io1 but optional for volumes gp3 which default to 3000 IOPS. IOPS parameters are not supported for volumes: gp2, standard, st1 and sc1.", + "Iops": "The number of I/O operations per second (IOPS) that the volume supports.", "SizeInGB": "The volume size, in gibibytes (GiB). This can be a number from 1 - 1024. If the volume type is EBS-optimized, the minimum value is 10.", - "VolumeType": "The volume type. Volume types supported are gp2, io1, standard sc1, st1 and gp3. For gp3, customer will be able to configure IOPs but not throughput. Throughput will default to 125 MiB/s." + "VolumeType": "The volume type. Volume types supported are gp2, io1, and standard." } }, "AWS::EMR::SecurityConfiguration": { @@ -15003,7 +15342,7 @@ "properties": { "AZMode": "Specifies whether the nodes in this Memcached cluster are created in a single Availability Zone or created across multiple Availability Zones in the cluster's region.\n\nThis parameter is only supported for Memcached clusters.\n\nIf the `AZMode` and `PreferredAvailabilityZones` are not specified, ElastiCache assumes `single-az` mode.", "AutoMinorVersionUpgrade": "If you are running Redis engine version 6.0 or later, set this parameter to yes if you want to opt-in to the next minor version upgrade campaign. This parameter is disabled for previous versions.", - "CacheNodeType": "The compute and memory capacity of the nodes in the node group (shard).\n\nThe following node types are supported by ElastiCache. Generally speaking, the current generation types provide more memory and computational power at lower cost when compared to their equivalent previous generation counterparts. Changing the CacheNodeType of a Memcached instance is currently not supported. If you need to scale using Memcached, we recommend forcing a replacement update by changing the `LogicalResourceId` of the resource.\n\n- General purpose:\n\n- Current generation:\n\n*M6g node types:* `cache.m6g.large` , `cache.m6g.xlarge` , `cache.m6g.2xlarge` , `cache.m6g.4xlarge` , `cache.m6g.12xlarge` , `cache.m6g.24xlarge`\n\n*M5 node types:* `cache.m5.large` , `cache.m5.xlarge` , `cache.m5.2xlarge` , `cache.m5.4xlarge` , `cache.m5.12xlarge` , `cache.m5.24xlarge`\n\n*M4 node types:* `cache.m4.large` , `cache.m4.xlarge` , `cache.m4.2xlarge` , `cache.m4.4xlarge` , `cache.m4.10xlarge`\n\n*T4g node types:* `cache.t4g.micro` , `cache.t4g.small` , `cache.t4g.medium`\n\n*T3 node types:* `cache.t3.micro` , `cache.t3.small` , `cache.t3.medium`\n\n*T2 node types:* `cache.t2.micro` , `cache.t2.small` , `cache.t2.medium`\n- Previous generation: (not recommended)\n\n*T1 node types:* `cache.t1.micro`\n\n*M1 node types:* `cache.m1.small` , `cache.m1.medium` , `cache.m1.large` , `cache.m1.xlarge`\n\n*M3 node types:* `cache.m3.medium` , `cache.m3.large` , `cache.m3.xlarge` , `cache.m3.2xlarge`\n- Compute optimized:\n\n- Previous generation: (not recommended)\n\n*C1 node types:* `cache.c1.xlarge`\n- Memory optimized:\n\n- Current generation:\n\n*R6gd node types:* `cache.r6gd.xlarge` , `cache.r6gd.2xlarge` , `cache.r6gd.4xlarge` , `cache.r6gd.8xlarge` , `cache.r6gd.12xlarge` , `cache.r6gd.16xlarge`\n\n> The `r6gd` family is available in the following regions: `us-east-2` , `us-east-1` , `us-west-2` , `us-west-1` , `eu-west-1` , `eu-central-1` , `ap-northeast-1` , `ap-southeast-1` , `ap-southeast-2` . \n\n*R6g node types:* `cache.r6g.large` , `cache.r6g.xlarge` , `cache.r6g.2xlarge` , `cache.r6g.4xlarge` , `cache.r6g.12xlarge` , `cache.r6g.24xlarge`\n\n*R5 node types:* `cache.r5.large` , `cache.r5.xlarge` , `cache.r5.2xlarge` , `cache.r5.4xlarge` , `cache.r5.12xlarge` , `cache.r5.24xlarge`\n\n*R4 node types:* `cache.r4.large` , `cache.r4.xlarge` , `cache.r4.2xlarge` , `cache.r4.4xlarge` , `cache.r4.8xlarge` , `cache.r4.16xlarge`\n- Previous generation: (not recommended)\n\n*M2 node types:* `cache.m2.xlarge` , `cache.m2.2xlarge` , `cache.m2.4xlarge`\n\n*R3 node types:* `cache.r3.large` , `cache.r3.xlarge` , `cache.r3.2xlarge` , `cache.r3.4xlarge` , `cache.r3.8xlarge`\n\nFor region availability, see [Supported Node Types by Amazon Region](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/CacheNodes.SupportedTypes.html#CacheNodes.SupportedTypesByRegion)\n\n*Additional node type info*\n\n- All current generation instance types are created in Amazon VPC by default.\n- Redis append-only files (AOF) are not supported for T1 or T2 instances.\n- Redis Multi-AZ with automatic failover is not supported on T1 instances.\n- Redis configuration variables `appendonly` and `appendfsync` are not supported on Redis version 2.8.22 and later.", + "CacheNodeType": "The compute and memory capacity of the nodes in the node group (shard).\n\nThe following node types are supported by ElastiCache. Generally speaking, the current generation types provide more memory and computational power at lower cost when compared to their equivalent previous generation counterparts. Changing the CacheNodeType of a Memcached instance is currently not supported. If you need to scale using Memcached, we recommend forcing a replacement update by changing the `LogicalResourceId` of the resource.\n\n- General purpose:\n\n- Current generation:\n\n*M6g node types:* `cache.m6g.large` , `cache.m6g.xlarge` , `cache.m6g.2xlarge` , `cache.m6g.4xlarge` , `cache.m6g.8xlarge` , `cache.m6g.12xlarge` , `cache.m6g.16xlarge` , `cache.m6g.24xlarge`\n\n*M5 node types:* `cache.m5.large` , `cache.m5.xlarge` , `cache.m5.2xlarge` , `cache.m5.4xlarge` , `cache.m5.12xlarge` , `cache.m5.24xlarge`\n\n*M4 node types:* `cache.m4.large` , `cache.m4.xlarge` , `cache.m4.2xlarge` , `cache.m4.4xlarge` , `cache.m4.10xlarge`\n\n*T4g node types:* `cache.t4g.micro` , `cache.t4g.small` , `cache.t4g.medium`\n\n*T3 node types:* `cache.t3.micro` , `cache.t3.small` , `cache.t3.medium`\n\n*T2 node types:* `cache.t2.micro` , `cache.t2.small` , `cache.t2.medium`\n- Previous generation: (not recommended)\n\n*T1 node types:* `cache.t1.micro`\n\n*M1 node types:* `cache.m1.small` , `cache.m1.medium` , `cache.m1.large` , `cache.m1.xlarge`\n\n*M3 node types:* `cache.m3.medium` , `cache.m3.large` , `cache.m3.xlarge` , `cache.m3.2xlarge`\n- Compute optimized:\n\n- Previous generation: (not recommended)\n\n*C1 node types:* `cache.c1.xlarge`\n- Memory optimized:\n\n- Current generation:\n\n*R6gd node types:* `cache.r6gd.xlarge` , `cache.r6gd.2xlarge` , `cache.r6gd.4xlarge` , `cache.r6gd.8xlarge` , `cache.r6gd.12xlarge` , `cache.r6gd.16xlarge`\n\n> The `r6gd` family is available in the following regions: `us-east-2` , `us-east-1` , `us-west-2` , `us-west-1` , `eu-west-1` , `eu-central-1` , `ap-northeast-1` , `ap-southeast-1` , `ap-southeast-2` . \n\n*R6g node types:* `cache.r6g.large` , `cache.r6g.xlarge` , `cache.r6g.2xlarge` , `cache.r6g.4xlarge` , `cache.r6g.8xlarge` , `cache.r6g.12xlarge` , `cache.r6g.16xlarge` , `cache.r6g.24xlarge`\n\n*R5 node types:* `cache.r5.large` , `cache.r5.xlarge` , `cache.r5.2xlarge` , `cache.r5.4xlarge` , `cache.r5.12xlarge` , `cache.r5.24xlarge`\n\n*R4 node types:* `cache.r4.large` , `cache.r4.xlarge` , `cache.r4.2xlarge` , `cache.r4.4xlarge` , `cache.r4.8xlarge` , `cache.r4.16xlarge`\n- Previous generation: (not recommended)\n\n*M2 node types:* `cache.m2.xlarge` , `cache.m2.2xlarge` , `cache.m2.4xlarge`\n\n*R3 node types:* `cache.r3.large` , `cache.r3.xlarge` , `cache.r3.2xlarge` , `cache.r3.4xlarge` , `cache.r3.8xlarge`\n\nFor region availability, see [Supported Node Types by Amazon Region](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/CacheNodes.SupportedTypes.html#CacheNodes.SupportedTypesByRegion)\n\n*Additional node type info*\n\n- All current generation instance types are created in Amazon VPC by default.\n- Redis append-only files (AOF) are not supported for T1 or T2 instances.\n- Redis Multi-AZ with automatic failover is not supported on T1 instances.\n- Redis configuration variables `appendonly` and `appendfsync` are not supported on Redis version 2.8.22 and later.", "CacheParameterGroupName": "The name of the parameter group to associate with this cluster. If this argument is omitted, the default parameter group for the specified engine is used. You cannot use any parameter group which has `cluster-enabled='yes'` when creating a cluster.", "CacheSecurityGroupNames": "A list of security group names to associate with this cluster.\n\nUse this parameter only when you are creating a cluster outside of an Amazon Virtual Private Cloud (Amazon VPC).", "CacheSubnetGroupName": "The name of the subnet group to be used for the cluster.\n\nUse this parameter only when you are creating a cluster in an Amazon Virtual Private Cloud (Amazon VPC).\n\n> If you're going to launch your cluster in an Amazon VPC, you need to create a subnet group before you start creating a cluster. For more information, see [AWS::ElastiCache::SubnetGroup](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticache-subnetgroup.html) .", @@ -15054,7 +15393,7 @@ "DestinationDetails": "Configuration details of either a CloudWatch Logs destination or Kinesis Data Firehose destination.", "DestinationType": "Specify either CloudWatch Logs or Kinesis Data Firehose as the destination type. Valid values are either `cloudwatch-logs` or `kinesis-firehose` .", "LogFormat": "Valid values are either `json` or `text` .", - "LogType": "Valid value is `slow-log` . Refers to [slow-log](https://docs.aws.amazon.com/https://redis.io/commands/slowlog) ." + "LogType": "Valid value is either `slow-log` , which refers to [slow-log](https://docs.aws.amazon.com/https://redis.io/commands/slowlog) or `engine-log` ." } }, "AWS::ElastiCache::GlobalReplicationGroup": { @@ -15082,7 +15421,7 @@ "properties": { "ReplicationGroupId": "The replication group id of the Global datastore member.", "ReplicationGroupRegion": "The Amazon region of the Global datastore member.", - "Role": "Indicates the role of the replication group, primary or secondary." + "Role": "Indicates the role of the replication group, `PRIMARY` or `SECONDARY` ." } }, "AWS::ElastiCache::GlobalReplicationGroup.RegionalConfiguration": { @@ -15163,7 +15502,7 @@ "SnapshotWindow": "The daily time range (in UTC) during which ElastiCache begins taking a daily snapshot of your node group (shard).\n\nExample: `05:00-09:00`\n\nIf you do not specify this parameter, ElastiCache automatically chooses an appropriate time range.", "SnapshottingClusterId": "The cluster ID that is used as the daily snapshot source for the replication group. This parameter cannot be set for Redis (cluster mode enabled) replication groups.", "Tags": "A list of tags to be added to this resource. Tags are comma-separated key,value pairs (e.g. Key= `myKey` , Value= `myKeyValue` . You can include multiple tags as shown following: Key= `myKey` , Value= `myKeyValue` Key= `mySecondKey` , Value= `mySecondKeyValue` . Tags on replication groups will be replicated to all nodes.", - "TransitEncryptionEnabled": "A flag that enables in-transit encryption when set to `true` .\n\nYou cannot modify the value of `TransitEncryptionEnabled` after the cluster is created. To enable in-transit encryption on a cluster you must set `TransitEncryptionEnabled` to `true` when you create a cluster.\n\nThis parameter is valid only if the `Engine` parameter is `redis` , the `EngineVersion` parameter is `3.2.6` or `4.x` or `5.x` , and the cluster is being created in an Amazon VPC.\n\nIf you enable in-transit encryption, you must also specify a value for `CacheSubnetGroup` .\n\n*Required:* Only available when creating a replication group in an Amazon VPC using redis version `3.2.6` or `4.x` onward.\n\nDefault: `false`\n\n> For HIPAA compliance, you must specify `TransitEncryptionEnabled` as `true` , an `AuthToken` , and a `CacheSubnetGroup` .", + "TransitEncryptionEnabled": "A flag that enables in-transit encryption when set to `true` .\n\nYou cannot modify the value of `TransitEncryptionEnabled` after the cluster is created. To enable in-transit encryption on a cluster you must set `TransitEncryptionEnabled` to `true` when you create a cluster.\n\nThis parameter is valid only if the `Engine` parameter is `redis` , the `EngineVersion` parameter is `3.2.6` or `4.x` onward, and the cluster is being created in an Amazon VPC.\n\nIf you enable in-transit encryption, you must also specify a value for `CacheSubnetGroup` .\n\n*Required:* Only available when creating a replication group in an Amazon VPC using redis version `3.2.6` or `4.x` onward.\n\nDefault: `false`\n\n> For HIPAA compliance, you must specify `TransitEncryptionEnabled` as `true` , an `AuthToken` , and a `CacheSubnetGroup` .", "UserGroupIds": "The list of user groups to associate with the replication group." } }, @@ -15178,7 +15517,7 @@ "attributes": {}, "description": "Configuration details of either a CloudWatch Logs destination or Kinesis Data Firehose destination.", "properties": { - "CloudWatchLogsDetails": "", + "CloudWatchLogsDetails": "The configuration details of the CloudWatch Logs destination. Note that this field is marked as required but only if CloudWatch Logs was chosen as the destination.", "KinesisFirehoseDetails": "The configuration details of the Kinesis Data Firehose destination. Note that this field is marked as required but only if Kinesis Data Firehose was chosen as the destination." } }, @@ -15196,7 +15535,7 @@ "DestinationDetails": "Configuration details of either a CloudWatch Logs destination or Kinesis Data Firehose destination.", "DestinationType": "Specify either CloudWatch Logs or Kinesis Data Firehose as the destination type. Valid values are either `cloudwatch-logs` or `kinesis-firehose` .", "LogFormat": "Valid values are either `json` or `text` .", - "LogType": "Valid value is `slow-log` . Refers to [slow-log](https://docs.aws.amazon.com/https://redis.io/commands/slowlog) ." + "LogType": "Valid value is either `slow-log` , which refers to [slow-log](https://docs.aws.amazon.com/https://redis.io/commands/slowlog) or `engine-log` ." } }, "AWS::ElastiCache::ReplicationGroup.NodeGroupConfiguration": { @@ -15861,7 +16200,7 @@ "description": "Specifies a target group for a load balancer.\n\nBefore you register a Lambda function as a target, you must create a `AWS::Lambda::Permission` resource that grants the Elastic Load Balancing service principal permission to invoke the Lambda function.", "properties": { "HealthCheckEnabled": "Indicates whether health checks are enabled. If the target type is `lambda` , health checks are disabled by default but can be enabled. If the target type is `instance` , `ip` , or `alb` , health checks are always enabled and cannot be disabled.", - "HealthCheckIntervalSeconds": "The approximate amount of time, in seconds, between health checks of an individual target. If the target group protocol is TCP, TLS, UDP, or TCP_UDP, the supported values are 10 and 30 seconds. If the target group protocol is HTTP or HTTPS, the default is 30 seconds. If the target group protocol is GENEVE, the default is 10 seconds. If the target type is `lambda` , the default is 35 seconds.", + "HealthCheckIntervalSeconds": "The approximate amount of time, in seconds, between health checks of an individual target. If the target group protocol is HTTP or HTTPS, the default is 30 seconds. If the target group protocol is TCP, TLS, UDP, or TCP_UDP, the supported values are 10 and 30 seconds and the default is 30 seconds. If the target group protocol is GENEVE, the default is 10 seconds. If the target type is `lambda` , the default is 35 seconds.", "HealthCheckPath": "[HTTP/HTTPS health checks] The destination for health checks on the targets.\n\n[HTTP1 or HTTP2 protocol version] The ping path. The default is /.\n\n[GRPC protocol version] The path of a custom health check method with the format /package.service/method. The default is / AWS .ALB/healthcheck.", "HealthCheckPort": "The port the load balancer uses when performing health checks on targets. If the protocol is HTTP, HTTPS, TCP, TLS, UDP, or TCP_UDP, the default is `traffic-port` , which is the port on which each target receives traffic from the load balancer. If the protocol is GENEVE, the default is port 80.", "HealthCheckProtocol": "The protocol the load balancer uses when performing health checks on targets. For Application Load Balancers, the default is HTTP. For Network Load Balancers and Gateway Load Balancers, the default is TCP. The TCP protocol is not supported for health checks if the protocol of the target group is HTTP or HTTPS. The GENEVE, TLS, UDP, and TCP_UDP protocols are not supported for health checks.", @@ -15913,7 +16252,7 @@ "DomainEndpoint": "The domain-specific endpoint that's used for requests to the OpenSearch APIs, such as `search-mystack-elasti-1ab2cdefghij-ab1c2deckoyb3hofw7wpqa3cm.us-west-1.es.amazonaws.com` .", "Ref": "When the logical ID of this resource is provided to the Ref intrinsic function, Ref returns the resource name, such as `mystack-elasticsea-abc1d2efg3h4.` For more information about using the Ref function, see [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) ." }, - "description": "The AWS::Elasticsearch::Domain resource creates an Amazon OpenSearch Service (successor to Amazon Elasticsearch Service) domain.\n\n> The `AWS::Elasticsearch::Domain` resource is being replaced by the [AWS::OpenSearchService::Domain](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html) resource. While the legacy Elasticsearch resource and options are still supported, we recommend modifying your existing Cloudformation templates to use the new OpenSearch Service resource, which supports both OpenSearch and Elasticsearch. For more information about the service rename, see [New resource types](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/rename.html#rename-resource) in the *Amazon OpenSearch Service Developer Guide* .", + "description": "The AWS::Elasticsearch::Domain resource creates an Amazon OpenSearch Service (successor to Amazon Elasticsearch Service) domain.\n\n> The `AWS::Elasticsearch::Domain` resource is being replaced by the [AWS::OpenSearchService::Domain](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html) resource. While the legacy Elasticsearch resource and options are still supported, we recommend modifying your existing Cloudformation templates to use the new OpenSearch Service resource, which supports both OpenSearch and legacy Elasticsearch. For instructions to upgrade domains defined within CloudFormation from Elasticsearch to OpenSearch, see [Remarks](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html#aws-resource-opensearchservice-domain--remarks) .", "properties": { "AccessPolicies": "An AWS Identity and Access Management ( IAM ) policy document that specifies who can access the OpenSearch Service domain and their permissions. For more information, see [Configuring access policies](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/ac.html#ac-creating) in the *Amazon OpenSearch Service Developer Guid* e.", "AdvancedOptions": "Additional options to specify for the OpenSearch Service domain. For more information, see [Advanced cluster parameters](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/createupdatedomains.html#createdomain-configure-advanced-options) in the *Amazon OpenSearch Service Developer Guide* .", @@ -16120,7 +16459,7 @@ "RegistryName": "The name of the schema registry.", "SchemaName": "The name of the schema.", "Tags": "Tags associated with the schema.", - "Type": "The type of schema." + "Type": "The type of schema.\n\nValid types include `OpenApi3` and `JSONSchemaDraft4` ." } }, "AWS::EventSchemas::Schema.TagsEntry": { @@ -16185,7 +16524,16 @@ "description": "Creates a new event bus within your account. This can be a custom event bus which you can use to receive events from your custom applications and services, or it can be a partner event bus which can be matched to a partner event source.", "properties": { "EventSourceName": "If you are creating a partner event bus, this specifies the partner event source that the new event bus will be matched with.", - "Name": "The name of the new event bus.\n\nEvent bus names cannot contain the / character. You can't use the name `default` for a custom event bus, as this name is already used for your account's default event bus.\n\nIf this is a partner event bus, the name must exactly match the name of the partner event source that this event bus is matched to." + "Name": "The name of the new event bus.\n\nEvent bus names cannot contain the / character. You can't use the name `default` for a custom event bus, as this name is already used for your account's default event bus.\n\nIf this is a partner event bus, the name must exactly match the name of the partner event source that this event bus is matched to.", + "Tags": "" + } + }, + "AWS::Events::EventBus.TagEntry": { + "attributes": {}, + "description": "", + "properties": { + "Key": "", + "Value": "" } }, "AWS::Events::EventBusPolicy": { @@ -16225,7 +16573,7 @@ "RoleArn": "The Amazon Resource Name (ARN) of the role that is used for target invocation.\n\nIf you're setting an event bus in another account as the target and that account granted permission to your account through an organization instead of directly by the account ID, you must specify a `RoleArn` with proper permissions in the `Target` structure, instead of here in this parameter.", "ScheduleExpression": "The scheduling expression. For example, \"cron(0 20 * * ? *)\", \"rate(5 minutes)\". For more information, see [Creating an Amazon EventBridge rule that runs on a schedule](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule-schedule.html) .", "State": "The state of the rule.", - "Targets": "Adds the specified targets to the specified rule, or updates the targets if they are already associated with the rule.\n\nTargets are the resources that are invoked when a rule is triggered.\n\n> Each rule can have up to five (5) targets associated with it at one time. \n\nYou can configure the following as targets for Events:\n\n- [API destination](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-api-destinations.html)\n- Amazon API Gateway REST API endpoints\n- API Gateway\n- AWS Batch job queue\n- CloudWatch Logs group\n- CodeBuild project\n- CodePipeline\n- Amazon EC2 `CreateSnapshot` API call\n- EC2 Image Builder\n- Amazon EC2 `RebootInstances` API call\n- Amazon EC2 `StopInstances` API call\n- Amazon EC2 `TerminateInstances` API call\n- Amazon ECS tasks\n- Event bus in a different AWS account or Region.\n\nYou can use an event bus in the US East (N. Virginia) us-east-1, US West (Oregon) us-west-2, or Europe (Ireland) eu-west-1 Regions as a target for a rule.\n- Firehose delivery stream (Kinesis Data Firehose)\n- Inspector assessment template (Amazon Inspector)\n- Kinesis stream (Kinesis Data Stream)\n- AWS Lambda function\n- Redshift clusters (Data API statement execution)\n- Amazon SNS topic\n- Amazon SQS queues (includes FIFO queues)\n- SSM Automation\n- SSM OpsItem\n- SSM Run Command\n- Step Functions state machines\n\nCreating rules with built-in targets is supported only in the AWS Management Console . The built-in targets are `EC2 CreateSnapshot API call` , `EC2 RebootInstances API call` , `EC2 StopInstances API call` , and `EC2 TerminateInstances API call` .\n\nFor some target types, `PutTargets` provides target-specific parameters. If the target is a Kinesis data stream, you can optionally specify which shard the event goes to by using the `KinesisParameters` argument. To invoke a command on multiple EC2 instances with one rule, you can use the `RunCommandParameters` field.\n\nTo be able to make API calls against the resources that you own, Amazon EventBridge needs the appropriate permissions. For AWS Lambda and Amazon SNS resources, EventBridge relies on resource-based policies. For EC2 instances, Kinesis Data Streams, AWS Step Functions state machines and API Gateway REST APIs, EventBridge relies on IAM roles that you specify in the `RoleARN` argument in `PutTargets` . For more information, see [Authentication and Access Control](https://docs.aws.amazon.com/eventbridge/latest/userguide/auth-and-access-control-eventbridge.html) in the *Amazon EventBridge User Guide* .\n\nIf another AWS account is in the same region and has granted you permission (using `PutPermission` ), you can send events to that account. Set that account's event bus as a target of the rules in your account. To send the matched events to the other account, specify that account's event bus as the `Arn` value when you run `PutTargets` . If your account sends events to another account, your account is charged for each sent event. Each event sent to another account is charged as a custom event. The account receiving the event is not charged. For more information, see [Amazon EventBridge Pricing](https://docs.aws.amazon.com/eventbridge/pricing/) .\n\n> `Input` , `InputPath` , and `InputTransformer` are not available with `PutTarget` if the target is an event bus of a different AWS account. \n\nIf you are setting the event bus of another account as the target, and that account granted permission to your account through an organization instead of directly by the account ID, then you must specify a `RoleArn` with proper permissions in the `Target` structure. For more information, see [Sending and Receiving Events Between AWS Accounts](https://docs.aws.amazon.com/eventbridge/latest/userguide/eventbridge-cross-account-event-delivery.html) in the *Amazon EventBridge User Guide* .\n\nFor more information about enabling cross-account events, see [PutPermission](https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_PutPermission.html) .\n\n*Input* , *InputPath* , and *InputTransformer* are mutually exclusive and optional parameters of a target. When a rule is triggered due to a matched event:\n\n- If none of the following arguments are specified for a target, then the entire event is passed to the target in JSON format (unless the target is Amazon EC2 Run Command or Amazon ECS task, in which case nothing from the event is passed to the target).\n- If *Input* is specified in the form of valid JSON, then the matched event is overridden with this constant.\n- If *InputPath* is specified in the form of JSONPath (for example, `$.detail` ), then only the part of the event specified in the path is passed to the target (for example, only the detail part of the event is passed).\n- If *InputTransformer* is specified, then one or more specified JSONPaths are extracted from the event and used as values in a template that you specify as the input to the target.\n\nWhen you specify `InputPath` or `InputTransformer` , you must use JSON dot notation, not bracket notation.\n\nWhen you add targets to a rule and the associated rule triggers soon after, new or updated targets might not be immediately invoked. Allow a short period of time for changes to take effect.\n\nThis action can partially fail if too many requests are made at the same time. If that happens, `FailedEntryCount` is non-zero in the response and each entry in `FailedEntries` provides the ID of the failed target and the error code." + "Targets": "Adds the specified targets to the specified rule, or updates the targets if they are already associated with the rule.\n\nTargets are the resources that are invoked when a rule is triggered.\n\n> Each rule can have up to five (5) targets associated with it at one time. \n\nYou can configure the following as targets for Events:\n\n- [API destination](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-api-destinations.html)\n- [API Gateway](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-api-gateway-target.html)\n- Batch job queue\n- CloudWatch group\n- CodeBuild project\n- CodePipeline\n- EC2 `CreateSnapshot` API call\n- EC2 Image Builder\n- EC2 `RebootInstances` API call\n- EC2 `StopInstances` API call\n- EC2 `TerminateInstances` API call\n- ECS task\n- [Event bus in a different account or Region](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-cross-account.html)\n- [Event bus in the same account and Region](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-bus-to-bus.html)\n- Firehose delivery stream\n- Glue workflow\n- [Incident Manager response plan](https://docs.aws.amazon.com//incident-manager/latest/userguide/incident-creation.html#incident-tracking-auto-eventbridge)\n- Inspector assessment template\n- Kinesis stream\n- Lambda function\n- Redshift cluster\n- SageMaker Pipeline\n- SNS topic\n- SQS queue\n- Step Functions state machine\n- Systems Manager Automation\n- Systems Manager OpsItem\n- Systems Manager Run Command\n\nCreating rules with built-in targets is supported only in the AWS Management Console . The built-in targets are `EC2 CreateSnapshot API call` , `EC2 RebootInstances API call` , `EC2 StopInstances API call` , and `EC2 TerminateInstances API call` .\n\nFor some target types, `PutTargets` provides target-specific parameters. If the target is a Kinesis data stream, you can optionally specify which shard the event goes to by using the `KinesisParameters` argument. To invoke a command on multiple EC2 instances with one rule, you can use the `RunCommandParameters` field.\n\nTo be able to make API calls against the resources that you own, Amazon EventBridge needs the appropriate permissions. For AWS Lambda and Amazon SNS resources, EventBridge relies on resource-based policies. For EC2 instances, Kinesis Data Streams, AWS Step Functions state machines and API Gateway REST APIs, EventBridge relies on IAM roles that you specify in the `RoleARN` argument in `PutTargets` . For more information, see [Authentication and Access Control](https://docs.aws.amazon.com/eventbridge/latest/userguide/auth-and-access-control-eventbridge.html) in the *Amazon EventBridge User Guide* .\n\nIf another AWS account is in the same region and has granted you permission (using `PutPermission` ), you can send events to that account. Set that account's event bus as a target of the rules in your account. To send the matched events to the other account, specify that account's event bus as the `Arn` value when you run `PutTargets` . If your account sends events to another account, your account is charged for each sent event. Each event sent to another account is charged as a custom event. The account receiving the event is not charged. For more information, see [Amazon EventBridge Pricing](https://docs.aws.amazon.com/eventbridge/pricing/) .\n\n> `Input` , `InputPath` , and `InputTransformer` are not available with `PutTarget` if the target is an event bus of a different AWS account. \n\nIf you are setting the event bus of another account as the target, and that account granted permission to your account through an organization instead of directly by the account ID, then you must specify a `RoleArn` with proper permissions in the `Target` structure. For more information, see [Sending and Receiving Events Between AWS Accounts](https://docs.aws.amazon.com/eventbridge/latest/userguide/eventbridge-cross-account-event-delivery.html) in the *Amazon EventBridge User Guide* .\n\nFor more information about enabling cross-account events, see [PutPermission](https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_PutPermission.html) .\n\n*Input* , *InputPath* , and *InputTransformer* are mutually exclusive and optional parameters of a target. When a rule is triggered due to a matched event:\n\n- If none of the following arguments are specified for a target, then the entire event is passed to the target in JSON format (unless the target is Amazon EC2 Run Command or Amazon ECS task, in which case nothing from the event is passed to the target).\n- If *Input* is specified in the form of valid JSON, then the matched event is overridden with this constant.\n- If *InputPath* is specified in the form of JSONPath (for example, `$.detail` ), then only the part of the event specified in the path is passed to the target (for example, only the detail part of the event is passed).\n- If *InputTransformer* is specified, then one or more specified JSONPaths are extracted from the event and used as values in a template that you specify as the input to the target.\n\nWhen you specify `InputPath` or `InputTransformer` , you must use JSON dot notation, not bracket notation.\n\nWhen you add targets to a rule and the associated rule triggers soon after, new or updated targets might not be immediately invoked. Allow a short period of time for changes to take effect.\n\nThis action can partially fail if too many requests are made at the same time. If that happens, `FailedEntryCount` is non-zero in the response and each entry in `FailedEntries` provides the ID of the failed target and the error code." } }, "AWS::Events::Rule.AwsVpcConfiguration": { @@ -16379,6 +16727,21 @@ "Values": "If `Key` is `tag:` *tag-key* , `Values` is a list of tag values. If `Key` is `InstanceIds` , `Values` is a list of Amazon EC2 instance IDs." } }, + "AWS::Events::Rule.SageMakerPipelineParameter": { + "attributes": {}, + "description": "Name/Value pair of a parameter to start execution of a SageMaker Model Building Pipeline.", + "properties": { + "Name": "Name of parameter to start execution of a SageMaker Model Building Pipeline.", + "Value": "Value of parameter to start execution of a SageMaker Model Building Pipeline." + } + }, + "AWS::Events::Rule.SageMakerPipelineParameters": { + "attributes": {}, + "description": "These are custom parameters to use when the target is a SageMaker Model Building Pipeline that starts based on EventBridge events.", + "properties": { + "PipelineParameterList": "List of Parameter names and values for SageMaker Model Building Pipeline execution." + } + }, "AWS::Events::Rule.SqsParameters": { "attributes": {}, "description": "This structure includes the custom parameter to be used when the target is an SQS FIFO queue.", @@ -16412,6 +16775,7 @@ "RetryPolicy": "The `RetryPolicy` object that contains the retry policy configuration to use for the dead-letter queue.", "RoleArn": "The Amazon Resource Name (ARN) of the IAM role to be used for this target when the rule is triggered. If one rule triggers multiple targets, you can use a different IAM role for each target.", "RunCommandParameters": "Parameters used when you are using the rule to invoke Amazon EC2 Run Command.", + "SageMakerPipelineParameters": "Contains the SageMaker Model Building Pipeline parameters to start execution of a SageMaker Model Building Pipeline.\n\nIf you specify a SageMaker Model Building Pipeline as a target, you can use this to specify parameters to start a pipeline execution based on EventBridge events.", "SqsParameters": "Contains the message group ID to use when the target is a FIFO queue.\n\nIf you specify an SQS FIFO queue as a target, the queue must have content-based deduplication enabled." } }, @@ -16442,7 +16806,7 @@ "EventPattern": "The EventBridge event pattern that defines how the metric is recorded.\n\nFor more information about EventBridge event patterns, see [Amazon EventBridge event patterns](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html) .", "MetricName": "A name for the metric. It can include up to 255 characters.", "UnitLabel": "A label for the units that the metric is measuring.", - "ValueKey": "A label for the units that the metric is measuring." + "ValueKey": "The JSON path to reference the numerical metric value in the event." } }, "AWS::Evidently::Experiment.OnlineAbConfigObject": { @@ -16631,7 +16995,7 @@ "Filters": "The filters to apply to identify target resources using specific attributes.", "ResourceArns": "The Amazon Resource Names (ARNs) of the resources.", "ResourceTags": "The tags for the target resources.", - "ResourceType": "The AWS resource type. The resource type must be supported for the specified action.", + "ResourceType": "The resource type. The resource type must be supported for the specified action.", "SelectionMode": "Scopes the identified resources to a specific count of the resources at random, or a percentage of the resources. All identified resources are included in the target.\n\n- ALL - Run the action on all identified targets. This is the default.\n- COUNT(n) - Run the action on the specified number of targets, chosen from the identified targets at random. For example, COUNT(1) selects one of the targets.\n- PERCENT(n) - Run the action on the specified percentage of targets, chosen from the identified targets at random. For example, PERCENT(25) selects 25% of the targets." } }, @@ -16671,7 +17035,7 @@ "ResourceType": "The type of resource protected by or in scope of the policy. This is in the format shown in the [AWS Resource Types Reference](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) . To apply this policy to multiple resource types, specify a resource type of `ResourceTypeList` and then specify the resource types in a `ResourceTypeList` .\n\nFor AWS WAF and Shield Advanced, example resource types include `AWS::ElasticLoadBalancingV2::LoadBalancer` and `AWS::CloudFront::Distribution` . For a security group common policy, valid values are `AWS::EC2::NetworkInterface` and `AWS::EC2::Instance` . For a security group content audit policy, valid values are `AWS::EC2::SecurityGroup` , `AWS::EC2::NetworkInterface` , and `AWS::EC2::Instance` . For a security group usage audit policy, the value is `AWS::EC2::SecurityGroup` . For an AWS Network Firewall policy or DNS Firewall policy, the value is `AWS::EC2::VPC` .", "ResourceTypeList": "An array of `ResourceType` objects. Use this only to specify multiple resource types. To specify a single resource type, use `ResourceType` .", "ResourcesCleanUp": "Indicates whether AWS Firewall Manager should automatically remove protections from resources that leave the policy scope and clean up resources that Firewall Manager is managing for accounts when those accounts leave policy scope. For example, Firewall Manager will disassociate a Firewall Manager managed web ACL from a protected customer resource when the customer resource leaves policy scope.\n\nBy default, Firewall Manager doesn't remove protections or delete Firewall Manager managed resources.\n\nThis option is not available for Shield Advanced or AWS WAF Classic policies.", - "SecurityServicePolicyData": "Details about the security service that is being used to protect the resources.\n\nThis contains the following settings:\n\n- Type - Indicates the service type that the policy uses to protect the resource. For security group policies, Firewall Manager supports one security group for each common policy and for each content audit policy. This is an adjustable limit that you can increase by contacting AWS Support .\n\nValid values: `WAFV2` | `WAF` | `SHIELD_ADVANCED` | `SECURITY_GROUPS_COMMON` | `SECURITY_GROUPS_CONTENT_AUDIT` | `SECURITY_GROUPS_USAGE_AUDIT` | `NETWORK_FIREWALL` | `DNS_FIREWALL` .\n- ManagedServiceData - Details about the service that are specific to the service type, in JSON format. For `SHIELD_ADVANCED` , this is an empty string.\n\n- Example: `WAFV2`\n\n`\"ManagedServiceData\": \"{\\\"type\\\":\\\"WAFV2\\\",\\\"preProcessRuleGroups\\\":[{\\\"ruleGroupArn\\\":null,\\\"overrideAction\\\":{\\\"type\\\":\\\"NONE\\\"},\\\"managedRuleGroupIdentifier\\\":{\\\"version\\\":null,\\\"vendorName\\\":\\\"AWS\\\",\\\"managedRuleGroupName\\\":\\\"AWSManagedRulesAmazonIpReputationList\\\"},\\\"ruleGroupType\\\":\\\"ManagedRuleGroup\\\",\\\"excludeRules\\\":[]}],\\\"postProcessRuleGroups\\\":[],\\\"defaultAction\\\":{\\\"type\\\":\\\"ALLOW\\\"},\\\"overrideCustomerWebACLAssociation\\\":false,\\\"loggingConfiguration\\\":{\\\"logDestinationConfigs\\\":[\\\"arn:aws:firehose:us-west-2:12345678912:deliverystream/aws-waf-logs-fms-admin-destination\\\"],\\\"redactedFields\\\":[{\\\"redactedFieldType\\\":\\\"SingleHeader\\\",\\\"redactedFieldValue\\\":\\\"Cookies\\\"},{\\\"redactedFieldType\\\":\\\"Method\\\"}]}}\"`\n\nIn the `loggingConfiguration` , you can specify one `logDestinationConfigs` , you can optionally provide up to 20 `redactedFields` , and the `RedactedFieldType` must be one of `URI` , `QUERY_STRING` , `HEADER` , or `METHOD` .\n- Example: `WAF Classic`\n\n`\"ManagedServiceData\": \"{\\\"type\\\": \\\"WAF\\\", \\\"ruleGroups\\\": [{\\\"id\\\":\\\"12345678-1bcd-9012-efga-0987654321ab\\\", \\\"overrideAction\\\" : {\\\"type\\\": \\\"COUNT\\\"}}],\\\"defaultAction\\\": {\\\"type\\\": \\\"BLOCK\\\"}}`\n\nAWS WAF Classic doesn't support rule groups in CloudFront . To create a WAF Classic policy through CloudFormation, create your rule groups outside of CloudFront , then provide the rule group IDs in the WAF managed service data specification.\n- Example: `SECURITY_GROUPS_COMMON`\n\n`\"SecurityServicePolicyData\":{\"Type\":\"SECURITY_GROUPS_COMMON\",\"ManagedServiceData\":\"{\\\"type\\\":\\\"SECURITY_GROUPS_COMMON\\\",\\\"revertManualSecurityGroupChanges\\\":false,\\\"exclusiveResourceSecurityGroupManagement\\\":false,\\\"securityGroups\\\":[{\\\"id\\\":\\\" sg-000e55995d61a06bd\\\"}]}\"},\"RemediationEnabled\":false,\"ResourceType\":\"AWS::EC2::NetworkInterface\"}`\n- Example: `SECURITY_GROUPS_CONTENT_AUDIT`\n\n`\"SecurityServicePolicyData\":{\"Type\":\"SECURITY_GROUPS_CONTENT_AUDIT\",\"ManagedServiceData\":\"{\\\"type\\\":\\\"SECURITY_GROUPS_CONTENT_AUDIT\\\",\\\"securityGroups\\\":[{\\\"id\\\":\\\" sg-000e55995d61a06bd \\\"}],\\\"securityGroupAction\\\":{\\\"type\\\":\\\"ALLOW\\\"}}\"},\"RemediationEnabled\":false,\"ResourceType\":\"AWS::EC2::NetworkInterface\"}`\n\nThe security group action for content audit can be `ALLOW` or `DENY` . For `ALLOW` , all in-scope security group rules must be within the allowed range of the policy's security group rules. For `DENY` , all in-scope security group rules must not contain a value or a range that matches a rule value or range in the policy security group.\n- Example: `SECURITY_GROUPS_USAGE_AUDIT`\n\n`\"SecurityServicePolicyData\":{\"Type\":\"SECURITY_GROUPS_USAGE_AUDIT\",\"ManagedServiceData\":\"{\\\"type\\\":\\\"SECURITY_GROUPS_USAGE_AUDIT\\\",\\\"deleteUnusedSecurityGroups\\\":true,\\\"coalesceRedundantSecurityGroups\\\":true}\"},\"RemediationEnabled\":false,\"Resou rceType\":\"AWS::EC2::SecurityGroup\"}`\n- Example: `NETWORK_FIREWALL`\n\n`\"ManagedServiceData\":\"{\\\"type\\\":\\\"NETWORK_FIREWALL\\\",\\\"networkFirewallStatelessRuleGroupReferences\\\":[{\\\"resourceARN\\\":\\\"arn:aws:network-firewall:us-east-1:000000000000:stateless-rulegroup\\/example\\\",\\\"priority\\\":1}],\\\"networkFirewallStatelessDefaultActions\\\":[\\\"aws:drop\\\",\\\"example\\\"],\\\"networkFirewallStatelessFragmentDefaultActions\\\":[\\\"aws:drop\\\",\\\"example\\\"],\\\"networkFirewallStatelessCustomActions\\\":[{\\\"actionName\\\":\\\"example\\\",\\\"actionDefinition\\\":{\\\"publishMetricAction\\\":{\\\"dimensions\\\":[{\\\"value\\\":\\\"example\\\"}]}}}],\\\"networkFirewallStatefulRuleGroupReferences\\\":[{\\\"resourceARN\\\":\\\"arn:aws:network-firewall:us-east-1:000000000000:stateful-rulegroup\\/example\\\"}],\\\"networkFirewallOrchestrationConfig\\\":{\\\"singleFirewallEndpointPerVPC\\\":false,\\\"allowedIPV4CidrList\\\":[]}}\"`\n- Example: `DNS_FIREWALL`\n\n`\"ManagedServiceData\": \"{ \\\"type\\\": \\\"DNS_FIREWALL\\\", \\\"preProcessRuleGroups\\\": [{\\\"ruleGroupId\\\": \\\"rslvr-frg-123456\\\", \\\"priority\\\": 11}], \\\"postProcessRuleGroups\\\": [{\\\"ruleGroupId\\\": \\\"rslvr-frg-123456\\\", \\\"priority\\\": 9902}]}\"`", + "SecurityServicePolicyData": "Details about the security service that is being used to protect the resources.\n\nThis contains the following settings:\n\n- Type - Indicates the service type that the policy uses to protect the resource. For security group policies, Firewall Manager supports one security group for each common policy and for each content audit policy. This is an adjustable limit that you can increase by contacting AWS Support .\n\nValid values: `DNS_FIREWALL` | `NETWORK_FIREWALL` | `SECURITY_GROUPS_COMMON` | `SECURITY_GROUPS_CONTENT_AUDIT` | `SECURITY_GROUPS_USAGE_AUDIT` | `SHIELD_ADVANCED` | `WAFV2` | `WAF`\n- ManagedServiceData - Details about the service that are specific to the service type, in JSON format.\n\n- Example: `DNS_FIREWALL`\n\n`\"ManagedServiceData\": \"{ \\\"type\\\": \\\"DNS_FIREWALL\\\", \\\"preProcessRuleGroups\\\": [{\\\"ruleGroupId\\\": \\\"rslvr-frg-123456\\\", \\\"priority\\\": 11}], \\\"postProcessRuleGroups\\\": [{\\\"ruleGroupId\\\": \\\"rslvr-frg-123456\\\", \\\"priority\\\": 9902}]}\"`\n- Example: `NETWORK_FIREWALL`\n\n`\"ManagedServiceData\":\"{\\\"type\\\":\\\"NETWORK_FIREWALL\\\",\\\"networkFirewallStatelessRuleGroupReferences\\\":[{\\\"resourceARN\\\":\\\"arn:aws:network-firewall:us-east-1:000000000000:stateless-rulegroup\\/example\\\",\\\"priority\\\":1}],\\\"networkFirewallStatelessDefaultActions\\\":[\\\"aws:drop\\\",\\\"example\\\"],\\\"networkFirewallStatelessFragmentDefaultActions\\\":[\\\"aws:drop\\\",\\\"example\\\"],\\\"networkFirewallStatelessCustomActions\\\":[{\\\"actionName\\\":\\\"example\\\",\\\"actionDefinition\\\":{\\\"publishMetricAction\\\":{\\\"dimensions\\\":[{\\\"value\\\":\\\"example\\\"}]}}}],\\\"networkFirewallStatefulRuleGroupReferences\\\":[{\\\"resourceARN\\\":\\\"arn:aws:network-firewall:us-east-1:000000000000:stateful-rulegroup\\/example\\\"}],\\\"networkFirewallOrchestrationConfig\\\":{\\\"singleFirewallEndpointPerVPC\\\":false,\\\"allowedIPV4CidrList\\\":[]}}\"`\n- Example: `SECURITY_GROUPS_COMMON`\n\n`\"SecurityServicePolicyData\":{\"Type\":\"SECURITY_GROUPS_COMMON\",\"ManagedServiceData\":\"{\\\"type\\\":\\\"SECURITY_GROUPS_COMMON\\\",\\\"revertManualSecurityGroupChanges\\\":false,\\\"exclusiveResourceSecurityGroupManagement\\\":false,\\\"securityGroups\\\":[{\\\"id\\\":\\\" sg-000e55995d61a06bd\\\"}]}\"},\"RemediationEnabled\":false,\"ResourceType\":\"AWS::EC2::NetworkInterface\"}`\n- Example: `SECURITY_GROUPS_CONTENT_AUDIT`\n\n`\"SecurityServicePolicyData\":{\"Type\":\"SECURITY_GROUPS_CONTENT_AUDIT\",\"ManagedServiceData\":\"{\\\"type\\\":\\\"SECURITY_GROUPS_CONTENT_AUDIT\\\",\\\"securityGroups\\\":[{\\\"id\\\":\\\" sg-000e55995d61a06bd \\\"}],\\\"securityGroupAction\\\":{\\\"type\\\":\\\"ALLOW\\\"}}\"},\"RemediationEnabled\":false,\"ResourceType\":\"AWS::EC2::NetworkInterface\"}`\n\nThe security group action for content audit can be `ALLOW` or `DENY` . For `ALLOW` , all in-scope security group rules must be within the allowed range of the policy's security group rules. For `DENY` , all in-scope security group rules must not contain a value or a range that matches a rule value or range in the policy security group.\n- Example: `SECURITY_GROUPS_USAGE_AUDIT`\n\n`\"SecurityServicePolicyData\":{\"Type\":\"SECURITY_GROUPS_USAGE_AUDIT\",\"ManagedServiceData\":\"{\\\"type\\\":\\\"SECURITY_GROUPS_USAGE_AUDIT\\\",\\\"deleteUnusedSecurityGroups\\\":true,\\\"coalesceRedundantSecurityGroups\\\":true}\"},\"RemediationEnabled\":false,\"Resou rceType\":\"AWS::EC2::SecurityGroup\"}`\n- Specification for `SHIELD_ADVANCED` for Amazon CloudFront distributions\n\n`\"ManagedServiceData\": \"{\\\"type\\\": \\\"SHIELD_ADVANCED\\\", \\\"automaticResponseConfiguration\\\": {\\\"automaticResponseStatus\\\":\\\"ENABLED|IGNORED|DISABLED\\\", \\\"automaticResponseAction\\\":\\\"BLOCK|COUNT\\\"}, \\\"overrideCustomerWebaclClassic\\\":true|false}\"`\n\nFor example: `\"ManagedServiceData\": \"{\\\"type\\\":\\\"SHIELD_ADVANCED\\\",\\\"automaticResponseConfiguration\\\": {\\\"automaticResponseStatus\\\":\\\"ENABLED\\\", \\\"automaticResponseAction\\\":\\\"COUNT\\\"}}\"`\n\nThe default value for `automaticResponseStatus` is `IGNORED` . The value for `automaticResponseAction` is only required when `automaticResponseStatus` is set to `ENABLED` . The default value for `overrideCustomerWebaclClassic` is `false` .\n\nFor other resource types that you can protect with a Shield Advanced policy, this `ManagedServiceData` configuration is an empty string.\n- Example: `WAFV2`\n\n`\"ManagedServiceData\": \"{\\\"type\\\":\\\"WAFV2\\\",\\\"preProcessRuleGroups\\\":[{\\\"ruleGroupArn\\\":null,\\\"overrideAction\\\":{\\\"type\\\":\\\"NONE\\\"},\\\"managedRuleGroupIdentifier\\\":{\\\"version\\\":null,\\\"vendorName\\\":\\\"AWS\\\",\\\"managedRuleGroupName\\\":\\\"AWSManagedRulesAmazonIpReputationList\\\"},\\\"ruleGroupType\\\":\\\"ManagedRuleGroup\\\",\\\"excludeRules\\\":[]}],\\\"postProcessRuleGroups\\\":[],\\\"defaultAction\\\":{\\\"type\\\":\\\"ALLOW\\\"},\\\"overrideCustomerWebACLAssociation\\\":false,\\\"loggingConfiguration\\\":{\\\"logDestinationConfigs\\\":[\\\"arn:aws:firehose:us-west-2:12345678912:deliverystream/aws-waf-logs-fms-admin-destination\\\"],\\\"redactedFields\\\":[{\\\"redactedFieldType\\\":\\\"SingleHeader\\\",\\\"redactedFieldValue\\\":\\\"Cookies\\\"},{\\\"redactedFieldType\\\":\\\"Method\\\"}]}}\"`\n\nIn the `loggingConfiguration` , you can specify one `logDestinationConfigs` , you can optionally provide up to 20 `redactedFields` , and the `RedactedFieldType` must be one of `URI` , `QUERY_STRING` , `HEADER` , or `METHOD` .\n- Example: `WAF Classic`\n\n`\"ManagedServiceData\": \"{\\\"type\\\": \\\"WAF\\\", \\\"ruleGroups\\\": [{\\\"id\\\":\\\"12345678-1bcd-9012-efga-0987654321ab\\\", \\\"overrideAction\\\" : {\\\"type\\\": \\\"COUNT\\\"}}],\\\"defaultAction\\\": {\\\"type\\\": \\\"BLOCK\\\"}}`\n\nAWS WAF Classic doesn't support rule groups in CloudFront . To create a WAF Classic policy through CloudFormation, create your rule groups outside of CloudFront , then provide the rule group IDs in the WAF managed service data specification.", "Tags": "A collection of key:value pairs associated with an AWS resource. The key:value pair can be anything you define. Typically, the tag key represents a category (such as \"environment\") and the tag value represents a specific value within that category (such as \"test,\" \"development,\" or \"production\"). You can add up to 50 tags to each AWS resource." } }, @@ -16701,12 +17065,12 @@ }, "AWS::FSx::FileSystem": { "attributes": { - "DNSName": "Use the DNSName value to access the DNS name of your Amazon FSx file system. The DNS name identifies the file system.", - "LustreMountName": "Use the LustreMountName value when mounting an Amazon FSx for Lustre file system. For SCRATCH_1 deployment types, this value is always \"fsx\". For SCRATCH_2 and PERSISTENT_1 deployment types, this value is a string that is unique within an AWS Region . For more information, see [Mounting from an Amazon EC2 Instance](https://docs.aws.amazon.com/fsx/latest/LustreGuide/mounting-ec2-instance.html) .", - "Ref": "`Ref` returns the function returns the file system resource ID. For example:\n\n`{\"Ref\":\"fs-01234567890123456\"}`\n\nFor the Amazon FSx file system `fs-01234567890123456` , Ref returns the file system ID.", - "RootVolumeId": "" + "DNSName": "Returns the FSx for Windows file system's DNSName.\n\nExample: `amznfsxp1honlek.corp.example.com`", + "LustreMountName": "Returns the file system's LustreMountName.\n\nExample for SCRATCH_1 deployment types: This value is always `fsx` .\n\nExample for SCRATCH_2 and PERSISTENT deployment types: `2p3fhbmv`", + "Ref": "`Ref` returns the file system resource ID. For example:\n\n`{\"Ref\":\"file_system_logical_id\"}`\n\nReturns `fs-0123456789abcdef6` .", + "RootVolumeId": "Returns the root volume ID of the FSx for OpenZFS file system.\n\nExample: `fsvol-0123456789abcdefa`" }, - "description": "The `AWS::FSx::FileSystem` resource is an Amazon FSx resource type that creates either an Amazon FSx for Windows File Server file system or an Amazon FSx for Lustre file system.", + "description": "The `AWS::FSx::FileSystem` resource is an Amazon FSx resource type that creates an Amazon FSx file system. You can create any of the following supported file system types:\n\n- Amazon FSx for Lustre\n- Amazon FSx for NetApp ONTAP\n- Amazon FSx for OpenZFS\n- Amazon FSx for Windows File Server", "properties": { "BackupId": "The ID of the source backup. Specifies the backup that you are copying.", "FileSystemType": "The type of Amazon FSx file system, which can be `LUSTRE` , `WINDOWS` , `ONTAP` , or `OPENZFS` .", @@ -16842,7 +17206,7 @@ "properties": { "ActiveDirectoryId": "The ID for an existing AWS Managed Microsoft Active Directory (AD) instance that the file system should join when it's created.", "Aliases": "An array of one or more DNS alias names that you want to associate with the Amazon FSx file system. Aliases allow you to use existing DNS names to access the data in your Amazon FSx file system. You can associate up to 50 aliases with a file system at any time.\n\nFor more information, see [Working with DNS Aliases](https://docs.aws.amazon.com/fsx/latest/WindowsGuide/managing-dns-aliases.html) and [Walkthrough 5: Using DNS aliases to access your file system](https://docs.aws.amazon.com/fsx/latest/WindowsGuide/walkthrough05-file-system-custom-CNAME.html) , including additional steps you must take to be able to access your file system using a DNS alias.\n\nAn alias name has to meet the following requirements:\n\n- Formatted as a fully-qualified domain name (FQDN), `hostname.domain` , for example, `accounting.example.com` .\n- Can contain alphanumeric characters, the underscore (_), and the hyphen (-).\n- Cannot start or end with a hyphen.\n- Can start with a numeric.\n\nFor DNS alias names, Amazon FSx stores alphabetical characters as lowercase letters (a-z), regardless of how you specify them: as uppercase letters, lowercase letters, or the corresponding letters in escape codes.", - "AuditLogConfiguration": "", + "AuditLogConfiguration": "The configuration that Amazon FSx for Windows File Server uses to audit and log user accesses of files, folders, and file shares on the Amazon FSx for Windows File Server file system.", "AutomaticBackupRetentionDays": "The number of days to retain automatic backups. The default is to retain backups for 7 days. Setting this value to 0 disables the creation of automatic backups. The maximum retention period for backups is 90 days.", "CopyTagsToBackups": "A Boolean flag indicating whether tags for the file system should be copied to backups. This value defaults to false. If it's set to true, all tags for the file system are copied to all automatic and user-initiated backups where the user doesn't specify tags. If this value is true, and you specify one or more tags, only the specified tags are copied to backups. If you specify one or more tags when creating a user-initiated backup, no tags are copied from the file system, regardless of this value.", "DailyAutomaticBackupStartTime": "The preferred time to take daily automatic backups, formatted HH:MM in the UTC time zone.", @@ -16855,8 +17219,8 @@ }, "AWS::FSx::Snapshot": { "attributes": { - "Ref": "", - "ResourceARN": "`Ref` returns the Amazon Resource Name (ARN)." + "Ref": "`Ref` returns the ID of the snapshot. For example:\n\n`{\"Ref\":\"logical_snapshot_id\"}`\n\nReturns `fsvolsnap-0123456789abcedf5` .", + "ResourceARN": "Returns the snapshot's Amazon Resource Name (ARN).\n\nExample: `arn:aws:fsx:us-east-2:111133334444:snapshot/fsvol-01234567890123456/fsvolsnap-0123456789abcedf5`" }, "description": "A snapshot of an Amazon FSx for OpenZFS volume.", "properties": { @@ -16867,10 +17231,10 @@ }, "AWS::FSx::StorageVirtualMachine": { "attributes": { - "Ref": "", - "ResourceARN": "`Ref` returns the Amazon Resource Name (ARN).", - "StorageVirtualMachineId": "`Ref` returns the SVM's system generated unique ID.", - "UUID": "`Ref` returns the volume's universally unique identifier (UUID)." + "Ref": "`Ref` returns the resource ID, such as `svm-01234567890123456` . For example:\n\n`{\"Ref\": \"svm_logical_id\"}` returns\n\n`svm-01234567890123456`", + "ResourceARN": "Returns the storage virtual machine's Amazon Resource Name (ARN).\n\nExample: `arn:aws:fsx:us-east-2:111111111111:storage-virtual-machine/fs-0123456789abcdef1/svm-01234567890123456`", + "StorageVirtualMachineId": "Returns the storgage virtual machine's system generated ID.\n\nExample: `svm-0123456789abcedf1`", + "UUID": "Returns the storage virtual machine's system generated unique identifier (UUID).\n\nExample: `abcd0123-cd45-ef67-11aa-1111aaaa23bc`" }, "description": "Creates a storage virtual machine (SVM) for an Amazon FSx for ONTAP file system.", "properties": { @@ -16904,10 +17268,10 @@ }, "AWS::FSx::Volume": { "attributes": { - "Ref": "", - "ResourceARN": "`Ref` returns the Amazon Resource Name (ARN).", - "UUID": "`Ref` returns the volume's universally unique identifier (UUID).", - "VolumeId": "`Ref` returns the system-generated, unique ID of the volume." + "Ref": "`Ref` returns the ID for the volume. For example:\n\n`{\"Ref\":\"vol_logical_id\"}`\n\nReturns `fsvol-0123456789abcdef6` .", + "ResourceARN": "Returns the volume's Amazon Resource Name (ARN).\n\nExample: `arn:aws:fsx:us-east-2:111122223333:volume/fs-0123456789abcdef9/fsvol-01234567891112223`", + "UUID": "Returns the volume's universally unique identifier (UUID).\n\nExample: `abcd0123-cd45-ef67-11aa-1111aaaa23bc`", + "VolumeId": "Returns the volume's ID.\n\nExample: `fsvol-0123456789abcdefa`" }, "description": "Creates an Amazon FSx for NetApp ONTAP or Amazon FSx for OpenZFS storage volume.", "properties": { @@ -17470,6 +17834,7 @@ "NotificationTarget": "An SNS topic ARN that is set up to receive game session placement notifications. See [Setting up notifications for game session placement](https://docs.aws.amazon.com/gamelift/latest/developerguide/queue-notification.html) .", "PlayerLatencyPolicies": "A set of policies that act as a sliding cap on player latency. FleetIQ works to deliver low latency for most players in a game session. These policies ensure that no individual player can be placed into a game with unreasonably high latency. Use multiple policies to gradually relax latency requirements a step at a time. Multiple policies are applied based on their maximum allowed latency, starting with the lowest value.", "PriorityConfiguration": "Custom settings to use when prioritizing destinations and locations for game session placements. This configuration replaces the FleetIQ default prioritization process. Priority types that are not explicitly named will be automatically applied at the end of the prioritization process.", + "Tags": "A list of labels to assign to the new game session queue resource. Tags are developer-defined key-value pairs. Tagging AWS resources are useful for resource management, access management and cost allocation. For more information, see [Tagging AWS Resources](https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html) in the *AWS General Reference* . Once the resource is created, you can use `TagResource` , `UntagResource` , and `ListTagsForResource` to add, remove, and view tags. The maximum tag limit may be lower than stated. See the AWS General Reference for actual tagging limits.", "TimeoutInSeconds": "The maximum time, in seconds, that a new game session placement request remains in the queue. When a request exceeds this time, the game session placement changes to a `TIMED_OUT` status." } }, @@ -17524,7 +17889,8 @@ "Name": "A unique identifier for a matchmaking configuration. Matchmaking requests use this name to identify which matchmaking configuration to use.", "NotificationTarget": "An SNS topic ARN that is set up to receive matchmaking notifications. See [Setting up notifications for matchmaking](https://docs.aws.amazon.com/gamelift/latest/flexmatchguide/match-notification.html) for more information.", "RequestTimeoutSeconds": "The maximum duration, in seconds, that a matchmaking ticket can remain in process before timing out. Requests that fail due to timing out can be resubmitted as needed.", - "RuleSetName": "A unique identifier for the matchmaking rule set to use with this configuration. You can use either the rule set name or ARN value. A matchmaking configuration can only use rule sets that are defined in the same Region." + "RuleSetName": "A unique identifier for the matchmaking rule set to use with this configuration. You can use either the rule set name or ARN value. A matchmaking configuration can only use rule sets that are defined in the same Region.", + "Tags": "A list of labels to assign to the new matchmaking configuration resource. Tags are developer-defined key-value pairs. Tagging AWS resources are useful for resource management, access management and cost allocation. For more information, see [Tagging AWS Resources](https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html) in the *AWS General Reference* . Once the resource is created, you can use `TagResource` , `UntagResource` , and `ListTagsForResource` to add, remove, and view tags. The maximum tag limit may be lower than stated. See the AWS General Reference for actual tagging limits." } }, "AWS::GameLift::MatchmakingConfiguration.GameProperty": { @@ -17544,7 +17910,8 @@ "description": "The `AWS::GameLift::MatchmakingRuleSet` resource creates a new rule set for FlexMatch matchmaking. A rule set describes the type of match to create, such as the number and size of teams. It also sets the parameters for acceptable player matches, such as minimum skill level or character type. A rule set is used by a matchmaking configuration.", "properties": { "Name": "A unique identifier for the matchmaking rule set. A matchmaking configuration identifies the rule set it uses by this name value. Note that the rule set name is different from the optional `name` field in the rule set body.", - "RuleSetBody": "A collection of matchmaking rules, formatted as a JSON string. Comments are not allowed in JSON, but most elements support a description field." + "RuleSetBody": "A collection of matchmaking rules, formatted as a JSON string. Comments are not allowed in JSON, but most elements support a description field.", + "Tags": "A list of labels to assign to the new matchmaking rule set resource. Tags are developer-defined key-value pairs. Tagging AWS resources are useful for resource management, access management and cost allocation. For more information, see [Tagging AWS Resources](https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html) in the *AWS General Reference* . Once the resource is created, you can use `TagResource` , `UntagResource` , and `ListTagsForResource` to add, remove, and view tags. The maximum tag limit may be lower than stated. See the AWS General Reference for actual tagging limits." } }, "AWS::GameLift::Script": { @@ -17557,6 +17924,7 @@ "properties": { "Name": "A descriptive label that is associated with a script. Script names do not need to be unique.", "StorageLocation": "The location in Amazon S3 where build or script files are stored for access by Amazon GameLift.", + "Tags": "", "Version": "The version that is associated with a build or script. Version strings do not need to be unique." } }, @@ -17770,6 +18138,14 @@ "Path": "The path of the JDBC target." } }, + "AWS::Glue::Crawler.MongoDBTarget": { + "attributes": {}, + "description": "Specifies an Amazon DocumentDB or MongoDB data store to crawl.", + "properties": { + "ConnectionName": "The name of the connection to use to connect to the Amazon DocumentDB or MongoDB target.", + "Path": "The path of the Amazon DocumentDB or MongoDB target (database/collection)." + } + }, "AWS::Glue::Crawler.RecrawlPolicy": { "attributes": {}, "description": "When crawling an Amazon S3 data source after the first crawl is complete, specifies whether to crawl the entire dataset again or to crawl only folders that were added since the last crawler run. For more information, see [Incremental Crawls in AWS Glue](https://docs.aws.amazon.com/glue/latest/dg/incremental-crawls.html) in the developer guide.", @@ -17782,8 +18158,11 @@ "description": "Specifies a data store in Amazon Simple Storage Service (Amazon S3).", "properties": { "ConnectionName": "The name of a connection which allows a job or crawler to access data in Amazon S3 within an Amazon Virtual Private Cloud environment (Amazon VPC).", + "DlqEventQueueArn": "", + "EventQueueArn": "", "Exclusions": "A list of glob patterns used to exclude from the crawl. For more information, see [Catalog Tables with a Crawler](https://docs.aws.amazon.com/glue/latest/dg/add-crawler.html) .", - "Path": "The path to the Amazon S3 target." + "Path": "The path to the Amazon S3 target.", + "SampleSize": "Sets the number of files in each leaf folder to be crawled when crawling sample files in a dataset. If not set, all the files are crawled. A valid value is an integer between 1 and 249." } }, "AWS::Glue::Crawler.Schedule": { @@ -17808,6 +18187,7 @@ "CatalogTargets": "Specifies AWS Glue Data Catalog targets.", "DynamoDBTargets": "Specifies Amazon DynamoDB targets.", "JdbcTargets": "Specifies JDBC targets.", + "MongoDBTargets": "A list of Mongo DB targets.", "S3Targets": "Specifies Amazon Simple Storage Service (Amazon S3) targets." } }, @@ -17884,10 +18264,10 @@ }, "AWS::Glue::Database.PrincipalPrivileges": { "attributes": {}, - "description": "", + "description": "the permissions granted to a principal", "properties": { - "Permissions": "", - "Principal": "" + "Permissions": "The permissions that are granted to the principal.", + "Principal": "The principal who is granted permissions." } }, "AWS::Glue::DevEndpoint": { @@ -19420,11 +19800,26 @@ }, "AWS::GuardDuty::Detector.CFNDataSourceConfigurations": { "attributes": {}, - "description": "Describes whether S3 data event logs will be enabled as a data source when the detector is created.", + "description": "Describes whether S3 data event logs or Kubernetes audit logs will be enabled as a data source when the detector is created.", "properties": { + "Kubernetes": "Describes which Kuberentes data sources are enabled for a detector.", "S3Logs": "Describes whether S3 data event logs are enabled as a data source." } }, + "AWS::GuardDuty::Detector.CFNKubernetesAuditLogsConfiguration": { + "attributes": {}, + "description": "Describes which optional data sources are enabled for a detector.", + "properties": { + "Enable": "Describes whether Kubernetes audit logs are enabled as a data source for the detector." + } + }, + "AWS::GuardDuty::Detector.CFNKubernetesConfiguration": { + "attributes": {}, + "description": "Describes which Kubernetes protection data sources are enabled for the detector.", + "properties": { + "AuditLogs": "Describes whether Kubernetes audit logs are enabled as a data source for the detector." + } + }, "AWS::GuardDuty::Detector.CFNS3LogsConfiguration": { "attributes": {}, "description": "Describes whether S3 data event logs will be enabled as a data source when the detector is created.", @@ -19443,7 +19838,7 @@ "DetectorId": "The ID of the detector belonging to the GuardDuty account that you want to create a filter for.", "FindingCriteria": "Represents the criteria to be used in the filter for querying findings.", "Name": "The name of the filter. Minimum length of 3. Maximum length of 64. Valid characters include alphanumeric characters, dot (.), underscore (_), and dash (-). Spaces are not allowed.", - "Rank": "Specifies the position of the filter in the list of current filters. Also specifies the order in which this filter is applied to the findings.\n\n> By default filters may not be created in the same order as they are ranked. To ensure filters are created in the correct order you can use the optional `DependsOn` attribute with the following syntax: `\"DependsOn\":[ \"ObjectName\" ]` . You can find more information on using this attribute [here](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-dependson.html) ." + "Rank": "" } }, "AWS::GuardDuty::Filter.Condition": { @@ -19800,7 +20195,8 @@ "properties": { "DestinationConfiguration": "A destination configuration contains information about where recorded video will be stored. See the [DestinationConfiguration](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ivs-recordingconfiguration-destinationconfiguration.html) property type for more information.", "Name": "Recording-configuration name. The value does not need to be unique.", - "Tags": "An array of key-value pairs to apply to this resource.\n\nFor more information, see [Tag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) ." + "Tags": "An array of key-value pairs to apply to this resource.\n\nFor more information, see [Tag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) .", + "ThumbnailConfiguration": "A thumbnail configuration enables/disables the recording of thumbnails for a live session and controls the interval at which thumbnails are generated for the live session. See the [ThumbnailConfiguration](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ivs-recordingconfiguration-thunbnailconfiguration.html) property type for more information." } }, "AWS::IVS::RecordingConfiguration.DestinationConfiguration": { @@ -19817,6 +20213,14 @@ "BucketName": "Location (S3 bucket name) where recorded videos will be stored." } }, + "AWS::IVS::RecordingConfiguration.ThumbnailConfiguration": { + "attributes": {}, + "description": "The ThumbnailConfiguration property type describes a configuration of thumbnails for recorded video.", + "properties": { + "RecordingMode": "Thumbnail recording mode. Valid values:\n\n- `DISABLED` : Use DISABLED to disable the generation of thumbnails for recorded video.\n- `INTERVAL` : Use INTERVAL to enable the generation of thumbnails for recorded video at a time interval controlled by the [TargetIntervalSeconds](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ivs-recordingconfiguration-thumbnailconfiguration.html#cfn-ivs-recordingconfiguration-thumbnailconfiguration-targetintervalseconds) property.\n\n*Default* : `INTERVAL`", + "TargetIntervalSeconds": "The targeted thumbnail-generation interval in seconds. This is configurable (and required) only if [RecordingMode](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ivs-recordingconfiguration-thumbnailconfiguration.html#cfn-ivs-recordingconfiguration-thumbnailconfiguration-recordingmode) is `INTERVAL` .\n\n> Setting a value for `TargetIntervalSeconds` does not guarantee that thumbnails are generated at the specified interval. For thumbnails to be generated at the `TargetIntervalSeconds` interval, the `IDR/Keyframe` value for the input video must be less than the `TargetIntervalSeconds` value. See [Amazon IVS Streaming Configuration](https://docs.aws.amazon.com/ivs/latest/userguide/streaming-config.html) for information on setting `IDR/Keyframe` to the recommended value in video-encoder settings. \n\n*Default* : 60\n\n*Valid Range* : Minumum value of 5. Maximum value of 60." + } + }, "AWS::IVS::StreamKey": { "attributes": { "Arn": "The stream-key ARN. For example: `arn:aws:ivs:us-west-2:123456789012:stream-key/g1H2I3j4k5L6`", @@ -19847,7 +20251,7 @@ "Platform": "The platform of the component. For example, `Windows` .", "SupportedOsVersions": "The operating system (OS) version supported by the component. If the OS information is available, a prefix match is performed against the base image OS version during image recipe creation.", "Tags": "The tags associated with the component.", - "Uri": "The uri of the component. Must be an Amazon S3 URL and the requester must have permission to access the Amazon S3 bucket. If you use Amazon S3, you can specify component content up to your service quota. Either `data` or `uri` can be used to specify the data within the component.", + "Uri": "The `uri` of a YAML component document file. This must be an S3 URL ( `s3://bucket/key` ), and the requester must have permission to access the S3 bucket it points to. If you use Amazon S3, you can specify component content up to your service quota.\n\nAlternatively, you can specify the YAML document inline, using the component `data` property. You cannot specify both properties.", "Version": "The component version. For example, `1.0.0` ." } }, @@ -20044,7 +20448,7 @@ "description": "In addition to your infrastruction configuration, these settings provide an extra layer of control over your build instances. For instances where Image Builder installs the Systems Manager agent, you can choose whether to keep it for the AMI that you create. You can also specify commands to run on launch for all of your build instances.", "properties": { "SystemsManagerAgent": "Contains settings for the Systems Manager agent on your build instance.", - "UserDataOverride": "Use this property to provide commands or a command script to run when you launch your build instance.\n\n> The userDataOverride property replaces any commands that Image Builder might have added to ensure that Systems Manager is installed on your Linux build instance. If you override the user data, make sure that you add commands to install Systems Manager, if it is not pre-installed on your base image." + "UserDataOverride": "Use this property to provide commands or a command script to run when you launch your build instance.\n\nThe userDataOverride property replaces any commands that Image Builder might have added to ensure that Systems Manager is installed on your Linux build instance. If you override the user data, make sure that you add commands to install Systems Manager, if it is not pre-installed on your base image.\n\n> The user data is always base 64 encoded. For example, the following commands are encoded as `IyEvYmluL2Jhc2gKbWtkaXIgLXAgL3Zhci9iYi8KdG91Y2ggL3Zhci$` :\n> \n> *#!/bin/bash*\n> \n> mkdir -p /var/bb/\n> \n> touch /var" } }, "AWS::ImageBuilder::ImageRecipe.ComponentConfiguration": { @@ -20324,9 +20728,9 @@ }, "description": "Use the `AWS::IoT::CustomMetric` resource to define a custom metric published by your devices to Device Defender. For API reference, see [CreateCustomMetric](https://docs.aws.amazon.com/iot/latest/apireference/API_CreateCustomMetric.html) and for general information, see [Custom metrics](https://docs.aws.amazon.com/iot/latest/developerguide/dd-detect-custom-metrics.html) .", "properties": { - "DisplayName": "Field that represents a friendly name in the console for the custom metric; it doesn't have to be unique. Don't use this name as the metric identifier in the device metric report. Can be updated.", - "MetricName": "The name of the custom metric. This will be used in the metric report submitted from the device/thing. It shouldn't begin with `aws:` . Cannot be updated once it's defined.", - "MetricType": "The type of the custom metric. Types include `string-list` , `ip-address-list` , `number-list` , and `number` .", + "DisplayName": "The friendly name in the console for the custom metric. This name doesn't have to be unique. Don't use this name as the metric identifier in the device metric report. You can update the friendly name after you define it.", + "MetricName": "The name of the custom metric. This will be used in the metric report submitted from the device/thing. The name can't begin with `aws:` . You can\u2019t change the name after you define it.", + "MetricType": "The type of the custom metric. Types include `string-list` , `ip-address-list` , `number-list` , and `number` .\n\n> The type `number` only takes a single metric value as an input, but when you submit the metrics value in the DeviceMetrics report, you must pass it as an array with a single value.", "Tags": "Metadata that can be used to manage the custom metric." } }, @@ -20421,6 +20825,7 @@ "Document": "The job document.\n\nRequired if you don't specify a value for `documentSource` .", "DocumentSource": "An S3 link to the job document to use in the template. Required if you don't specify a value for `document` .\n\n> If the job document resides in an S3 bucket, you must use a placeholder link when specifying the document.\n> \n> The placeholder link is of the following form:\n> \n> `${aws:iot:s3-presigned-url:https://s3.amazonaws.com/ *bucket* / *key* }`\n> \n> where *bucket* is your bucket name and *key* is the object in the bucket to which you are linking.", "JobArn": "The ARN of the job to use as the basis for the job template.", + "JobExecutionsRetryConfig": "Allows you to create the criteria to retry a job.", "JobExecutionsRolloutConfig": "Allows you to create a staged rollout of a job.", "JobTemplateId": "A unique identifier for the job template. We recommend using a UUID. Alpha-numeric characters, \"-\", and \"_\" are valid for use here.", "PresignedUrlConfig": "Configuration for pre-signed S3 URLs.", @@ -20430,13 +20835,13 @@ }, "AWS::IoT::Logging": { "attributes": { - "Ref": "`Ref` returns\n\n`{ \"Ref\": \"AccountId\" }`" + "Ref": "`Ref` returns the log ID. For example:\n\n`{\"Ref\": \"Log-12345\"}`" }, - "description": "Sets the logging options in the V2 logging service.", + "description": "Configure logging.", "properties": { - "AccountId": "The unique identifier of the account to use when writing to CloudWatch logs.", - "DefaultLogLevel": "The logging level. Valid values are `DEBUG` , `INFO` , `ERROR` , `WARN` , and `DISABLED` .", - "RoleArn": "The ARN of the role that allows IoT to write to Cloudwatch logs." + "AccountId": "The account ID.", + "DefaultLogLevel": "The default log level.Valid Values: `DEBUG | INFO | ERROR | WARN | DISABLED`", + "RoleArn": "The role ARN used for the log." } }, "AWS::IoT::MitigationAction": { @@ -20554,14 +20959,14 @@ }, "AWS::IoT::ResourceSpecificLogging": { "attributes": { - "Ref": "`Ref` returns\n\n`{ \"Ref\": \"TargetType:TargetName\" }`", - "TargetId": "The unique identifier of the log target." + "Ref": "`Ref` returns the resource-specific log ID. For example:\n\n`{\"Ref\": \"MyResourceLog-12345\" }`", + "TargetId": "The target Id." }, - "description": "Sets the logging options for a specific resource in the V2 logging service.", + "description": "Configure resource-specific logging.", "properties": { - "LogLevel": "The logging level. Valid values are `DEBUG` , `INFO` , `ERROR` , `WARN` , and `DISABLED` .", - "TargetName": "The log target name.", - "TargetType": "The log target type." + "LogLevel": "The default log level.Valid Values: `DEBUG | INFO | ERROR | WARN | DISABLED`", + "TargetName": "The target name.", + "TargetType": "The target type. Valid Values: `DEFAULT | THING_GROUP`" } }, "AWS::IoT::ScheduledAudit": { @@ -20656,10 +21061,10 @@ "properties": { "Cidrs": "If the `comparisonOperator` calls for a set of CIDRs, use this to specify that set to be compared with the `metric` .", "Count": "If the `comparisonOperator` calls for a numeric value, use this to specify that numeric value to be compared with the `metric` .", - "Number": "", - "Numbers": "", + "Number": "The numeric values of a metric.", + "Numbers": "The numeric value of a metric.", "Ports": "If the `comparisonOperator` calls for a set of ports, use this to specify that set to be compared with the `metric` .", - "Strings": "" + "Strings": "The string values of a metric." } }, "AWS::IoT::SecurityProfile.StatisticalThreshold": { @@ -20711,7 +21116,7 @@ "description": "Describes the actions associated with a rule.", "properties": { "CloudwatchAlarm": "Change the state of a CloudWatch alarm.", - "CloudwatchLogs": "", + "CloudwatchLogs": "Sends data to CloudWatch.", "CloudwatchMetric": "Capture a CloudWatch metric.", "DynamoDB": "Write to a DynamoDB table.", "DynamoDBv2": "Write to a DynamoDB table. This is a new version of the DynamoDB action. It allows you to write each attribute in an MQTT message payload into a separate DynamoDB column.", @@ -20828,7 +21233,7 @@ "attributes": {}, "description": "Describes an action that writes data to an Amazon Kinesis Firehose stream.", "properties": { - "BatchMode": "", + "BatchMode": "Whether to deliver the Kinesis Data Firehose stream as a batch by using [`PutRecordBatch`](https://docs.aws.amazon.com/firehose/latest/APIReference/API_PutRecordBatch.html) . The default value is `false` .\n\nWhen `batchMode` is `true` and the rule's SQL statement evaluates to an Array, each Array element forms one record in the [`PutRecordBatch`](https://docs.aws.amazon.com/firehose/latest/APIReference/API_PutRecordBatch.html) request. The resulting array can't have more than 500 records.", "DeliveryStreamName": "The delivery stream name.", "RoleArn": "The IAM role that grants access to the Amazon Kinesis Firehose stream.", "Separator": "A character separator that will be used to separate records written to the Firehose stream. Valid values are: '\\n' (newline), '\\t' (tab), '\\r\\n' (Windows newline), ',' (comma)." @@ -20863,7 +21268,7 @@ "attributes": {}, "description": "Sends message data to an AWS IoT Analytics channel.", "properties": { - "BatchMode": "", + "BatchMode": "Whether to process the action as a batch. The default value is `false` .\n\nWhen `batchMode` is `true` and the rule SQL statement evaluates to an Array, each Array element is delivered as a separate message when passed by [`BatchPutMessage`](https://docs.aws.amazon.com/iotanalytics/latest/APIReference/API_BatchPutMessage.html) The resulting array can't have more than 100 messages.", "ChannelName": "The name of the IoT Analytics channel to which message data will be sent.", "RoleArn": "The ARN of the role which has a policy that grants IoT Analytics permission to send message data via IoT Analytics (iotanalytics:BatchPutMessage)." } @@ -20872,7 +21277,7 @@ "attributes": {}, "description": "Sends an input to an AWS IoT Events detector.", "properties": { - "BatchMode": "", + "BatchMode": "Whether to process the event actions as a batch. The default value is `false` .\n\nWhen `batchMode` is `true` , you can't specify a `messageId` .\n\nWhen `batchMode` is `true` and the rule SQL statement evaluates to an Array, each Array element is treated as a separate message when Events by calling [`BatchPutMessage`](https://docs.aws.amazon.com/iotevents/latest/apireference/API_iotevents-data_BatchPutMessage.html) . The resulting array can't have more than 10 messages.", "InputName": "The name of the AWS IoT Events input.", "MessageId": "The ID of the message. The default `messageId` is a new UUID value.\n\nWhen `batchMode` is `true` , you can't specify a `messageId` --a new UUID value will be assigned.\n\nAssign a value to this property to ensure that only one input (message) with a given `messageId` will be processed by an AWS IoT Events detector.", "RoleArn": "The ARN of the role that grants AWS IoT permission to send an input to an AWS IoT Events detector. (\"Action\":\"iotevents:BatchPutMessage\")." @@ -21001,7 +21406,7 @@ "attributes": {}, "description": "Describes an action that writes records into an Amazon Timestream table.", "properties": { - "BatchMode": "", + "BatchMode": "Whether to process the action as a batch.", "DatabaseName": "The name of an Amazon Timestream database that has the table to write records into.", "Dimensions": "Metadata attributes of the time series that are written in each measure record.", "RoleArn": "The Amazon Resource Name (ARN) of the role that grants AWS IoT permission to write to the Timestream database table.", @@ -21041,7 +21446,7 @@ "attributes": { "Arn": "The topic rule destination URL.", "Ref": "`Ref` returns the topic rule destination. For example:\n\n`{ \"Ref\": \"TopicRuleDestination\" }`\n\nA value similar to the following is returned:\n\n`a1234567b89c012d3e4fg567hij8k9l01mno1p23q45678901rs234567890t1u2`", - "StatusReason": "" + "StatusReason": "Additional details or reason why the topic rule destination is in the current status." }, "description": "A topic rule destination.", "properties": { @@ -21911,8 +22316,9 @@ }, "AWS::IoTSiteWise::Asset": { "attributes": { - "AssetId": "", - "Ref": "`Ref` returns the `AssetId` ." + "AssetArn": "The ARN of the asset.", + "AssetId": "The ID of the asset.", + "Ref": "`Ref` returns `AssetId` and `AssetArn` ." }, "description": "Creates an asset from an existing asset model. For more information, see [Creating assets](https://docs.aws.amazon.com/iot-sitewise/latest/userguide/create-assets.html) in the *AWS IoT SiteWise User Guide* .", "properties": { @@ -22040,7 +22446,7 @@ "description": "Contains a tumbling window, which is a repeating fixed-sized, non-overlapping, and contiguous time window. You can use this window in metrics to aggregate data from properties and other assets.\n\nYou can use `m` , `h` , `d` , and `w` when you specify an interval or offset. Note that `m` represents minutes, `h` represents hours, `d` represents days, and `w` represents weeks. You can also use `s` to represent seconds in `offset` .\n\nThe `interval` and `offset` parameters support the [ISO 8601 format](https://docs.aws.amazon.com/https://en.wikipedia.org/wiki/ISO_8601) . For example, `PT5S` represents 5 seconds, `PT5M` represents 5 minutes, and `PT5H` represents 5 hours.", "properties": { "Interval": "The time interval for the tumbling window. The interval time must be between 1 minute and 1 week.\n\nAWS IoT SiteWise computes the `1w` interval the end of Sunday at midnight each week (UTC), the `1d` interval at the end of each day at midnight (UTC), the `1h` interval at the end of each hour, and so on.\n\nWhen AWS IoT SiteWise aggregates data points for metric computations, the start of each interval is exclusive and the end of each interval is inclusive. AWS IoT SiteWise places the computed data point at the end of the interval.", - "Offset": "" + "Offset": "The offset for the tumbling window. The `offset` parameter accepts the following:\n\n- The offset time.\n\nFor example, if you specify `18h` for `offset` and `1d` for `interval` , AWS IoT SiteWise aggregates data in one of the following ways:\n\n- If you create the metric before or at 6 PM (UTC), you get the first aggregation result at 6 PM (UTC) on the day when you create the metric.\n- If you create the metric after 6 PM (UTC), you get the first aggregation result at 6 PM (UTC) the next day.\n- The ISO 8601 format.\n\nFor example, if you specify `PT18H` for `offset` and `1d` for `interval` , AWS IoT SiteWise aggregates data in one of the following ways:\n\n- If you create the metric before or at 6 PM (UTC), you get the first aggregation result at 6 PM (UTC) on the day when you create the metric.\n- If you create the metric after 6 PM (UTC), you get the first aggregation result at 6 PM (UTC) the next day.\n- The 24-hour clock.\n\nFor example, if you specify `00:03:00` for `offset` , `5m` for `interval` , and you create the metric at 2 PM (UTC), you get the first aggregation result at 2:03 PM (UTC). You get the second aggregation result at 2:08 PM (UTC).\n- The offset time zone.\n\nFor example, if you specify `2021-07-23T18:00-08` for `offset` and `1d` for `interval` , AWS IoT SiteWise aggregates data in one of the following ways:\n\n- If you create the metric before or at 6 PM (PST), you get the first aggregation result at 6 PM (PST) on the day when you create the metric.\n- If you create the metric after 6 PM (PST), you get the first aggregation result at 6 PM (PST) the next day." } }, "AWS::IoTSiteWise::AssetModel.VariableValue": { @@ -22091,7 +22497,8 @@ "attributes": {}, "description": "Contains a gateway's platform information.", "properties": { - "Greengrass": "A gateway that runs on AWS IoT Greengrass ." + "Greengrass": "A gateway that runs on AWS IoT Greengrass .", + "GreengrassV2": "A gateway that runs on AWS IoT Greengrass V2." } }, "AWS::IoTSiteWise::Gateway.Greengrass": { @@ -22101,6 +22508,13 @@ "GroupArn": "The [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) of the Greengrass group. For more information about how to find a group's ARN, see [ListGroups](https://docs.aws.amazon.com/greengrass/latest/apireference/listgroups-get.html) and [GetGroup](https://docs.aws.amazon.com/greengrass/latest/apireference/getgroup-get.html) in the *AWS IoT Greengrass API Reference* ." } }, + "AWS::IoTSiteWise::Gateway.GreengrassV2": { + "attributes": {}, + "description": "Contains details for a gateway that runs on AWS IoT Greengrass V2. To create a gateway that runs on AWS IoT Greengrass V2, you must deploy the IoT SiteWise Edge component to your gateway device. Your [Greengrass device role](https://docs.aws.amazon.com/greengrass/v2/developerguide/device-service-role.html) must use the `AWSIoTSiteWiseEdgeAccess` policy. For more information, see [Using AWS IoT SiteWise at the edge](https://docs.aws.amazon.com/iot-sitewise/latest/userguide/sw-gateways.html) in the *AWS IoT SiteWise User Guide* .", + "properties": { + "CoreDeviceThingName": "The name of the AWS IoT thing for your AWS IoT Greengrass V2 core device." + } + }, "AWS::IoTSiteWise::Portal": { "attributes": { "PortalArn": "The [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) of the portal, which has the following format.\n\n`arn:${Partition}:iotsitewise:${Region}:${Account}:portal/${PortalId}`", @@ -22129,7 +22543,7 @@ }, "description": "Creates a project in the specified portal.\n\n> Make sure that the project name and description don't contain confidential information.", "properties": { - "AssetIds": "", + "AssetIds": "A list that contains the IDs of each asset associated with the project.", "PortalId": "The ID of the portal in which to create the project.", "ProjectDescription": "A description for the project.", "ProjectName": "A friendly name for the project.", @@ -22506,7 +22920,7 @@ "attributes": { "Ref": "`Ref` returns the alias name, such as `alias/exampleAlias` ." }, - "description": "The `AWS::KMS::Alias` resource specifies a display name for a [KMS key](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#kms_keys) . You can use an alias to identify a KMS key in the AWS KMS console, in the [DescribeKey](https://docs.aws.amazon.com/kms/latest/APIReference/API_DescribeKey.html) operation, and in [cryptographic operations](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#cryptographic-operations) , such as [Decrypt](https://docs.aws.amazon.com/kms/latest/APIReference/API_Decrypt.html) and [GenerateDataKey](https://docs.aws.amazon.com/kms/latest/APIReference/API_GenerateDataKey.html) .\n\n> Adding, deleting, or updating an alias can allow or deny permission to the KMS key. For details, see [Using ABAC in AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/abac.html) in the *AWS Key Management Service Developer Guide* . \n\nUsing an alias to refer to a KMS key can help you simplify key management. For example, an alias in your code can be associated with different KMS keys in different AWS Regions . For more information, see [Using aliases](https://docs.aws.amazon.com/kms/latest/developerguide/kms-alias.html) in the *AWS Key Management Service Developer Guide* .\n\nWhen specifying an alias, observe the following rules.\n\n- Each alias is associated with one KMS key, but multiple aliases can be associated with the same KMS key.\n- The alias and its associated KMS key must be in the same AWS account and Region.\n- The alias name must be unique in the AWS account and Region. However, you can create aliases with the same name in different AWS Regions . For example, you can have an `alias/projectKey` in multiple Regions, each of which is associated with a KMS key in its Region.\n- Each alias name must begin with `alias/` followed by a name, such as `alias/exampleKey` . The alias name can contain only alphanumeric characters, forward slashes (/), underscores (_), and dashes (-). Alias names cannot begin with `alias/aws/` . That alias name prefix is reserved for [AWS managed keys](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#aws-managed-cmk) .", + "description": "The `AWS::KMS::Alias` resource specifies a display name for a [KMS key](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#kms_keys) . You can use an alias to identify a KMS key in the AWS KMS console, in the [DescribeKey](https://docs.aws.amazon.com/kms/latest/APIReference/API_DescribeKey.html) operation, and in [cryptographic operations](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#cryptographic-operations) , such as [Decrypt](https://docs.aws.amazon.com/kms/latest/APIReference/API_Decrypt.html) and [GenerateDataKey](https://docs.aws.amazon.com/kms/latest/APIReference/API_GenerateDataKey.html) .\n\n> Adding, deleting, or updating an alias can allow or deny permission to the KMS key. For details, see [ABAC for AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/abac.html) in the *AWS Key Management Service Developer Guide* . \n\nUsing an alias to refer to a KMS key can help you simplify key management. For example, an alias in your code can be associated with different KMS keys in different AWS Regions . For more information, see [Using aliases](https://docs.aws.amazon.com/kms/latest/developerguide/kms-alias.html) in the *AWS Key Management Service Developer Guide* .\n\nWhen specifying an alias, observe the following rules.\n\n- Each alias is associated with one KMS key, but multiple aliases can be associated with the same KMS key.\n- The alias and its associated KMS key must be in the same AWS account and Region.\n- The alias name must be unique in the AWS account and Region. However, you can create aliases with the same name in different AWS Regions . For example, you can have an `alias/projectKey` in multiple Regions, each of which is associated with a KMS key in its Region.\n- Each alias name must begin with `alias/` followed by a name, such as `alias/exampleKey` . The alias name can contain only alphanumeric characters, forward slashes (/), underscores (_), and dashes (-). Alias names cannot begin with `alias/aws/` . That alias name prefix is reserved for [AWS managed keys](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#aws-managed-cmk) .", "properties": { "AliasName": "Specifies the alias name. This value must begin with `alias/` followed by a name, such as `alias/ExampleAlias` .\n\n> If you change the value of a `Replacement` property, such as `AliasName` , the existing alias is deleted and a new alias is created for the specified KMS key. This change can disrupt applications that use the alias. It can also allow or deny access to a KMS key affected by attribute-based access control (ABAC). \n\nThe alias must be string of 1-256 characters. It can contain only alphanumeric characters, forward slashes (/), underscores (_), and dashes (-). The alias name cannot begin with `alias/aws/` . The `alias/aws/` prefix is reserved for [AWS managed keys](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#aws-managed-cmk) .\n\n*Pattern* : `alias/^[a-zA-Z0-9/_-]+$`\n\n*Minimum* : `1`\n\n*Maximum* : `256`", "TargetKeyId": "Associates the alias with the specified [customer managed key](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#customer-cmk) . The KMS key must be in the same AWS account and Region.\n\nA valid key ID is required. If you supply a null or empty string value, this operation returns an error.\n\nFor help finding the key ID and ARN, see [Finding the key ID and ARN](https://docs.aws.amazon.com/kms/latest/developerguide/viewing-keys.html#find-cmk-id-arn) in the *AWS Key Management Service Developer Guide* .\n\nSpecify the key ID or the key ARN of the KMS key.\n\nFor example:\n\n- Key ID: `1234abcd-12ab-34cd-56ef-1234567890ab`\n- Key ARN: `arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab`\n\nTo get the key ID and key ARN for a KMS key, use [ListKeys](https://docs.aws.amazon.com/kms/latest/APIReference/API_ListKeys.html) or [DescribeKey](https://docs.aws.amazon.com/kms/latest/APIReference/API_DescribeKey.html) ." @@ -22518,7 +22932,7 @@ "KeyId": "The key ID of the KMS key, such as `1234abcd-12ab-34cd-56ef-1234567890ab` .\n\nFor information about the key ID of a KMS key, see [Key ID](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#key-id-key-id) in the *AWS Key Management Service Developer Guide* .", "Ref": "`Ref` returns the key ID, such as `1234abcd-12ab-34cd-56ef-1234567890ab` ." }, - "description": "The `AWS::KMS::Key` resource specifies a [symmetric or asymmetric](https://docs.aws.amazon.com/kms/latest/developerguide/symmetric-asymmetric.html) [KMS key](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#kms_keys) in AWS Key Management Service ( AWS KMS ).\n\n> AWS KMS is replacing the term *customer master key (CMK)* with *AWS KMS key* and *KMS key* . The concept has not changed. To prevent breaking changes, AWS KMS is keeping some variations of this term. \n\nYou can use symmetric KMS keys to encrypt and decrypt small amounts of data, but they are more commonly used to generate data keys and data key pairs. You can also use symmetric KMS key to encrypt data stored in AWS services that are [integrated with AWS KMS](https://docs.aws.amazon.com//kms/features/#AWS_Service_Integration) . For more information, see [What is AWS Key Management Service ?](https://docs.aws.amazon.com/kms/latest/developerguide/overview.html) in the *AWS Key Management Service Developer Guide* .\n\nYou can use asymmetric KMS keys to encrypt and decrypt data or sign messages and verify signatures. To create an asymmetric key, you must specify an asymmetric `KeySpec` value and a `KeyUsage` value.\n\n> If you change the value of a `Replacement` property, such as `KeyUsage` or `KeySpec` , on an existing KMS key, the existing KMS key is [scheduled for deletion](https://docs.aws.amazon.com/kms/latest/developerguide/deleting-keys.html) and a new KMS key is created with the specified value.\n> \n> While scheduled for deletion, the existing KMS key becomes unusable. If you don't [cancel the scheduled deletion](https://docs.aws.amazon.com/kms/latest/developerguide/deleting-keys.html#deleting-keys-scheduling-key-deletion) of the existing KMS key outside of CloudFormation, all data encrypted under the existing KMS key becomes unrecoverable when the KMS key is deleted.", + "description": "The `AWS::KMS::Key` resource specifies a [symmetric or asymmetric](https://docs.aws.amazon.com/kms/latest/developerguide/symmetric-asymmetric.html) [KMS key](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#kms_keys) in AWS Key Management Service ( AWS KMS ).\n\nYou can use the `AWS::KMS::Key` resource to specify a symmetric or asymmetric multi-Region primary key. To specify a replica key, use the [AWS::KMS::ReplicaKey](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kms-replicakey.html) resource. For information about multi-Region keys, see [Multi-Region keys](https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html) in the *AWS Key Management Service Developer Guide* .\n\nYou cannot use the `AWS::KMS::Key` resource to specify a KMS key with [imported key material](https://docs.aws.amazon.com/kms/latest/developerguide/importing-keys.html) or a KMS key in a [custom key store](https://docs.aws.amazon.com/kms/latest/developerguide/custom-key-store-overview.html) .\n\n> AWS KMS is replacing the term *customer master key (CMK)* with *AWS KMS key* and *KMS key* . The concept has not changed. To prevent breaking changes, AWS KMS is keeping some variations of this term. \n\nYou can use symmetric KMS keys to encrypt and decrypt small amounts of data, but they are more commonly used to generate data keys and data key pairs. You can also use symmetric KMS key to encrypt data stored in AWS services that are [integrated with AWS KMS](https://docs.aws.amazon.com//kms/features/#AWS_Service_Integration) . For more information, see [What is AWS Key Management Service ?](https://docs.aws.amazon.com/kms/latest/developerguide/overview.html) in the *AWS Key Management Service Developer Guide* .\n\nYou can use asymmetric KMS keys to encrypt and decrypt data or sign messages and verify signatures. To create an asymmetric key, you must specify an asymmetric `KeySpec` value and a `KeyUsage` value.\n\n> If you change the value of the `KeyUsage` , `KeySpec` , or `MultiRegion` property on an existing KMS key, the existing KMS key is [scheduled for deletion](https://docs.aws.amazon.com/kms/latest/developerguide/deleting-keys.html) and a new KMS key is created with the specified value.\n> \n> While scheduled for deletion, the existing KMS key becomes unusable. If you don't [cancel the scheduled deletion](https://docs.aws.amazon.com/kms/latest/developerguide/deleting-keys.html#deleting-keys-scheduling-key-deletion) of the existing KMS key outside of CloudFormation, all data encrypted under the existing KMS key becomes unrecoverable when the KMS key is deleted. \n\n*Regions*\n\nAWS KMS CloudFormation resources are supported in all Regions in which AWS CloudFormation is supported. However, in the (ap-southeast-3), you cannot use a CloudFormation template to create or manage asymmetric KMS keys or multi-Region KMS keys (primary or replica).", "properties": { "Description": "A description of the KMS key. Use a description that helps you to distinguish this KMS key from others in the account, such as its intended use.", "EnableKeyRotation": "Enables automatic rotation of the key material for the specified KMS key. By default, automatic key rotation is not enabled.\n\nAWS KMS does not support automatic key rotation on asymmetric KMS keys. For asymmetric KMS keys, omit the `EnableKeyRotation` property or set it to `false` .\n\nWhen you enable automatic rotation, AWS KMS automatically creates new key material for the KMS key 365 days after the enable (or reenable) date and every 365 days thereafter. AWS KMS retains all key material until you delete the KMS key. For detailed information about automatic key rotation, see [Rotating KMS keys](https://docs.aws.amazon.com/kms/latest/developerguide/rotate-keys.html) in the *AWS Key Management Service Developer Guide* .", @@ -22526,9 +22940,9 @@ "KeyPolicy": "The key policy that authorizes use of the KMS key. The key policy must conform to the following rules.\n\n- The key policy must allow the caller to make a subsequent [PutKeyPolicy](https://docs.aws.amazon.com/kms/latest/APIReference/API_PutKeyPolicy.html) request on the KMS key. This reduces the risk that the KMS key becomes unmanageable. For more information, refer to the scenario in the [Default key policy](https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html#key-policy-default-allow-root-enable-iam) section of the **AWS Key Management Service Developer Guide** .\n- Each statement in the key policy must contain one or more principals. The principals in the key policy must exist and be visible to AWS KMS . When you create a new AWS principal (for example, an IAM user or role), you might need to enforce a delay before including the new principal in a key policy because the new principal might not be immediately visible to AWS KMS . For more information, see [Changes that I make are not always immediately visible](https://docs.aws.amazon.com/IAM/latest/UserGuide/troubleshoot_general.html#troubleshoot_general_eventual-consistency) in the *AWS Identity and Access Management User Guide* .\n- The key policy size limit is 32 kilobytes (32768 bytes).\n\nIf you are unsure of which policy to use, consider the *default key policy* . This is the key policy that AWS KMS applies to KMS keys that are created by using the CreateKey API with no specified key policy. It gives the AWS account that owns the key permission to perform all operations on the key. It also allows you write IAM policies to authorize access to the key. For details, see [Default key policy](https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html#key-policy-default) in the *AWS Key Management Service Developer Guide* .\n\n*Minimum* : `1`\n\n*Maximum* : `32768`", "KeySpec": "Specifies the type of KMS key to create. The default value, `SYMMETRIC_DEFAULT` , creates a KMS key with a 256-bit symmetric key for encryption and decryption. For help choosing a key spec for your KMS key, see [How to choose Your KMS key configuration](https://docs.aws.amazon.com/kms/latest/developerguide/symm-asymm-choose.html) in the *AWS Key Management Service Developer Guide* .\n\nThe `KeySpec` property determines whether the KMS key contains a symmetric key or an asymmetric key pair. It also determines the encryption algorithms or signing algorithms that the KMS key supports. You can't change the `KeySpec` after the KMS key is created. To further restrict the algorithms that can be used with the KMS key, use a condition key in its key policy or IAM policy. For more information, see [kms:EncryptionAlgorithm](https://docs.aws.amazon.com/kms/latest/developerguide/policy-conditions.html#conditions-kms-encryption-algorithm) or [kms:Signing Algorithm](https://docs.aws.amazon.com/kms/latest/developerguide/policy-conditions.html#conditions-kms-signing-algorithm) in the *AWS Key Management Service Developer Guide* .\n\n> If you change the `KeySpec` of an existing KMS key, the existing KMS key is scheduled for deletion and a new KMS key is created with the specified `KeySpec` value. While the scheduled deletion is pending, you can't use the existing KMS key. Unless you [cancel the scheduled deletion](https://docs.aws.amazon.com/kms/latest/developerguide/deleting-keys.html#deleting-keys-scheduling-key-deletion) of the KMS key outside of CloudFormation, all data encrypted under the existing KMS key becomes unrecoverable when the KMS key is deleted. > [AWS services that are integrated with AWS KMS](https://docs.aws.amazon.com/kms/features/#AWS_Service_Integration) use symmetric KMS keys to protect your data. These services do not support asymmetric KMS keys. For help determining whether a KMS key is symmetric or asymmetric, see [Identifying Symmetric and Asymmetric KMS keys](https://docs.aws.amazon.com/kms/latest/developerguide/find-symm-asymm.html) in the *AWS Key Management Service Developer Guide* . \n\nAWS KMS supports the following key specs for KMS keys:\n\n- Symmetric key (default)\n\n- `SYMMETRIC_DEFAULT` (AES-256-GCM)\n- Asymmetric RSA key pairs\n\n- `RSA_2048`\n- `RSA_3072`\n- `RSA_4096`\n- Asymmetric NIST-recommended elliptic curve key pairs\n\n- `ECC_NIST_P256` (secp256r1)\n- `ECC_NIST_P384` (secp384r1)\n- `ECC_NIST_P521` (secp521r1)\n- Other asymmetric elliptic curve key pairs\n\n- `ECC_SECG_P256K1` (secp256k1), commonly used for cryptocurrencies.", "KeyUsage": "Determines the [cryptographic operations](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#cryptographic-operations) for which you can use the KMS key. The default value is `ENCRYPT_DECRYPT` . This property is required only for asymmetric KMS keys. You can't change the `KeyUsage` value after the KMS key is created.\n\n> If you change the `KeyUsage` of an existing KMS key, the existing KMS key is scheduled for deletion and a new KMS key is created with the specified `KeyUsage` value. While the scheduled deletion is pending, you can't use the existing KMS key. Unless you [cancel the scheduled deletion](https://docs.aws.amazon.com/kms/latest/developerguide/deleting-keys.html#deleting-keys-scheduling-key-deletion) of the KMS key outside of CloudFormation, all data encrypted under the existing KMS key becomes unrecoverable when the KMS key is deleted. \n\nSelect only one valid value.\n\n- For symmetric KMS keys, omit the property or specify `ENCRYPT_DECRYPT` .\n- For asymmetric KMS keys with RSA key material, specify `ENCRYPT_DECRYPT` or `SIGN_VERIFY` .\n- For asymmetric KMS keys with ECC key material, specify `SIGN_VERIFY` .", - "MultiRegion": "Creates a multi-Region primary key that you can replicate in other AWS Regions .\n\n> If you change the `MultiRegion` property of an existing KMS key, the existing KMS key is scheduled for deletion and a new KMS key is created with the specified `Multi-Region` value. While the scheduled deletion is pending, you can't use the existing KMS key. Unless you [cancel the scheduled deletion](https://docs.aws.amazon.com/kms/latest/developerguide/deleting-keys.html#deleting-keys-scheduling-key-deletion) of the KMS key outside of CloudFormation, all data encrypted under the existing KMS key becomes unrecoverable when the KMS key is deleted. \n\nFor a multi-Region key, set to this property to `true` . For a single-Region key, omit this property or set it to `false` . The default value is `false` .\n\n*Multi-Region keys* are an AWS KMS feature that lets you create multiple interoperable KMS keys in different AWS Regions . Because these KMS keys have the same key ID, key material, and other metadata, you can use them to encrypt data in one AWS Region and decrypt it in a different AWS Region without making a cross-Region call or exposing the plaintext data. For more information, see [Using multi-Region keys](https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html) in the *AWS Key Management Service Developer Guide* .\n\nYou can create a symmetric or asymmetric multi-Region key, and you can create a multi-Region key with imported key material. However, you cannot create a multi-Region key in a custom key store.\n\nTo create a replica of this primary key in a different AWS Region , create an [AWS::KMS::ReplicaKey](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kms-replicakey.html) resource in a CloudFormation stack in the replica Region. Specify the key ARN of this primary key.", + "MultiRegion": "Creates a multi-Region primary key that you can replicate in other AWS Regions .\n\n> If you change the `MultiRegion` property of an existing KMS key, the existing KMS key is scheduled for deletion and a new KMS key is created with the specified `Multi-Region` value. While the scheduled deletion is pending, you can't use the existing KMS key. Unless you [cancel the scheduled deletion](https://docs.aws.amazon.com/kms/latest/developerguide/deleting-keys.html#deleting-keys-scheduling-key-deletion) of the KMS key outside of CloudFormation, all data encrypted under the existing KMS key becomes unrecoverable when the KMS key is deleted. \n\nFor a multi-Region key, set to this property to `true` . For a single-Region key, omit this property or set it to `false` . The default value is `false` .\n\n*Multi-Region keys* are an AWS KMS feature that lets you create multiple interoperable KMS keys in different AWS Regions . Because these KMS keys have the same key ID, key material, and other metadata, you can use them to encrypt data in one AWS Region and decrypt it in a different AWS Region without making a cross-Region call or exposing the plaintext data. For more information, see [Multi-Region keys](https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html) in the *AWS Key Management Service Developer Guide* .\n\nYou can create a symmetric or asymmetric multi-Region key, and you can create a multi-Region key with imported key material. However, you cannot create a multi-Region key in a custom key store.\n\nTo create a replica of this primary key in a different AWS Region , create an [AWS::KMS::ReplicaKey](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kms-replicakey.html) resource in a CloudFormation stack in the replica Region. Specify the key ARN of this primary key.", "PendingWindowInDays": "Specifies the number of days in the waiting period before AWS KMS deletes a KMS key that has been removed from a CloudFormation stack. Enter a value between 7 and 30 days. The default value is 30 days.\n\nWhen you remove a KMS key from a CloudFormation stack, AWS KMS schedules the KMS key for deletion and starts the mandatory waiting period. The `PendingWindowInDays` property determines the length of waiting period. During the waiting period, the key state of KMS key is `Pending Deletion` or `Pending Replica Deletion` , which prevents the KMS key from being used in cryptographic operations. When the waiting period expires, AWS KMS permanently deletes the KMS key.\n\nAWS KMS will not delete a [multi-Region primary key](https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html) that has replica keys. If you remove a multi-Region primary key from a CloudFormation stack, its key state changes to `PendingReplicaDeletion` so it cannot be replicated or used in cryptographic operations. This state can persist indefinitely. When the last of its replica keys is deleted, the key state of the primary key changes to `PendingDeletion` and the waiting period specified by `PendingWindowInDays` begins. When this waiting period expires, AWS KMS deletes the primary key. For details, see [Deleting multi-Region keys](https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-delete.html) in the *AWS Key Management Service Developer Guide* .\n\nYou cannot use a CloudFormation template to cancel deletion of the KMS key after you remove it from the stack, regardless of the waiting period. If you specify a KMS key in your template, even one with the same name, CloudFormation creates a new KMS key. To cancel deletion of a KMS key, use the AWS KMS console or the [CancelKeyDeletion](https://docs.aws.amazon.com/kms/latest/APIReference/API_CancelKeyDeletion.html) operation.\n\nFor information about the `Pending Deletion` and `Pending Replica Deletion` key states, see [Key state: Effect on your KMS key](https://docs.aws.amazon.com/kms/latest/developerguide/key-state.html) in the *AWS Key Management Service Developer Guide* . For more information about deleting KMS keys, see the [ScheduleKeyDeletion](https://docs.aws.amazon.com/kms/latest/APIReference/API_ScheduleKeyDeletion.html) operation in the *AWS Key Management Service API Reference* and [Deleting KMS keys](https://docs.aws.amazon.com/kms/latest/developerguide/deleting-keys.html) in the *AWS Key Management Service Developer Guide* .\n\n*Minimum* : 7\n\n*Maximum* : 30", - "Tags": "Assigns one or more tags to the replica key.\n\n> Tagging or untagging a KMS key can allow or deny permission to the KMS key. For details, see [Using ABAC in AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/abac.html) in the *AWS Key Management Service Developer Guide* . \n\nFor information about tags in AWS KMS , see [Tagging keys](https://docs.aws.amazon.com/kms/latest/developerguide/tagging-keys.html) in the *AWS Key Management Service Developer Guide* . For information about tags in CloudFormation, see [Tag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) ." + "Tags": "Assigns one or more tags to the replica key.\n\n> Tagging or untagging a KMS key can allow or deny permission to the KMS key. For details, see [ABAC for AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/abac.html) in the *AWS Key Management Service Developer Guide* . \n\nFor information about tags in AWS KMS , see [Tagging keys](https://docs.aws.amazon.com/kms/latest/developerguide/tagging-keys.html) in the *AWS Key Management Service Developer Guide* . For information about tags in CloudFormation, see [Tag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) ." } }, "AWS::KMS::ReplicaKey": { @@ -22537,14 +22951,14 @@ "KeyId": "The key ID of the replica key, such as `mrk-1234abcd12ab34cd56ef1234567890ab` .\n\nRelated multi-Region keys have the same key ID. For information about the key IDs of multi-Region keys, see [How multi-Region keys work](https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html#mrk-how-it-works) in the *AWS Key Management Service Developer Guide* .", "Ref": "`Ref` returns the key ID, such as `mrk-1234abcd12ab34cd56ef1234567890ab` ." }, - "description": "The `AWS::KMS::ReplicaKey` resource specifies a multi-Region replica key that is based on a multi-Region primary key.\n\n*Multi-Region keys* are an AWS KMS feature that lets you create multiple interoperable KMS keys in different AWS Regions . Because these KMS keys have the same key ID, key material, and other metadata, you can use them to encrypt data in one AWS Region and decrypt it in a different AWS Region without making a cross-Region call or exposing the plaintext data. For more information, see [Using multi-Region keys](https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html) in the *AWS Key Management Service Developer Guide* .\n\nA multi-Region *primary key* is a fully functional symmetric or asymmetric KMS key that is also the model for replica keys in other AWS Regions . To create a multi-Region primary key, add an [AWS::KMS::Key](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kms-key.html) resource to your CloudFormation stack. Set its `MultiRegion` property to true.\n\nA multi-Region *replica key* is a fully functional symmetric or asymmetric KMS key that has the same key ID and key material as a multi-Region primary key, but is located in a different AWS Region of the same AWS partition. There can be multiple replicas of a primary key, but each must be in a different AWS Region .\n\nA primary key and its replicas have the same key ID and key material. They also have the same key spec, key usage, key material origin, and automatic key rotation status. These properties are known as *shared properties* . If they change, AWS KMS synchronizes the change to all related multi-Region keys. All other properties of a replica key can differ, including its key policy, tags, aliases, and key state. AWS KMS does not synchronize these properties.", + "description": "The `AWS::KMS::ReplicaKey` resource specifies a multi-Region replica key that is based on a multi-Region primary key.\n\n*Multi-Region keys* are an AWS KMS feature that lets you create multiple interoperable KMS keys in different AWS Regions . Because these KMS keys have the same key ID, key material, and other metadata, you can use them to encrypt data in one AWS Region and decrypt it in a different AWS Region without making a cross-Region call or exposing the plaintext data. For more information, see [Multi-Region keys](https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html) in the *AWS Key Management Service Developer Guide* .\n\nA multi-Region *primary key* is a fully functional symmetric or asymmetric KMS key that is also the model for replica keys in other AWS Regions . To create a multi-Region primary key, add an [AWS::KMS::Key](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kms-key.html) resource to your CloudFormation stack. Set its `MultiRegion` property to true.\n\nA multi-Region *replica key* is a fully functional symmetric or asymmetric KMS key that has the same key ID and key material as a multi-Region primary key, but is located in a different AWS Region of the same AWS partition. There can be multiple replicas of a primary key, but each must be in a different AWS Region .\n\nA primary key and its replicas have the same key ID and key material. They also have the same key spec, key usage, key material origin, and automatic key rotation status. These properties are known as *shared properties* . If they change, AWS KMS synchronizes the change to all related multi-Region keys. All other properties of a replica key can differ, including its key policy, tags, aliases, and key state. AWS KMS does not synchronize these properties.\n\n*Regions*\n\nAWS KMS CloudFormation resources are supported in all Regions in which AWS CloudFormation is supported. However, in the (ap-southeast-3), you cannot use a CloudFormation template to create or manage multi-Region KMS keys (primary or replica).", "properties": { "Description": "A description of the KMS key.\n\nThe default value is an empty string (no description).\n\nThe description is not a shared property of multi-Region keys. You can specify the same description or a different description for each key in a set of related multi-Region keys. AWS Key Management Service does not synchronize this property.", "Enabled": "Specifies whether the replica key is enabled. Disabled KMS keys cannot be used in cryptographic operations.\n\nWhen `Enabled` is `true` , the *key state* of the KMS key is `Enabled` . When `Enabled` is `false` , the key state of the KMS key is `Disabled` . The default value is `true` .\n\nThe actual key state of the replica might be affected by actions taken outside of CloudFormation, such as running the [EnableKey](https://docs.aws.amazon.com/kms/latest/APIReference/API_EnableKey.html) , [DisableKey](https://docs.aws.amazon.com/kms/latest/APIReference/API_DisableKey.html) , or [ScheduleKeyDeletion](https://docs.aws.amazon.com/kms/latest/APIReference/API_ScheduleKeyDeletion.html) operations. Also, while the replica key is being created, its key state is `Creating` . When the process is complete, the key state of the replica key changes to `Enabled` .\n\nFor information about the key states of a KMS key, see [Key state: Effect on your KMS key](https://docs.aws.amazon.com/kms/latest/developerguide/key-state.html) in the *AWS Key Management Service Developer Guide* .", "KeyPolicy": "The key policy that authorizes use of the replica key.\n\nThe key policy is not a shared property of multi-Region keys. You can specify the same key policy or a different key policy for each key in a set of related multi-Region keys. AWS KMS does not synchronize this property.\n\nThe key policy must conform to the following rules.\n\n- The key policy must give the caller [PutKeyPolicy](https://docs.aws.amazon.com/kms/latest/APIReference/API_PutKeyPolicy.html) permission on the KMS key. This reduces the risk that the KMS key becomes unmanageable. For more information, refer to the scenario in the [Default key policy](https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html#key-policy-default-allow-root-enable-iam) section of the **AWS Key Management Service Developer Guide** .\n- Each statement in the key policy must contain one or more principals. The principals in the key policy must exist and be visible to AWS KMS . When you create a new AWS principal (for example, an IAM user or role), you might need to enforce a delay before including the new principal in a key policy because the new principal might not be immediately visible to AWS KMS . For more information, see [Changes that I make are not always immediately visible](https://docs.aws.amazon.com/IAM/latest/UserGuide/troubleshoot_general.html#troubleshoot_general_eventual-consistency) in the *AWS Identity and Access Management User Guide* .\n- The key policy size limit is 32 kilobytes (32768 bytes).\n\n*Minimum* : `1`\n\n*Maximum* : `32768`", "PendingWindowInDays": "Specifies the number of days in the waiting period before AWS KMS deletes a replica key that has been removed from a CloudFormation stack. Enter a value between 7 and 30 days. The default value is 30 days.\n\nWhen you remove a replica key from a CloudFormation stack, AWS KMS schedules the replica key for deletion and starts the mandatory waiting period. The `PendingWindowInDays` property determines the length of waiting period. During the waiting period, the key state of replica key is `Pending Deletion` , which prevents it from being used in cryptographic operations. When the waiting period expires, AWS KMS permanently deletes the replica key.\n\nYou cannot use a CloudFormation template to cancel deletion of the replica after you remove it from the stack, regardless of the waiting period. However, if you specify a replica key in your template that is based on the same primary key as the original replica key, CloudFormation creates a new replica key with the same key ID, key material, and other shared properties of the original replica key. This new replica key can decrypt ciphertext that was encrypted under the original replica key, or any related multi-Region key.\n\nFor detailed information about deleting multi-Region keys, see [Deleting multi-Region keys](https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-delete.html) in the *AWS Key Management Service Developer Guide* .\n\nFor information about the `PendingDeletion` key state, see [Key state: Effect on your KMS key](https://docs.aws.amazon.com/kms/latest/developerguide/key-state.html) in the *AWS Key Management Service Developer Guide* . For more information about deleting KMS keys, see the [ScheduleKeyDeletion](https://docs.aws.amazon.com/kms/latest/APIReference/API_ScheduleKeyDeletion.html) operation in the *AWS Key Management Service API Reference* and [Deleting KMS keys](https://docs.aws.amazon.com/kms/latest/developerguide/deleting-keys.html) in the *AWS Key Management Service Developer Guide* .\n\n*Minimum* : 7\n\n*Maximum* : 30", "PrimaryKeyArn": "Specifies the multi-Region primary key to replicate. The primary key must be in a different AWS Region of the same AWS partition. You can create only one replica of a given primary key in each AWS Region .\n\n> If you change the `PrimaryKeyArn` value of a replica key, the existing replica key is scheduled for deletion and a new replica key is created based on the specified primary key. While it is scheduled for deletion, the existing replica key becomes unusable. You can cancel the scheduled deletion of the key outside of CloudFormation.\n> \n> However, if you inadvertently delete a replica key, you can decrypt ciphertext encrypted by that replica key by using any related multi-Region key. If necessary, you can recreate the replica in the same Region after the previous one is completely deleted. For details, see [Deleting multi-Region keys](https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-delete.html) in the *AWS Key Management Service Developer Guide* \n\nSpecify the key ARN of an existing multi-Region primary key. For example, `arn:aws:kms:us-east-2:111122223333:key/mrk-1234abcd12ab34cd56ef1234567890ab` .", - "Tags": "Assigns one or more tags to the replica key.\n\n> Tagging or untagging a KMS key can allow or deny permission to the KMS key. For details, see [Using ABAC in AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/abac.html) in the *AWS Key Management Service Developer Guide* . \n\nTags are not a shared property of multi-Region keys. You can specify the same tags or different tags for each key in a set of related multi-Region keys. AWS KMS does not synchronize this property.\n\nEach tag consists of a tag key and a tag value. Both the tag key and the tag value are required, but the tag value can be an empty (null) string. You cannot have more than one tag on a KMS key with the same tag key. If you specify an existing tag key with a different tag value, AWS KMS replaces the current tag value with the specified one.\n\nWhen you assign tags to an AWS resource, AWS generates a cost allocation report with usage and costs aggregated by tags. Tags can also be used to control access to a KMS key. For details, see [Tagging keys](https://docs.aws.amazon.com/kms/latest/developerguide/tagging-keys.html) ." + "Tags": "Assigns one or more tags to the replica key.\n\n> Tagging or untagging a KMS key can allow or deny permission to the KMS key. For details, see [ABAC for AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/abac.html) in the *AWS Key Management Service Developer Guide* . \n\nTags are not a shared property of multi-Region keys. You can specify the same tags or different tags for each key in a set of related multi-Region keys. AWS KMS does not synchronize this property.\n\nEach tag consists of a tag key and a tag value. Both the tag key and the tag value are required, but the tag value can be an empty (null) string. You cannot have more than one tag on a KMS key with the same tag key. If you specify an existing tag key with a different tag value, AWS KMS replaces the current tag value with the specified one.\n\nWhen you assign tags to an AWS resource, AWS generates a cost allocation report with usage and costs aggregated by tags. Tags can also be used to control access to a KMS key. For details, see [Tagging keys](https://docs.aws.amazon.com/kms/latest/developerguide/tagging-keys.html) ." } }, "AWS::Kendra::DataSource": { @@ -22594,29 +23008,29 @@ "attributes": {}, "description": "Specifies the attachment settings for the Confluence data source. Attachment settings are optional, if you don't specify settings attachments, Amazon Kendra won't index them.", "properties": { - "AttachmentFieldMappings": "Defines how attachment metadata fields should be mapped to index fields. Before you can map a field, you must first create an index field with a matching type using the console or the `UpdateIndex` operation.\n\nIf you specify the `AttachentFieldMappings` parameter, you must specify at least one field mapping.", + "AttachmentFieldMappings": "Defines how attachment metadata fields should be mapped to index fields. Before you can map a field, you must first create an index field with a matching type using the console or the `UpdateIndex` API.\n\nIf you specify the `AttachentFieldMappings` parameter, you must specify at least one field mapping.", "CrawlAttachments": "Indicates whether Amazon Kendra indexes attachments to the pages and blogs in the Confluence data source." } }, "AWS::Kendra::DataSource.ConfluenceAttachmentToIndexFieldMapping": { "attributes": {}, - "description": "Defines the mapping between a field in the Confluence data source to a Amazon Kendra index field.\n\nYou must first create the index field using the `UpdateIndex` operation.", + "description": "Defines the mapping between a field in the Confluence data source to a Amazon Kendra index field.\n\nYou must first create the index field using the `UpdateIndex` API.", "properties": { - "DataSourceFieldName": "The name of the field in the data source.\n\nYou must first create the index field using the `UpdateIndex` operation.", + "DataSourceFieldName": "The name of the field in the data source.\n\nYou must first create the index field using the `UpdateIndex` API.", "DateFieldFormat": "The format for date fields in the data source. If the field specified in `DataSourceFieldName` is a date field you must specify the date format. If the field is not a date field, an exception is thrown.", "IndexFieldName": "The name of the index field to map to the Confluence data source field. The index field type must match the Confluence field type." } }, "AWS::Kendra::DataSource.ConfluenceBlogConfiguration": { "attributes": {}, - "description": "Specifies the blog settings for the Confluence data source. Blogs are always indexed unless filtered from the index by the `ExclusionPatterns` or `InclusionPatterns` fields in the `ConfluenceConfiguration` type.", + "description": "Specifies the blog settings for the Confluence data source. Blogs are always indexed unless filtered from the index by the `ExclusionPatterns` or `InclusionPatterns` fields in the `ConfluenceConfiguration` object.", "properties": { - "BlogFieldMappings": "Defines how blog metadata fields should be mapped to index fields. Before you can map a field, you must first create an index field with a matching type using the console or the `UpdateIndex` operation.\n\nIf you specify the `BlogFieldMappings` parameter, you must specify at least one field mapping." + "BlogFieldMappings": "Defines how blog metadata fields should be mapped to index fields. Before you can map a field, you must first create an index field with a matching type using the console or the `UpdateIndex` API.\n\nIf you specify the `BlogFieldMappings` parameter, you must specify at least one field mapping." } }, "AWS::Kendra::DataSource.ConfluenceBlogToIndexFieldMapping": { "attributes": {}, - "description": "Defines the mapping between a blog field in the Confluence data source to a Amazon Kendra index field.\n\nYou must first create the index field using the `UpdateIndex` operation.", + "description": "Defines the mapping between a blog field in the Confluence data source to a Amazon Kendra index field.\n\nYou must first create the index field using the `UpdateIndex` API.", "properties": { "DataSourceFieldName": "The name of the field in the data source.", "DateFieldFormat": "The format for date fields in the data source. If the field specified in `DataSourceFieldName` is a date field you must specify the date format. If the field is not a date field, an exception is thrown.", @@ -22625,7 +23039,7 @@ }, "AWS::Kendra::DataSource.ConfluenceConfiguration": { "attributes": {}, - "description": "Provides configuration information for data sources that connect to Confluence.", + "description": "Provides the configuration information to connect to Confluence as your data source.", "properties": { "AttachmentConfiguration": "Specifies configuration information for indexing attachments to Confluence blogs and pages.", "BlogConfiguration": "Specifies configuration information for indexing Confluence blogs.", @@ -22643,12 +23057,12 @@ "attributes": {}, "description": "Specifies the page settings for the Confluence data source.", "properties": { - "PageFieldMappings": "Defines how page metadata fields should be mapped to index fields. Before you can map a field, you must first create an index field with a matching type using the console or the `UpdateIndex` operation.\n\nIf you specify the `PageFieldMappings` parameter, you must specify at least one field mapping." + "PageFieldMappings": "Defines how page metadata fields should be mapped to index fields. Before you can map a field, you must first create an index field with a matching type using the console or the `UpdateIndex` API.\n\nIf you specify the `PageFieldMappings` parameter, you must specify at least one field mapping." } }, "AWS::Kendra::DataSource.ConfluencePageToIndexFieldMapping": { "attributes": {}, - "description": "Defines the mapping between a field in the Confluence data source to a Amazon Kendra index field.\n\nYou must first create the index field using the `UpdateIndex` operation.", + "description": "Defines the mapping between a field in the Confluence data source to a Amazon Kendra index field.\n\nYou must first create the index field using the `UpdateIndex` API.", "properties": { "DataSourceFieldName": "The name of the field in the data source.", "DateFieldFormat": "The format for date fields in the data source. If the field specified in `DataSourceFieldName` is a date field you must specify the date format. If the field is not a date field, an exception is thrown.", @@ -22663,12 +23077,12 @@ "CrawlPersonalSpaces": "Specifies whether Amazon Kendra should index personal spaces. Users can add restrictions to items in personal spaces. If personal spaces are indexed, queries without user context information may return restricted items from a personal space in their results. For more information, see [Filtering on user context](https://docs.aws.amazon.com/kendra/latest/dg/user-context-filter.html) .", "ExcludeSpaces": "A list of space keys of Confluence spaces. If you include a key, the blogs, documents, and attachments in the space are not indexed. If a space is in both the `ExcludeSpaces` and the `IncludeSpaces` list, the space is excluded.", "IncludeSpaces": "A list of space keys for Confluence spaces. If you include a key, the blogs, documents, and attachments in the space are indexed. Spaces that aren't in the list aren't indexed. A space in the list must exist. Otherwise, Amazon Kendra logs an error when the data source is synchronized. If a space is in both the `IncludeSpaces` and the `ExcludeSpaces` list, the space is excluded.", - "SpaceFieldMappings": "Defines how space metadata fields should be mapped to index fields. Before you can map a field, you must first create an index field with a matching type using the console or the `UpdateIndex` operation.\n\nIf you specify the `SpaceFieldMappings` parameter, you must specify at least one field mapping." + "SpaceFieldMappings": "Defines how space metadata fields should be mapped to index fields. Before you can map a field, you must first create an index field with a matching type using the console or the `UpdateIndex` API.\n\nIf you specify the `SpaceFieldMappings` parameter, you must specify at least one field mapping." } }, "AWS::Kendra::DataSource.ConfluenceSpaceToIndexFieldMapping": { "attributes": {}, - "description": "Defines the mapping between a field in the Confluence data source to a Amazon Kendra index field.\n\nYou must first create the index field using the `UpdateIndex` operation.", + "description": "Defines the mapping between a field in the Confluence data source to an Amazon Kendra index field.\n\nYou must first create the index field using the `UpdateIndex` API.", "properties": { "DataSourceFieldName": "The name of the field in the data source.", "DateFieldFormat": "The format for date fields in the data source. If the field specified in `DataSourceFieldName` is a date field you must specify the date format. If the field is not a date field, an exception is thrown.", @@ -22677,7 +23091,7 @@ }, "AWS::Kendra::DataSource.ConnectionConfiguration": { "attributes": {}, - "description": "Provides the information necessary to connect to a database.", + "description": "Provides the configuration information that's required to connect to a database.", "properties": { "DatabaseHost": "The name of the host for the database. Can be either a string (host.subdomain.domain.tld) or an IPv4 or IPv6 address.", "DatabaseName": "The name of the database containing the document data.", @@ -22688,18 +23102,18 @@ }, "AWS::Kendra::DataSource.DataSourceConfiguration": { "attributes": {}, - "description": "Configuration information for an Amazon Kendra data source.", + "description": "Provides the configuration information for an Amazon Kendra data source.", "properties": { "ConfluenceConfiguration": "Provides configuration information for connecting to a Confluence data source.", - "DatabaseConfiguration": "Provides information necessary to create a data source connector for a database.", - "GoogleDriveConfiguration": "Provides configuration for data sources that connect to Google Drive.", - "OneDriveConfiguration": "Provides configuration for data sources that connect to Microsoft OneDrive.", - "S3Configuration": "Provides information to create a data source connector for a document repository in an Amazon S3 bucket.", - "SalesforceConfiguration": "Provides configuration information for data sources that connect to a Salesforce site.", - "ServiceNowConfiguration": "Provides configuration for data sources that connect to ServiceNow instances.", - "SharePointConfiguration": "Provides information necessary to create a data source connector for a Microsoft SharePoint site.", + "DatabaseConfiguration": "Provides the configuration information to connect to a database as your data source.", + "GoogleDriveConfiguration": "Provides the configuration information to connect to Google Drive as your data source.", + "OneDriveConfiguration": "Provides the configuration information to connect to Microsoft OneDrive as your data source.", + "S3Configuration": "Provides the configuration information to connect to an Amazon S3 bucket as your data source.", + "SalesforceConfiguration": "Provides the configuration information to connect to Salesforce as your data source.", + "ServiceNowConfiguration": "Provides the configuration information to connect to ServiceNow as your data source.", + "SharePointConfiguration": "Provides the configuration information to connect to Microsoft SharePoint as your data source.", "WebCrawlerConfiguration": "Provides the configuration information required for Amazon Kendra Web Crawler.", - "WorkDocsConfiguration": "" + "WorkDocsConfiguration": "Provides the configuration information to connect to Amazon WorkDocs as your data source." } }, "AWS::Kendra::DataSource.DataSourceToIndexFieldMapping": { @@ -22713,7 +23127,7 @@ }, "AWS::Kendra::DataSource.DataSourceVpcConfiguration": { "attributes": {}, - "description": "Provides information for connecting to an Amazon VPC.", + "description": "Provides the configuration information to connect to an Amazon VPC.", "properties": { "SecurityGroupIds": "A list of identifiers of security groups within your Amazon VPC. The security groups should enable Amazon Kendra to connect to the data source.", "SubnetIds": "A list of identifiers for subnets within your Amazon VPC. The subnets should be able to connect to each other in the VPC, and they should have outgoing access to the Internet through a NAT device." @@ -22721,11 +23135,11 @@ }, "AWS::Kendra::DataSource.DatabaseConfiguration": { "attributes": {}, - "description": "Provides the information necessary to connect a database to an index.", + "description": "Provides the configuration information to connect to a index.", "properties": { "AclConfiguration": "Information about the database column that provides information for user context filtering.", "ColumnConfiguration": "Information about where the index should get the document information from the database.", - "ConnectionConfiguration": "The information necessary to connect to a database.", + "ConnectionConfiguration": "Configuration information that's required to connect to a database.", "DatabaseEngineType": "The type of database engine that runs the database.", "SqlConfiguration": "Provides information about how Amazon Kendra uses quote marks around SQL identifiers when querying a database data source.", "VpcConfiguration": "Provides information for connecting to an Amazon VPC." @@ -22740,20 +23154,20 @@ }, "AWS::Kendra::DataSource.GoogleDriveConfiguration": { "attributes": {}, - "description": "Provides configuration information for data sources that connect to Google Drive.", + "description": "Provides the configuration information to connect to Google Drive as your data source.", "properties": { "ExcludeMimeTypes": "A list of MIME types to exclude from the index. All documents matching the specified MIME type are excluded.\n\nFor a list of MIME types, see [Using a Google Workspace Drive data source](https://docs.aws.amazon.com/kendra/latest/dg/data-source-google-drive.html) .", "ExcludeSharedDrives": "A list of identifiers or shared drives to exclude from the index. All files and folders stored on the shared drive are excluded.", "ExcludeUserAccounts": "A list of email addresses of the users. Documents owned by these users are excluded from the index. Documents shared with excluded users are indexed unless they are excluded in another way.", "ExclusionPatterns": "A list of regular expression patterns that apply to the path on Google Drive. Items that match the pattern are excluded from the index from both shared drives and users' My Drives. Items that don't match the pattern are included in the index. If an item matches both an exclusion pattern and an inclusion pattern, it is excluded from the index.", - "FieldMappings": "Defines mapping between a field in the Google Drive and a Amazon Kendra index field.\n\nIf you are using the console, you can define index fields when creating the mapping. If you are using the API, you must first create the field using the `UpdateIndex` operation.", + "FieldMappings": "Defines mapping between a field in the Google Drive and a Amazon Kendra index field.\n\nIf you are using the console, you can define index fields when creating the mapping. If you are using the API, you must first create the field using the `UpdateIndex` API.", "InclusionPatterns": "A list of regular expression patterns that apply to path on Google Drive. Items that match the pattern are included in the index from both shared drives and users' My Drives. Items that don't match the pattern are excluded from the index. If an item matches both an inclusion pattern and an exclusion pattern, it is excluded from the index.", "SecretArn": "The Amazon Resource Name (ARN) of a AWS Secrets Manager secret that contains the credentials required to connect to Google Drive. For more information, see [Using a Google Workspace Drive data source](https://docs.aws.amazon.com/kendra/latest/dg/data-source-google-drive.html) ." } }, "AWS::Kendra::DataSource.OneDriveConfiguration": { "attributes": {}, - "description": "Provides configuration information for data sources that connect to OneDrive.", + "description": "Provides the configuration information to connect to OneDrive as your data source.", "properties": { "DisableLocalGroups": "A Boolean value that specifies whether local groups are disabled ( `True` ) or enabled ( `False` ).", "ExclusionPatterns": "List of regular expressions applied to documents. Items that match the exclusion pattern are not indexed. If you provide both an inclusion pattern and an exclusion pattern, any item that matches the exclusion pattern isn't indexed.\n\nThe exclusion pattern is applied to the file name.", @@ -22774,16 +23188,16 @@ }, "AWS::Kendra::DataSource.ProxyConfiguration": { "attributes": {}, - "description": "", + "description": "Provides the configuration information for a web proxy to connect to website hosts.", "properties": { - "Credentials": "", - "Host": "", - "Port": "" + "Credentials": "Your secret ARN, which you can create in [AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html)\n\nThe credentials are optional. You use a secret if web proxy credentials are required to connect to a website host. Amazon Kendra currently support basic authentication to connect to a web proxy server. The secret stores your credentials.", + "Host": "The name of the website host you want to connect to via a web proxy server.\n\nFor example, the host name of https://a.example.com/page1.html is \"a.example.com\".", + "Port": "The port number of the website host you want to connect to via a web proxy server.\n\nFor example, the port for https://a.example.com/page1.html is 443, the standard port for HTTPS." } }, "AWS::Kendra::DataSource.S3DataSourceConfiguration": { "attributes": {}, - "description": "Provides configuration information for a data source to index documents in an Amazon S3 bucket.", + "description": "Provides the configuration information to connect to an Amazon S3 bucket.", "properties": { "AccessControlListConfiguration": "Provides the path to the S3 bucket that contains the user context filtering files for the data source. For the format of the file, see [Access control for S3 data sources](https://docs.aws.amazon.com/kendra/latest/dg/s3-acl.html) .", "BucketName": "The name of the bucket that contains the documents.", @@ -22803,7 +23217,7 @@ }, "AWS::Kendra::DataSource.SalesforceChatterFeedConfiguration": { "attributes": {}, - "description": "Defines configuration for syncing a Salesforce chatter feed. The contents of the object comes from the Salesforce FeedItem table.", + "description": "The configuration information for syncing a Salesforce chatter feed. The contents of the object comes from the Salesforce FeedItem table.", "properties": { "DocumentDataFieldName": "The name of the column in the Salesforce FeedItem table that contains the content to index. Typically this is the `Body` column.", "DocumentTitleFieldName": "The name of the column in the Salesforce FeedItem table that contains the title of the document. This is typically the `Title` column.", @@ -22822,7 +23236,7 @@ "KnowledgeArticleConfiguration": "Specifies configuration information for the knowledge article types that Amazon Kendra indexes. Amazon Kendra indexes standard knowledge articles and the standard fields of knowledge articles, or the custom fields of custom knowledge articles, but not both.", "SecretArn": "The Amazon Resource Name (ARN) of an AWS Secrets Manager secret that contains the key/value pairs required to connect to your Salesforce instance. The secret must contain a JSON structure with the following keys:\n\n- authenticationUrl - The OAUTH endpoint that Amazon Kendra connects to get an OAUTH token.\n- consumerKey - The application public key generated when you created your Salesforce application.\n- consumerSecret - The application private key generated when you created your Salesforce application.\n- password - The password associated with the user logging in to the Salesforce instance.\n- securityToken - The token associated with the user account logging in to the Salesforce instance.\n- username - The user name of the user logging in to the Salesforce instance.", "ServerUrl": "The instance URL for the Salesforce site that you want to index.", - "StandardObjectAttachmentConfiguration": "Provides configuration information for processing attachments to Salesforce standard objects.", + "StandardObjectAttachmentConfiguration": "Configuration information for processing attachments to Salesforce standard objects.", "StandardObjectConfigurations": "Specifies the Salesforce standard objects that Amazon Kendra indexes." } }, @@ -22838,16 +23252,16 @@ }, "AWS::Kendra::DataSource.SalesforceKnowledgeArticleConfiguration": { "attributes": {}, - "description": "Specifies configuration information for the knowledge article types that Amazon Kendra indexes. Amazon Kendra indexes standard knowledge articles and the standard fields of knowledge articles, or the custom fields of custom knowledge articles, but not both", + "description": "Provides the configuration information for the knowledge article types that Amazon Kendra indexes. Amazon Kendra indexes standard knowledge articles and the standard fields of knowledge articles, or the custom fields of custom knowledge articles, but not both", "properties": { - "CustomKnowledgeArticleTypeConfigurations": "Provides configuration information for custom Salesforce knowledge articles.", + "CustomKnowledgeArticleTypeConfigurations": "Configuration information for custom Salesforce knowledge articles.", "IncludedStates": "Specifies the document states that should be included when Amazon Kendra indexes knowledge articles. You must specify at least one state.", - "StandardKnowledgeArticleTypeConfiguration": "Provides configuration information for standard Salesforce knowledge articles." + "StandardKnowledgeArticleTypeConfiguration": "Configuration information for standard Salesforce knowledge articles." } }, "AWS::Kendra::DataSource.SalesforceStandardKnowledgeArticleTypeConfiguration": { "attributes": {}, - "description": "Provides configuration information for standard Salesforce knowledge articles.", + "description": "Configuration information for standard Salesforce knowledge articles.", "properties": { "DocumentDataFieldName": "The name of the field that contains the document data to index.", "DocumentTitleFieldName": "The name of the field that contains the document title.", @@ -22856,7 +23270,7 @@ }, "AWS::Kendra::DataSource.SalesforceStandardObjectAttachmentConfiguration": { "attributes": {}, - "description": "Provides configuration information for processing attachments to Salesforce standard objects.", + "description": "Provides the configuration information for processing attachments to Salesforce standard objects.", "properties": { "DocumentTitleFieldName": "The name of the field used for the document title.", "FieldMappings": "One or more objects that map fields in attachments to Amazon Kendra index fields." @@ -22874,19 +23288,19 @@ }, "AWS::Kendra::DataSource.ServiceNowConfiguration": { "attributes": {}, - "description": "Provides configuration information required to connect to a ServiceNow data source.", + "description": "Provides the configuration information to connect to ServiceNow as your data source.", "properties": { "AuthenticationType": "Determines the type of authentication used to connect to the ServiceNow instance. If you choose `HTTP_BASIC` , Amazon Kendra is authenticated using the user name and password provided in the AWS Secrets Manager secret in the `SecretArn` field. When you choose `OAUTH2` , Amazon Kendra is authenticated using the OAuth token and secret provided in the Secrets Manager secret, and the user name and password are used to determine which information Amazon Kendra has access to.\n\nWhen you use `OAUTH2` authentication, you must generate a token and a client secret using the ServiceNow console. For more information, see [Using a ServiceNow data source](https://docs.aws.amazon.com/kendra/latest/dg/data-source-servicenow.html) .", "HostUrl": "The ServiceNow instance that the data source connects to. The host endpoint should look like the following: `{instance}.service-now.com.`", - "KnowledgeArticleConfiguration": "Provides configuration information for crawling knowledge articles in the ServiceNow site.", + "KnowledgeArticleConfiguration": "Configuration information for crawling knowledge articles in the ServiceNow site.", "SecretArn": "The Amazon Resource Name (ARN) of the AWS Secrets Manager secret that contains the user name and password required to connect to the ServiceNow instance.", - "ServiceCatalogConfiguration": "Provides configuration information for crawling service catalogs in the ServiceNow site.", + "ServiceCatalogConfiguration": "Configuration information for crawling service catalogs in the ServiceNow site.", "ServiceNowBuildVersion": "The identifier of the release that the ServiceNow host is running. If the host is not running the `LONDON` release, use `OTHERS` ." } }, "AWS::Kendra::DataSource.ServiceNowKnowledgeArticleConfiguration": { "attributes": {}, - "description": "Provides configuration information for crawling knowledge articles in the ServiceNow site.", + "description": "Provides the configuration information for crawling knowledge articles in the ServiceNow site.", "properties": { "CrawlAttachments": "Indicates whether Amazon Kendra should index attachments to knowledge articles.", "DocumentDataFieldName": "The name of the ServiceNow field that is mapped to the index document contents field in the Amazon Kendra index.", @@ -22899,7 +23313,7 @@ }, "AWS::Kendra::DataSource.ServiceNowServiceCatalogConfiguration": { "attributes": {}, - "description": "Provides configuration information for crawling service catalog items in the ServiceNow site", + "description": "Provides the configuration information for crawling service catalog items in the ServiceNow site", "properties": { "CrawlAttachments": "Indicates whether Amazon Kendra should crawl attachments to the service catalog items.", "DocumentDataFieldName": "The name of the ServiceNow field that is mapped to the index document contents field in the Amazon Kendra index.", @@ -22911,7 +23325,7 @@ }, "AWS::Kendra::DataSource.SharePointConfiguration": { "attributes": {}, - "description": "Provides configuration information for connecting to a Microsoft SharePoint data source.", + "description": "Provides the configuration information to connect to Microsoft SharePoint as your data source.", "properties": { "CrawlAttachments": "`TRUE` to include attachments to documents stored in your Microsoft SharePoint site in the index; otherwise, `FALSE` .", "DisableLocalGroups": "A Boolean value that specifies whether local groups are disabled ( `True` ) or enabled ( `False` ).", @@ -22924,7 +23338,7 @@ "SslCertificateS3Path": "Information required to find a specific file in an Amazon S3 bucket.", "Urls": "The URLs of the Microsoft SharePoint site that contains the documents that should be indexed.", "UseChangeLog": "Set to `TRUE` to use the Microsoft SharePoint change log to determine the documents that need to be updated in the index. Depending on the size of the SharePoint change log, it may take longer for Amazon Kendra to use the change log than it takes it to determine the changed documents using the Amazon Kendra document crawler.", - "VpcConfiguration": "" + "VpcConfiguration": "Provides information for connecting to an Amazon VPC." } }, "AWS::Kendra::DataSource.SqlConfiguration": { @@ -22952,17 +23366,17 @@ }, "AWS::Kendra::DataSource.WebCrawlerConfiguration": { "attributes": {}, - "description": "", + "description": "Provides the configuration information required for Amazon Kendra Web Crawler.", "properties": { - "AuthenticationConfiguration": "", - "CrawlDepth": "", - "MaxContentSizePerPageInMegaBytes": "", - "MaxLinksPerPage": "", - "MaxUrlsPerMinuteCrawlRate": "", - "ProxyConfiguration": "", - "UrlExclusionPatterns": "", - "UrlInclusionPatterns": "", - "Urls": "Specifies the seed or starting point URLs of the websites or the sitemap URLs of the websites you want to crawl.\n\nYou can include website subdomains. You can list up to 100 seed URLs and up to three sitemap URLs.\n\nYou can only crawl websites that use the secure communication protocol, Hypertext Transfer Protocol Secure (HTTPS). If you receive an error when crawling a website, it could be that the website is blocked from crawling.\n\n*When selecting websites to index, you must adhere to the [Amazon Acceptable Use Policy](https://docs.aws.amazon.com/aup/) and all other Amazon terms. Remember that you must only use the Amazon Kendra web crawler to index your own webpages, or webpages that you have authorization to index.*" + "AuthenticationConfiguration": "Configuration information required to connect to websites using authentication.\n\nYou can connect to websites using basic authentication of user name and password.\n\nYou must provide the website host name and port number. For example, the host name of https://a.example.com/page1.html is \"a.example.com\" and the port is 443, the standard port for HTTPS. You use a secret in [AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html) to store your authentication credentials.", + "CrawlDepth": "Specifies the number of levels in a website that you want to crawl.\n\nThe first level begins from the website seed or starting point URL. For example, if a website has 3 levels \u2013 index level (i.e. seed in this example), sections level, and subsections level \u2013 and you are only interested in crawling information up to the sections level (i.e. levels 0-1), you can set your depth to 1.\n\nThe default crawl depth is set to 2.", + "MaxContentSizePerPageInMegaBytes": "The maximum size (in MB) of a webpage or attachment to crawl.\n\nFiles larger than this size (in MB) are skipped/not crawled.\n\nThe default maximum size of a webpage or attachment is set to 50 MB.", + "MaxLinksPerPage": "The maximum number of URLs on a webpage to include when crawling a website. This number is per webpage.\n\nAs a website\u2019s webpages are crawled, any URLs the webpages link to are also crawled. URLs on a webpage are crawled in order of appearance.\n\nThe default maximum links per page is 100.", + "MaxUrlsPerMinuteCrawlRate": "The maximum number of URLs crawled per website host per minute.\n\nA minimum of one URL is required.\n\nThe default maximum number of URLs crawled per website host per minute is 300.", + "ProxyConfiguration": "Configuration information required to connect to your internal websites via a web proxy.\n\nYou must provide the website host name and port number. For example, the host name of https://a.example.com/page1.html is \"a.example.com\" and the port is 443, the standard port for HTTPS.\n\nWeb proxy credentials are optional and you can use them to connect to a web proxy server that requires basic authentication. To store web proxy credentials, you use a secret in [AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html) .", + "UrlExclusionPatterns": "The regular expression pattern to exclude certain URLs to crawl.\n\nIf there is a regular expression pattern to include certain URLs that conflicts with the exclude pattern, the exclude pattern takes precedence.", + "UrlInclusionPatterns": "The regular expression pattern to include certain URLs to crawl.\n\nIf there is a regular expression pattern to exclude certain URLs that conflicts with the include pattern, the exclude pattern takes precedence.", + "Urls": "Specifies the seed or starting point URLs of the websites or the sitemap URLs of the websites you want to crawl.\n\nYou can include website subdomains. You can list up to 100 seed URLs and up to three sitemap URLs.\n\nYou can only crawl websites that use the secure communication protocol, Hypertext Transfer Protocol Secure (HTTPS). If you receive an error when crawling a website, it could be that the website is blocked from crawling.\n\n*When selecting websites to index, you must adhere to the [Amazon Acceptable Use Policy](https://docs.aws.amazon.com/aup/) and all other Amazon terms. Remember that you must only use Amazon Kendra Web Crawler to index your own webpages, or webpages that you have authorization to index.*" } }, "AWS::Kendra::DataSource.WebCrawlerSeedUrlConfiguration": { @@ -22977,27 +23391,27 @@ "attributes": {}, "description": "Provides the configuration information of the sitemap URLs to crawl.\n\n*When selecting websites to index, you must adhere to the [Amazon Acceptable Use Policy](https://docs.aws.amazon.com/aup/) and all other Amazon terms. Remember that you must only use the Amazon Kendra web crawler to index your own webpages, or webpages that you have authorization to index.*", "properties": { - "SiteMaps": "" + "SiteMaps": "The list of sitemap URLs of the websites you want to crawl.\n\nThe list can include a maximum of three sitemap URLs." } }, "AWS::Kendra::DataSource.WebCrawlerUrls": { "attributes": {}, "description": "Specifies the seed or starting point URLs of the websites or the sitemap URLs of the websites you want to crawl.\n\nYou can include website subdomains. You can list up to 100 seed URLs and up to three sitemap URLs.\n\nYou can only crawl websites that use the secure communication protocol, Hypertext Transfer Protocol Secure (HTTPS). If you receive an error when crawling a website, it could be that the website is blocked from crawling.\n\n*When selecting websites to index, you must adhere to the [Amazon Acceptable Use Policy](https://docs.aws.amazon.com/aup/) and all other Amazon terms. Remember that you must only use the Amazon Kendra web crawler to index your own webpages, or webpages that you have authorization to index.*", "properties": { - "SeedUrlConfiguration": "Provides the configuration of the seed or starting point URLs of the websites you want to crawl.\n\nYou can choose to crawl only the website host names, or the website host names with subdomains, or the website host names with subdomains and other domains that the webpages link to.\n\nYou can list up to 100 seed URLs.", - "SiteMapsConfiguration": "Provides the configuration of the sitemap URLs of the websites you want to crawl.\n\nOnly URLs belonging to the same website host names are crawled. You can list up to three sitemap URLs." + "SeedUrlConfiguration": "Configuration of the seed or starting point URLs of the websites you want to crawl.\n\nYou can choose to crawl only the website host names, or the website host names with subdomains, or the website host names with subdomains and other domains that the webpages link to.\n\nYou can list up to 100 seed URLs.", + "SiteMapsConfiguration": "Configuration of the sitemap URLs of the websites you want to crawl.\n\nOnly URLs belonging to the same website host names are crawled. You can list up to three sitemap URLs." } }, "AWS::Kendra::DataSource.WorkDocsConfiguration": { "attributes": {}, - "description": "", + "description": "Provides the configuration information to connect to Amazon WorkDocs as your data source.\n\nAmazon WorkDocs connector is available in Oregon, North Virginia, Sydney, Singapore and Ireland regions.", "properties": { - "CrawlComments": "", - "ExclusionPatterns": "", - "FieldMappings": "", - "InclusionPatterns": "", - "OrganizationId": "", - "UseChangeLog": "" + "CrawlComments": "`TRUE` to include comments on documents in your index. Including comments in your index means each comment is a document that can be searched on.\n\nThe default is set to `FALSE` .", + "ExclusionPatterns": "A list of regular expression patterns to exclude certain files in your Amazon WorkDocs site repository. Files that match the patterns are excluded from the index. Files that don\u2019t match the patterns are included in the index. If a file matches both an inclusion pattern and an exclusion pattern, the exclusion pattern takes precedence and the file isn\u2019t included in the index.", + "FieldMappings": "A list of `DataSourceToIndexFieldMapping` objects that map Amazon WorkDocs field names to custom index field names in Amazon Kendra. You must first create the custom index fields using the `UpdateIndex` API before you map to Amazon WorkDocs fields. For more information, see [Mapping Data Source Fields](https://docs.aws.amazon.com/kendra/latest/dg/field-mapping.html) . The Amazon WorkDocs data source field names need to exist in your Amazon WorkDocs custom metadata.", + "InclusionPatterns": "A list of regular expression patterns to include certain files in your Amazon WorkDocs site repository. Files that match the patterns are included in the index. Files that don't match the patterns are excluded from the index. If a file matches both an inclusion pattern and an exclusion pattern, the exclusion pattern takes precedence and the file isn\u2019t included in the index.", + "OrganizationId": "The identifier of the directory corresponding to your Amazon WorkDocs site repository.\n\nYou can find the organization ID in the [AWS Directory Service](https://docs.aws.amazon.com/directoryservicev2/) by going to *Active Directory* , then *Directories* . Your Amazon WorkDocs site directory has an ID, which is the organization ID. You can also set up a new Amazon WorkDocs directory in the AWS Directory Service console and enable a Amazon WorkDocs site for the directory in the Amazon WorkDocs console.", + "UseChangeLog": "`TRUE` to use the change logs to update documents in your index instead of scanning all documents.\n\nIf you are syncing your Amazon WorkDocs data source with your index for the first time, all documents are scanned. After your first sync, you can use the change logs to update your documents in your index for future syncs.\n\nThe default is set to `FALSE` ." } }, "AWS::Kendra::Faq": { @@ -23065,7 +23479,7 @@ }, "AWS::Kendra::Index.JsonTokenTypeConfiguration": { "attributes": {}, - "description": "Configuration information for the JSON token type.", + "description": "Provides the configuration information for the JSON token type.", "properties": { "GroupAttributeField": "The group attribute field.", "UserNameAttributeField": "The user name attribute field." @@ -23073,7 +23487,7 @@ }, "AWS::Kendra::Index.JwtTokenTypeConfiguration": { "attributes": {}, - "description": "Configuration information for the JWT token type.", + "description": "Provides the configuration information for the JWT token type.", "properties": { "ClaimRegex": "The regular expression that identifies the claim.", "GroupAttributeField": "The group attribute field.", @@ -23114,7 +23528,7 @@ }, "AWS::Kendra::Index.UserTokenConfiguration": { "attributes": {}, - "description": "Provides configuration information for a token configuration.", + "description": "Provides the configuration information for a token.", "properties": { "JsonTokenTypeConfiguration": "Information about the JSON token type configuration.", "JwtTokenTypeConfiguration": "Information about the JWT token type configuration." @@ -23490,11 +23904,6 @@ "S3ContentLocation": "The location of the custom artifacts." } }, - "AWS::KinesisAnalyticsV2::Application.CustomArtifactsConfiguration": { - "attributes": {}, - "description": "A list of `CustomArtifactConfiguration` objects.", - "properties": {} - }, "AWS::KinesisAnalyticsV2::Application.DeployAsApplicationConfiguration": { "attributes": {}, "description": "The information required to deploy a Kinesis Data Analytics Studio notebook as an application with durable state.", @@ -24299,7 +24708,7 @@ "attributes": {}, "description": "A structure for the resource.", "properties": { - "DataLocationResource": "Currently not supported by AWS CloudFormation .", + "DataLocationResource": "A structure for a data location object where permissions are granted or revoked.", "DatabaseResource": "A structure for the database object.", "TableResource": "A structure for the table object. A table is a metadata definition that represents your data. You can Grant and Revoke table privileges to a principal.", "TableWithColumnsResource": "Currently not supported by AWS CloudFormation ." @@ -24312,12 +24721,12 @@ "CatalogId": "", "DatabaseName": "The name of the database for the table. Unique to a Data Catalog. A database is a set of associated table definitions organized into a logical group. You can Grant and Revoke database privileges to a principal.", "Name": "The name of the table.", - "TableWildcard": "" + "TableWildcard": "An empty object representing all tables under a database. If this field is specified instead of the `Name` field, all tables under `DatabaseName` will have permission changes applied." } }, "AWS::LakeFormation::Permissions.TableWildcard": { "attributes": {}, - "description": "", + "description": "A wildcard object representing every table under a database.", "properties": {} }, "AWS::LakeFormation::Permissions.TableWithColumnsResource": { @@ -24453,7 +24862,7 @@ "FilterCriteria": "(Streams and Amazon SQS) An object that defines the filter criteria that determine whether Lambda should process an event. For more information, see [Lambda event filtering](https://docs.aws.amazon.com/lambda/latest/dg/invocation-eventfiltering.html) .", "FunctionName": "The name of the Lambda function.\n\n**Name formats** - *Function name* - `MyFunction` .\n- *Function ARN* - `arn:aws:lambda:us-west-2:123456789012:function:MyFunction` .\n- *Version or Alias ARN* - `arn:aws:lambda:us-west-2:123456789012:function:MyFunction:PROD` .\n- *Partial ARN* - `123456789012:function:MyFunction` .\n\nThe length constraint applies only to the full ARN. If you specify only the function name, it's limited to 64 characters in length.", "FunctionResponseTypes": "(Streams and SQS) A list of current response type enums applied to the event source mapping.\n\nValid Values: `ReportBatchItemFailures`", - "MaximumBatchingWindowInSeconds": "(Streams and Amazon SQS standard queues) The maximum amount of time, in seconds, that Lambda spends gathering records before invoking the function.\n\nDefault: 0\n\nRelated setting: When you set `BatchSize` to a value greater than 10, you must set `MaximumBatchingWindowInSeconds` to at least 1.", + "MaximumBatchingWindowInSeconds": "The maximum amount of time, in seconds, that Lambda spends gathering records before invoking the function.\n\n*Default ( Kinesis , DynamoDB , Amazon SQS event sources)* : 0\n\n*Default ( Amazon MSK , Kafka, Amazon MQ event sources)* : 500 ms", "MaximumRecordAgeInSeconds": "(Streams only) Discard records older than the specified age. The default value is -1,\nwhich sets the maximum age to infinite. When the value is set to infinite, Lambda never discards old records.", "MaximumRetryAttempts": "(Streams only) Discard records after the specified number of retries. The default value is -1,\nwhich sets the maximum number of retries to infinite. When MaximumRetryAttempts is infinite, Lambda retries failed records until the record expires in the event source.", "ParallelizationFactor": "(Streams only) The number of batches to process concurrently from each shard. The default value is 1.", @@ -24709,6 +25118,13 @@ "Enabled": "Indicates whether an intent uses the dialog code hook during a conversation with a user." } }, + "AWS::Lex::Bot.ExternalSourceSetting": { + "attributes": {}, + "description": "Provides information about the external source of the slot type's definition.", + "properties": { + "GrammarSlotTypeSetting": "Settings required for a slot type based on a grammar that you provide." + } + }, "AWS::Lex::Bot.FulfillmentCodeHookSetting": { "attributes": {}, "description": "Determines if a Lambda function should be invoked for a specific intent.", @@ -24746,6 +25162,22 @@ "UpdateResponse": "Provides configuration information for messages sent periodically to the user while the fulfillment Lambda function is running." } }, + "AWS::Lex::Bot.GrammarSlotTypeSetting": { + "attributes": {}, + "description": "Settings required for a slot type based on a grammar that you provide.", + "properties": { + "Source": "The source of the grammar used to create the slot type." + } + }, + "AWS::Lex::Bot.GrammarSlotTypeSource": { + "attributes": {}, + "description": "Describes the Amazon S3 bucket name and location for the grammar that is the source of the slot type.", + "properties": { + "KmsKeyArn": "The AWS Key Management Service key required to decrypt the contents of the grammar, if any.", + "S3BucketName": "The name of the S3 bucket that contains the grammar source.", + "S3ObjectKey": "The path to the grammar in the S3 bucket." + } + }, "AWS::Lex::Bot.ImageResponseCard": { "attributes": {}, "description": "A card that is shown to the user by a messaging platform. You define the contents of the card, the card is displayed by the platform.\n\nWhen you use a response card, the response from the user is constrained to the text associated with a button on the card.", @@ -24858,7 +25290,7 @@ }, "AWS::Lex::Bot.PostFulfillmentStatusSpecification": { "attributes": {}, - "description": "Provides a setting that determines whether the post-fulfillment response is sent to the user. For more information, see [Post-fulfillment response](https://docs.aws.amazon.com/latest/dg/streaming-progress.html#progress-complete) in the *Amazon Lex developer guide* .", + "description": "Provides a setting that determines whether the post-fulfillment response is sent to the user. For more information, see [Post-fulfillment response](https://docs.aws.amazon.com/lex/latest/dg/streaming-progress.html#progress-complete) in the *Amazon Lex developer guide* .", "properties": { "FailureResponse": "Specifies a list of message groups that Amazon Lex uses to respond when fulfillment isn't successful.", "SuccessResponse": "Specifies a list of message groups that Amazon Lex uses to respond when the fulfillment is successful.", @@ -24951,6 +25383,7 @@ "description": "Describes a slot type.", "properties": { "Description": "A description of the slot type. Use the description to help identify the slot type in lists.", + "ExternalSourceSetting": "Sets the type of external information used to create the slot type.", "Name": "The name of the slot type. A slot type name must be unique withing the account.", "ParentSlotTypeSignature": "The built-in slot type used as a parent of this slot type. When you define a parent slot type, the new slot type has the configuration of the parent lot type.\n\nOnly AMAZON.AlphaNumeric is supported.", "SlotTypeValues": "A list of SlotTypeValue objects that defines the values that the slot type can take. Each value can have a list of synonyms, additional values that help train the machine learning model about the values that it resolves for the slot.", @@ -25140,6 +25573,14 @@ "SourceBotVersion": "The version of a bot used for a bot locale." } }, + "AWS::Lex::BotVersion.BotVersionLocaleSpecification": { + "attributes": {}, + "description": "Specifies the locale that Amazon Lex adds to this version. You can choose the Draft version or any other previously published version for each locale. When you specify a source version, the locale data is copied from the source version to the new version.", + "properties": { + "BotVersionLocaleDetails": "The version of a bot used for a bot locale.", + "LocaleId": "The identifier of the locale to add to the version." + } + }, "AWS::Lex::BotVersion.BotVersionLocaleSpecificationItem": { "attributes": {}, "description": "Specifies the details of a locale in a bot version.", @@ -25158,6 +25599,11 @@ "ResourceArn": "The Amazon Resource Name (ARN) of the bot or bot alias that the resource policy is attached to." } }, + "AWS::Lex::ResourcePolicy.Policy": { + "attributes": {}, + "description": "The resource policy to add to the resource. The policy is a JSON structure that contains one or more statements that define the policy. The policy must follow the IAM policy syntax. For more information about the contents of a JSON policy document, see the [IAM JSON policy reference](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies.html) .", + "properties": {} + }, "AWS::LicenseManager::Grant": { "attributes": { "GrantArn": "The Amazon Resource Name (ARN) of the grant.", @@ -25255,6 +25701,148 @@ "End": "End of the time range." } }, + "AWS::Lightsail::Alarm": { + "attributes": { + "AlarmArn": "The Amazon Resource Name (ARN) of the alarm.", + "Ref": "", + "State": "The current state of the alarm.\n\nAn alarm has the following possible states:\n\n- `ALARM` - The metric is outside of the defined threshold.\n- `INSUFFICIENT_DATA` - The alarm has recently started, the metric is not available, or not enough data is available for the metric to determine the alarm state.\n- `OK` - The metric is within the defined threshold." + }, + "description": "The `AWS::Lightsail::Alarm` resource specifies an alarm that can be used to monitor a single metric for one of your Lightsail resources.", + "properties": { + "AlarmName": "The name of the alarm.", + "ComparisonOperator": "The arithmetic operation to use when comparing the specified statistic and threshold.", + "ContactProtocols": "The contact protocols for the alarm, such as `Email` , `SMS` (text messaging), or both.\n\n*Allowed Values* : `Email` | `SMS`", + "DatapointsToAlarm": "The number of data points within the evaluation periods that must be breaching to cause the alarm to go to the `ALARM` state.", + "EvaluationPeriods": "The number of periods over which data is compared to the specified threshold.", + "MetricName": "The name of the metric associated with the alarm.", + "MonitoredResourceName": "The name of the Lightsail resource that the alarm monitors.", + "NotificationEnabled": "A Boolean value indicating whether the alarm is enabled.", + "NotificationTriggers": "The alarm states that trigger a notification.\n\n> To specify the `OK` and `INSUFFICIENT_DATA` values, you must also specify `ContactProtocols` values. Otherwise, the `OK` and `INSUFFICIENT_DATA` values will not take effect and the stack will drift. \n\n*Allowed Values* : `OK` | `ALARM` | `INSUFFICIENT_DATA`", + "Threshold": "The value against which the specified statistic is compared.", + "TreatMissingData": "Specifies how the alarm handles missing data points.\n\nAn alarm can treat missing data in the following ways:\n\n- `breaching` - Assumes the missing data is not within the threshold. Missing data counts towards the number of times that the metric is not within the threshold.\n- `notBreaching` - Assumes the missing data is within the threshold. Missing data does not count towards the number of times that the metric is not within the threshold.\n- `ignore` - Ignores the missing data. Maintains the current alarm state.\n- `missing` - Missing data is treated as missing." + } + }, + "AWS::Lightsail::Bucket": { + "attributes": { + "AbleToUpdateBundle": "A Boolean value indicating whether the bundle that is currently applied to your distribution can be changed to another bundle.", + "BucketArn": "The Amazon Resource Name (ARN) of the bucket.", + "Ref": "", + "Url": "The URL of the bucket." + }, + "description": "The `AWS::Lightsail::Bucket` resource specifies a bucket.", + "properties": { + "AccessRules": "An object that describes the access rules for the bucket.", + "BucketName": "The name of the bucket.", + "BundleId": "The bundle ID for the bucket (for example, `small_1_0` ).\n\nA bucket bundle specifies the monthly cost, storage space, and data transfer quota for a bucket.", + "ObjectVersioning": "Indicates whether object versioning is enabled for the bucket.\n\nThe following options can be configured:\n\n- `Enabled` - Object versioning is enabled.\n- `Suspended` - Object versioning was previously enabled but is currently suspended. Existing object versions are retained.\n- `NeverEnabled` - Object versioning has never been enabled.", + "ReadOnlyAccessAccounts": "An array of AWS account IDs that have read-only access to the bucket.", + "ResourcesReceivingAccess": "An array of Lightsail instances that have access to the bucket.", + "Tags": "An array of key-value pairs to apply to this resource.\n\nFor more information, see [Tag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) in the *AWS CloudFormation User Guide* .\n\n> The `Value` of `Tags` is optional for Lightsail resources." + } + }, + "AWS::Lightsail::Bucket.AccessRules": { + "attributes": {}, + "description": "`AccessRules` is a property of the [AWS::Lightsail::Bucket](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-bucket.html) resource. It describes access rules for a bucket.", + "properties": { + "AllowPublicOverrides": "A Boolean value indicating whether the access control list (ACL) permissions that are applied to individual objects override the `GetObject` option that is currently specified.\n\nWhen this is true, you can use the [PutObjectAcl](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectAcl.html) Amazon S3 API operation to set individual objects to public (read-only) or private, using either the `public-read` ACL or the `private` ACL.", + "GetObject": "Specifies the anonymous access to all objects in a bucket.\n\nThe following options can be specified:\n\n- `public` - Sets all objects in the bucket to public (read-only), making them readable by everyone on the internet.\n\nIf the `GetObject` value is set to `public` , then all objects in the bucket default to public regardless of the `allowPublicOverrides` value.\n- `private` - Sets all objects in the bucket to private, making them readable only by you and anyone that you grant access to.\n\nIf the `GetObject` value is set to `private` , and the `allowPublicOverrides` value is set to `true` , then all objects in the bucket default to private unless they are configured with a `public-read` ACL. Individual objects with a `public-read` ACL are readable by everyone on the internet." + } + }, + "AWS::Lightsail::Certificate": { + "attributes": { + "CertificateArn": "The Amazon Resource Name (ARN) of the certificate.", + "Ref": "", + "Status": "The validation status of the certificate." + }, + "description": "The `AWS::Lightsail::Certificate` resource specifies an SSL/TLS certificate that you can use with a content delivery network (CDN) distribution and a container service.\n\n> For information about certificates that you can use with a load balancer, see [AWS::Lightsail::LoadBalancerTlsCertificate](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-loadbalancertlscertificate.html) .", + "properties": { + "CertificateName": "The name of the certificate.", + "DomainName": "The domain name of the certificate.", + "SubjectAlternativeNames": "An array of strings that specify the alternate domains (such as `example.org` ) and subdomains (such as `blog.example.com` ) of the certificate.", + "Tags": "An array of key-value pairs to apply to this resource.\n\nFor more information, see [Tag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) in the *AWS CloudFormation User Guide* .\n\n> The `Value` of `Tags` is optional for Lightsail resources." + } + }, + "AWS::Lightsail::Container": { + "attributes": { + "ContainerArn": "The Amazon Resource Name (ARN) of the container.", + "Ref": "", + "Url": "The publicly accessible URL of the container service.\n\nIf no public endpoint is specified in the current deployment, this URL returns a 404 response." + }, + "description": "The `AWS::Lightsail::Container` resource specifies a container service.\n\nA Lightsail container service is a compute resource to which you can deploy containers.", + "properties": { + "ContainerServiceDeployment": "An object that describes the current container deployment of the container service.", + "IsDisabled": "A Boolean value indicating whether the container service is disabled.", + "Power": "The power specification of the container service.\n\nThe power specifies the amount of RAM, the number of vCPUs, and the base price of the container service.", + "PublicDomainNames": "The public domain name of the container service, such as `example.com` and `www.example.com` .\n\nYou can specify up to four public domain names for a container service. The domain names that you specify are used when you create a deployment with a container that is configured as the public endpoint of your container service.\n\nIf you don't specify public domain names, then you can use the default domain of the container service.\n\n> You must create and validate an SSL/TLS certificate before you can use public domain names with your container service. Use the [AWS::Lightsail::Certificate](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-certificate.html) resource to create a certificate for the public domain names that you want to use with your container service.", + "Scale": "The scale specification of the container service.\n\nThe scale specifies the allocated compute nodes of the container service.", + "ServiceName": "The name of the container service.", + "Tags": "An array of key-value pairs to apply to this resource.\n\nFor more information, see [Tag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) in the *AWS CloudFormation User Guide* .\n\n> The `Value` of `Tags` is optional for Lightsail resources." + } + }, + "AWS::Lightsail::Container.Container": { + "attributes": {}, + "description": "`Container` is a property of the [ContainerServiceDeployment](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-containerservicedeployment.html) property. It describes the settings of a container that will be launched, or that is launched, to an Amazon Lightsail container service.", + "properties": { + "Command": "The launch command for the container.", + "ContainerName": "The name of the container.", + "Environment": "The environment variables of the container.", + "Image": "The name of the image used for the container.\n\nContainer images that are sourced from (registered and stored on) your container service start with a colon ( `:` ). For example, if your container service name is `container-service-1` , the container image label is `mystaticsite` , and you want to use the third version ( `3` ) of the registered container image, then you should specify `:container-service-1.mystaticsite.3` . To use the latest version of a container image, specify `latest` instead of a version number (for example, `:container-service-1.mystaticsite.latest` ). Your container service will automatically use the highest numbered version of the registered container image.\n\nContainer images that are sourced from a public registry like Docker Hub don\u2019t start with a colon. For example, `nginx:latest` or `nginx` .", + "Ports": "An object that describes the open firewall ports and protocols of the container." + } + }, + "AWS::Lightsail::Container.ContainerServiceDeployment": { + "attributes": {}, + "description": "`ContainerServiceDeployment` is a property of the [AWS::Lightsail::Container](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-container.html) resource. It describes a container deployment configuration of a container service.\n\nA deployment specifies the settings, such as the ports and launch command, of containers that are deployed to your container service.", + "properties": { + "Containers": "An object that describes the configuration for the containers of the deployment.", + "PublicEndpoint": "An object that describes the endpoint of the deployment." + } + }, + "AWS::Lightsail::Container.EnvironmentVariable": { + "attributes": {}, + "description": "`EnvironmentVariable` is a property of the [Container](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-container.html) property. It describes the environment variables of a container on a container service which are key-value parameters that provide dynamic configuration of the application or script run by the container.", + "properties": { + "Value": "The environment variable value.", + "Variable": "The environment variable key." + } + }, + "AWS::Lightsail::Container.HealthCheckConfig": { + "attributes": {}, + "description": "`HealthCheckConfig` is a property of the [PublicEndpoint](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-publicendpoint.html) property. It describes the healthcheck configuration of a container deployment on a container service.", + "properties": { + "HealthyThreshold": "The number of consecutive health check successes required before moving the container to the `Healthy` state. The default value is `2` .", + "IntervalSeconds": "The approximate interval, in seconds, between health checks of an individual container. You can specify between `5` and `300` seconds. The default value is `5` .", + "Path": "The path on the container on which to perform the health check. The default value is `/` .", + "SuccessCodes": "The HTTP codes to use when checking for a successful response from a container. You can specify values between `200` and `499` . You can specify multiple values (for example, `200,202` ) or a range of values (for example, `200-299` ).", + "TimeoutSeconds": "The amount of time, in seconds, during which no response means a failed health check. You can specify between `2` and `60` seconds. The default value is `2` .", + "UnhealthyThreshold": "The number of consecutive health check failures required before moving the container to the `Unhealthy` state. The default value is `2` ." + } + }, + "AWS::Lightsail::Container.PortInfo": { + "attributes": {}, + "description": "`PortInfo` is a property of the [Container](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-container.html) property. It describes the ports to open and the protocols to use for a container on a Amazon Lightsail container service.", + "properties": { + "Port": "The open firewall ports of the container.", + "Protocol": "The protocol name for the open ports.\n\n*Allowed values* : `HTTP` | `HTTPS` | `TCP` | `UDP`" + } + }, + "AWS::Lightsail::Container.PublicDomainName": { + "attributes": {}, + "description": "`PublicDomainName` is a property of the [AWS::Lightsail::Container](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-container.html) resource. It describes the public domain names to use with a container service, such as `example.com` and `www.example.com` . It also describes the certificates to use with a container service.", + "properties": { + "CertificateName": "The name of the certificate for the public domains.", + "DomainNames": "The public domain names to use with the container service." + } + }, + "AWS::Lightsail::Container.PublicEndpoint": { + "attributes": {}, + "description": "`PublicEndpoint` is a property of the [ContainerServiceDeployment](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-containerservicedeployment.html) property. It describes describes the settings of the public endpoint of a container on a container service.", + "properties": { + "ContainerName": "The name of the container entry of the deployment that the endpoint configuration applies to.", + "ContainerPort": "The port of the specified container to which traffic is forwarded to.", + "HealthCheckConfig": "An object that describes the health check configuration of the container." + } + }, "AWS::Lightsail::Database": { "attributes": { "DatabaseArn": "The Amazon Resource Name (ARN) of the database (for example, `arn:aws:lightsail:us-east-2:123456789101:RelationalDatabase/244ad76f-8aad-4741-809f-12345EXAMPLE` ).", @@ -25275,7 +25863,7 @@ "RelationalDatabaseBundleId": "The bundle ID for the database (for example, `medium_1_0` ).", "RelationalDatabaseName": "The name of the instance.", "RelationalDatabaseParameters": "An array of parameters for the database.", - "RotateMasterUserPassword": "A boolean value indicating whether to change the primary user password to a new, strong password generated by Lightsail .\n\n> The `RotateMasterUserPassword` and `MasterUserPassword` parameters cannot be used together in the same template.", + "RotateMasterUserPassword": "A Boolean value indicating whether to change the primary user password to a new, strong password generated by Lightsail .\n\n> The `RotateMasterUserPassword` and `MasterUserPassword` parameters cannot be used together in the same template.", "Tags": "An array of key-value pairs to apply to this resource.\n\nFor more information, see [Tag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) in the *AWS CloudFormation User Guide* .\n\n> The `Value` of `Tags` is optional for Lightsail resources." } }, @@ -25331,6 +25919,89 @@ "SnapshotTimeOfDay": "The daily time when an automatic snapshot will be created.\n\nConstraints:\n\n- Must be in `HH:00` format, and in an hourly increment.\n- Specified in Coordinated Universal Time (UTC).\n- The snapshot will be automatically created between the time specified and up to 45 minutes after." } }, + "AWS::Lightsail::Distribution": { + "attributes": { + "AbleToUpdateBundle": "Indicates whether you can update the distribution\u2019s current bundle to another bundle.", + "DistributionArn": "The Amazon Resource Name (ARN) of the distribution.", + "Ref": "", + "Status": "The status of the distribution." + }, + "description": "The `AWS::Lightsail::Distribution` resource specifies a content delivery network (CDN) distribution. You can create distributions only in the `us-east-1` AWS Region.\n\nA distribution is a globally distributed network of caching servers that improve the performance of your website or web application hosted on a Lightsail instance, static content hosted on a Lightsail bucket, or through a Lightsail load balancer.", + "properties": { + "BundleId": "The ID of the bundle applied to the distribution.", + "CacheBehaviorSettings": "An object that describes the cache behavior settings of the distribution.", + "CacheBehaviors": "An array of objects that describe the per-path cache behavior of the distribution.", + "CertificateName": "The name of the SSL/TLS certificate attached to the distribution.", + "DefaultCacheBehavior": "An object that describes the default cache behavior of the distribution.", + "DistributionName": "The name of the distribution", + "IpAddressType": "The IP address type of the distribution.\n\nThe possible values are `ipv4` for IPv4 only, and `dualstack` for IPv4 and IPv6.", + "IsEnabled": "A Boolean value indicating whether the distribution is enabled.", + "Origin": "An object that describes the origin resource of the distribution, such as a Lightsail instance, bucket, or load balancer.\n\nThe distribution pulls, caches, and serves content from the origin.", + "Tags": "An array of key-value pairs to apply to this resource.\n\nFor more information, see [Tag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) in the *AWS CloudFormation User Guide* .\n\n> The `Value` of `Tags` is optional for Lightsail resources." + } + }, + "AWS::Lightsail::Distribution.CacheBehavior": { + "attributes": {}, + "description": "`CacheBehavior` is a property of the [AWS::Lightsail::Distribution](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-distribution.html) resource. It describes the default cache behavior of an Amazon Lightsail content delivery network (CDN) distribution.", + "properties": { + "Behavior": "The cache behavior of the distribution.\n\nThe following cache behaviors can be specified:\n\n- *`cache`* - This option is best for static sites. When specified, your distribution caches and serves your entire website as static content. This behavior is ideal for websites with static content that doesn't change depending on who views it, or for websites that don't use cookies, headers, or query strings to personalize content.\n- *`dont-cache`* - This option is best for sites that serve a mix of static and dynamic content. When specified, your distribution caches and serves only the content that is specified in the distribution\u2019s `CacheBehaviorPerPath` parameter. This behavior is ideal for websites or web applications that use cookies, headers, and query strings to personalize content for individual users." + } + }, + "AWS::Lightsail::Distribution.CacheBehaviorPerPath": { + "attributes": {}, + "description": "`CacheBehaviorPerPath` is a property of the [AWS::Lightsail::Distribution](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-distribution.html) resource. It describes the per-path cache behavior of an Amazon Lightsail content delivery network (CDN) distribution.\n\nUse a per-path cache behavior to override the default cache behavior of a distribution, or to add an exception to it. For example, if you set the `CacheBehavior` to `cache` , you can use a per-path cache behavior to specify a directory, file, or file type that your distribution will cache. If you don\u2019t want your distribution to cache a specified directory, file, or file type, set the per-path cache behavior to `dont-cache` .", + "properties": { + "Behavior": "The cache behavior for the specified path.\n\nYou can specify one of the following per-path cache behaviors:\n\n- *`cache`* - This behavior caches the specified path.\n- *`dont-cache`* - This behavior doesn't cache the specified path.", + "Path": "The path to a directory or file to cache, or not cache. Use an asterisk symbol to specify wildcard directories ( `path/to/assets/*` ), and file types ( `*.html` , `*jpg` , `*js` ). Directories and file paths are case-sensitive.\n\nExamples:\n\n- Specify the following to cache all files in the document root of an Apache web server running on a instance.\n\n`var/www/html/`\n- Specify the following file to cache only the index page in the document root of an Apache web server.\n\n`var/www/html/index.html`\n- Specify the following to cache only the .html files in the document root of an Apache web server.\n\n`var/www/html/*.html`\n- Specify the following to cache only the .jpg, .png, and .gif files in the images sub-directory of the document root of an Apache web server.\n\n`var/www/html/images/*.jpg`\n\n`var/www/html/images/*.png`\n\n`var/www/html/images/*.gif`\n\nSpecify the following to cache all files in the images subdirectory of the document root of an Apache web server.\n\n`var/www/html/images/`" + } + }, + "AWS::Lightsail::Distribution.CacheSettings": { + "attributes": {}, + "description": "`CacheSettings` is a property of the [AWS::Lightsail::Distribution](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-distribution.html) resource. It describes the cache settings of an Amazon Lightsail content delivery network (CDN) distribution.\n\nThese settings apply only to your distribution\u2019s `CacheBehaviors` that have a `Behavior` of `cache` . This includes the `DefaultCacheBehavior` .", + "properties": { + "AllowedHTTPMethods": "The HTTP methods that are processed and forwarded to the distribution's origin.\n\nYou can specify the following options:\n\n- `GET,HEAD` - The distribution forwards the `GET` and `HEAD` methods.\n- `GET,HEAD,OPTIONS` - The distribution forwards the `GET` , `HEAD` , and `OPTIONS` methods.\n- `GET,HEAD,OPTIONS,PUT,PATCH,POST,DELETE` - The distribution forwards the `GET` , `HEAD` , `OPTIONS` , `PUT` , `PATCH` , `POST` , and `DELETE` methods.\n\nIf you specify `GET,HEAD,OPTIONS,PUT,PATCH,POST,DELETE` , you might need to restrict access to your distribution's origin so users can't perform operations that you don't want them to. For example, you might not want users to have permission to delete objects from your origin.", + "CachedHTTPMethods": "The HTTP method responses that are cached by your distribution.\n\nYou can specify the following options:\n\n- `GET,HEAD` - The distribution caches responses to the `GET` and `HEAD` methods.\n- `GET,HEAD,OPTIONS` - The distribution caches responses to the `GET` , `HEAD` , and `OPTIONS` methods.", + "DefaultTTL": "The default amount of time that objects stay in the distribution's cache before the distribution forwards another request to the origin to determine whether the content has been updated.\n\n> The value specified applies only when the origin does not add HTTP headers such as `Cache-Control max-age` , `Cache-Control s-maxage` , and `Expires` to objects.", + "ForwardedCookies": "An object that describes the cookies that are forwarded to the origin. Your content is cached based on the cookies that are forwarded.", + "ForwardedHeaders": "An object that describes the headers that are forwarded to the origin. Your content is cached based on the headers that are forwarded.", + "ForwardedQueryStrings": "An object that describes the query strings that are forwarded to the origin. Your content is cached based on the query strings that are forwarded.", + "MaximumTTL": "The maximum amount of time that objects stay in the distribution's cache before the distribution forwards another request to the origin to determine whether the object has been updated.\n\nThe value specified applies only when the origin adds HTTP headers such as `Cache-Control max-age` , `Cache-Control s-maxage` , and `Expires` to objects.", + "MinimumTTL": "The minimum amount of time that objects stay in the distribution's cache before the distribution forwards another request to the origin to determine whether the object has been updated.\n\nA value of `0` must be specified for `minimumTTL` if the distribution is configured to forward all headers to the origin." + } + }, + "AWS::Lightsail::Distribution.CookieObject": { + "attributes": {}, + "description": "`CookieObject` is a property of the [CacheSettings](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cachesettings.html) property. It describes whether an Amazon Lightsail content delivery network (CDN) distribution forwards cookies to the origin and, if so, which ones.\n\nFor the cookies that you specify, your distribution caches separate versions of the specified content based on the cookie values in viewer requests.", + "properties": { + "CookiesAllowList": "The specific cookies to forward to your distribution's origin.", + "Option": "Specifies which cookies to forward to the distribution's origin for a cache behavior.\n\nUse one of the following configurations for your distribution:\n\n- *`all`* - Forwards all cookies to your origin.\n- *`none`* - Doesn\u2019t forward cookies to your origin.\n- *`allow-list`* - Forwards only the cookies that you specify using the `CookiesAllowList` parameter." + } + }, + "AWS::Lightsail::Distribution.HeaderObject": { + "attributes": {}, + "description": "`HeaderObject` is a property of the [CacheSettings](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cachesettings.html) property. It describes the request headers used by your distribution, which caches your content based on the request headers.\n\nFor the headers that you specify, your distribution caches separate versions of the specified content based on the header values in viewer requests. For example, suppose that viewer requests for logo.jpg contain a custom product header that has a value of either acme or apex. Also, suppose that you configure your distribution to cache your content based on values in the product header. Your distribution forwards the product header to the origin and caches the response from the origin once for each header value.", + "properties": { + "HeadersAllowList": "The specific headers to forward to your distribution's origin.", + "Option": "The headers that you want your distribution to forward to your origin. Your distribution caches your content based on these headers.\n\nUse one of the following configurations for your distribution:\n\n- *`all`* - Forwards all headers to your origin..\n- *`none`* - Forwards only the default headers.\n- *`allow-list`* - Forwards only the headers that you specify using the `HeadersAllowList` parameter." + } + }, + "AWS::Lightsail::Distribution.InputOrigin": { + "attributes": {}, + "description": "`InputOrigin` is a property of the [AWS::Lightsail::Distribution](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-distribution.html) resource. It describes the origin resource of an Amazon Lightsail content delivery network (CDN) distribution.\n\nAn origin can be a instance, bucket, or load balancer. A distribution pulls content from an origin, caches it, and serves it to viewers through a worldwide network of edge servers.", + "properties": { + "Name": "The name of the origin resource.", + "ProtocolPolicy": "The protocol that your Amazon Lightsail distribution uses when establishing a connection with your origin to pull content.", + "RegionName": "The AWS Region name of the origin resource." + } + }, + "AWS::Lightsail::Distribution.QueryStringObject": { + "attributes": {}, + "description": "`QueryStringObject` is a property of the [CacheSettings](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cachesettings.html) property. It describes the query string parameters that an Amazon Lightsail content delivery network (CDN) distribution to bases caching on.\n\nFor the query strings that you specify, your distribution caches separate versions of the specified content based on the query string values in viewer requests.", + "properties": { + "Option": "Indicates whether the distribution forwards and caches based on query strings.", + "QueryStringsAllowList": "The specific query strings that the distribution forwards to the origin.\n\nYour distribution caches content based on the specified query strings.\n\nIf the `option` parameter is true, then your distribution forwards all query strings, regardless of what you specify using the `QueryStringsAllowList` parameter." + } + }, "AWS::Lightsail::Instance": { "attributes": { "Hardware.CpuCount": "The number of vCPUs the instance has.", @@ -25449,6 +26120,38 @@ "Name": "The state of the instance (for example, `running` or `pending` )." } }, + "AWS::Lightsail::LoadBalancer": { + "attributes": { + "LoadBalancerArn": "The Amazon Resource Name (ARN) of the load balancer.", + "Ref": "" + }, + "description": "The `AWS::Lightsail::LoadBalancer` resource specifies a load balancer that can be used with Lightsail instances.\n\n> You cannot attach attach TLS certificates to a load balancer using the `AWS::Lightsail::LoadBalancer` resource type. Instead, use the `LoadBalancerTlsCertificate` resource type to create a certificate and attach it to a load balancer.", + "properties": { + "AttachedInstances": "The Lightsail instances to attach to the load balancer.", + "HealthCheckPath": "The path on the attached instance where the health check will be performed. If no path is specified, the load balancer tries to make a request to the default (root) page ( `/index.html` ).", + "InstancePort": "The port that the load balancer uses to direct traffic to your Lightsail instances. For HTTP traffic, specify port `80` . For HTTPS traffic, specify port `443` .", + "IpAddressType": "The IP address type of the load balancer.\n\nThe possible values are `ipv4` for IPv4 only, and `dualstack` for both IPv4 and IPv6.", + "LoadBalancerName": "The name of the load balancer.", + "SessionStickinessEnabled": "A Boolean value indicating whether session stickiness is enabled.\n\nEnable session stickiness (also known as *session affinity* ) to bind a user's session to a specific instance. This ensures that all requests from the user during the session are sent to the same instance.", + "SessionStickinessLBCookieDurationSeconds": "The time period, in seconds, after which the load balancer session stickiness cookie should be considered stale. If you do not specify this parameter, the default value is 0, which indicates that the sticky session should last for the duration of the browser session.", + "Tags": "An array of key-value pairs to apply to this resource.\n\nFor more information, see [Tag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) in the *AWS CloudFormation User Guide* .\n\n> The `Value` of `Tags` is optional for Lightsail resources." + } + }, + "AWS::Lightsail::LoadBalancerTlsCertificate": { + "attributes": { + "LoadBalancerTlsCertificateArn": "The Amazon Resource Name (ARN) of the SSL/TLS certificate.", + "Ref": "", + "Status": "The validation status of the SSL/TLS certificate.\n\nValid Values: `PENDING_VALIDATION` | `ISSUED` | `INACTIVE` | `EXPIRED` | `VALIDATION_TIMED_OUT` | `REVOKED` | `FAILED` | `UNKNOWN`" + }, + "description": "The `AWS::Lightsail::LoadBalancerTlsCertificate` resource specifies a TLS certificate that can be used with a Lightsail load balancer.", + "properties": { + "CertificateAlternativeNames": "An array of alternative domain names and subdomain names for your SSL/TLS certificate.\n\nIn addition to the primary domain name, you can have up to nine alternative domain names. Wildcards (such as `*.example.com` ) are not supported.", + "CertificateDomainName": "The domain name for the SSL/TLS certificate. For example, `example.com` or `www.example.com` .", + "CertificateName": "The name of the SSL/TLS certificate.", + "IsAttached": "A Boolean value indicating whether the SSL/TLS certificate is attached to a Lightsail load balancer.", + "LoadBalancerName": "The name of the load balancer that the SSL/TLS certificate is attached to." + } + }, "AWS::Lightsail::StaticIp": { "attributes": { "IpAddress": "The IP address of the static IP.", @@ -25464,50 +26167,60 @@ }, "AWS::Location::GeofenceCollection": { "attributes": { - "Arn": "", - "CreateTime": "", - "Ref": "" + "Arn": "The Amazon Resource Name (ARN) for the geofence collection resource. Used when you need to specify a resource across all AWS .\n\n- Format example: `arn:aws:geo:region:account-id:geofence-collection/ExampleGeofenceCollection`", + "CollectionArn": "Synonym for `Arn` . The Amazon Resource Name (ARN) for the geofence collection resource. Used when you need to specify a resource across all AWS .\n\n- Format example: `arn:aws:geo:region:account-id:geofence-collection/ExampleGeofenceCollection`", + "CreateTime": "The timestamp for when the geofence collection resource was created in [ISO 8601](https://docs.aws.amazon.com/https://www.iso.org/iso-8601-date-and-time-format.html) format: `YYYY-MM-DDThh:mm:ss.sssZ` .", + "Ref": "`Ref` returns the `GeofenceCollection` name.", + "UpdateTime": "The timestamp for when the geofence collection resource was last updated in [ISO 8601](https://docs.aws.amazon.com/https://www.iso.org/iso-8601-date-and-time-format.html) format: `YYYY-MM-DDThh:mm:ss.sssZ` ." }, "description": "The `AWS::Location::GeofenceCollection` resource specifies the ability to detect and act when a tracked device enters or exits a defined geographical boundary known as a geofence.", "properties": { "CollectionName": "The name for the geofence collection.", "Description": "An optional description for the geofence collection.", "KmsKeyId": "A key identifier for an [AWS KMS customer managed key](https://docs.aws.amazon.com/kms/latest/developerguide/create-keys.html) . Enter a key ID, key ARN, alias name, or alias ARN.", - "PricingPlan": "Specifies the pricing plan for the geofence collection.\n\nFor additional details and restrictions on each pricing plan option, see the [Amazon Location Service pricing page](https://docs.aws.amazon.com/location/pricing/) .", - "PricingPlanDataSource": "Specifies the data provider for the geofence collection.\n\n- Required value for the following pricing plans: `MobileAssetTracking` | `MobileAssetManagement`\n\nFor more information about [Data Providers](https://docs.aws.amazon.com/location/data-providers/) , and [Pricing plans](https://docs.aws.amazon.com/location/pricing/) , see the Amazon Location Service product page.\n\n> Amazon Location Service only uses `PricingPlanDataSource` to calculate billing for your geofence collection. Your data will not be shared with the data provider, and will remain in your AWS account or region unless you move it. \n\nValid Values: `Esri` | `Here`" + "PricingPlan": "No longer used. If included, the only allowed value is `RequestBasedUsage` .\n\n*Allowed Values* : `RequestBasedUsage`", + "PricingPlanDataSource": "This parameter is no longer used." } }, "AWS::Location::Map": { "attributes": { "Arn": "The Amazon Resource Name (ARN) for the map resource. Used to specify a resource across all AWS .\n\n- Format example: `arn:aws:geo:region:account-id:maps/ExampleMap`", - "DataSource": "", - "Ref": "", - "UpdateTime": "" + "CreateTime": "The timestamp for when the map resource was created in [ISO 8601](https://docs.aws.amazon.com/https://www.iso.org/iso-8601-date-and-time-format.html) format: `YYYY-MM-DDThh:mm:ss.sssZ` .", + "DataSource": "The data provider for the associated map tiles.", + "MapArn": "Synonym for `Arn` . The Amazon Resource Name (ARN) for the map resource. Used to specify a resource across all AWS .\n\n- Format example: `arn:aws:geo:region:account-id:maps/ExampleMap`", + "Ref": "`Ref` returns the `Map` name.", + "UpdateTime": "The timestamp for when the map resource was last updated in [ISO 8601](https://docs.aws.amazon.com/https://www.iso.org/iso-8601-date-and-time-format.html) format: `YYYY-MM-DDThh:mm:ss.sssZ` ." }, "description": "The `AWS::Location::Map` resource specifies a map resource in your AWS account, which provides map tiles of different styles sourced from global location data providers.", "properties": { "Configuration": "Specifies the map style selected from an available data provider.", "Description": "An optional description for the map resource.", "MapName": "The name for the map resource.\n\nRequirements:\n\n- Must contain only alphanumeric characters (A\u2013Z, a\u2013z, 0\u20139), hyphens (-), periods (.), and underscores (_).\n- Must be a unique map resource name.\n- No spaces allowed. For example, `ExampleMap` .", - "PricingPlan": "Specifies the pricing plan for your map resource.\n\nFor additional details and restrictions on each pricing plan option, see the [Amazon Location Service pricing page](https://docs.aws.amazon.com/location/pricing/) ." + "PricingPlan": "No longer used. If included, the only allowed value is `RequestBasedUsage` .\n\n*Allowed Values* : `RequestBasedUsage`" } }, "AWS::Location::Map.MapConfiguration": { "attributes": {}, "description": "Specifies the map tile style selected from an available provider.", "properties": { - "Style": "Specifies the map style selected from an available data provider.\n\nValid styles: `VectorEsriStreets` , `VectorEsriTopographic` , `VectorEsriNavigation` , `VectorEsriDarkGrayCanvas` , `VectorEsriLightGrayCanvas` , `VectorHereBerlin` .\n\n> When using HERE as your data provider, and selecting the Style `VectorHereBerlin` , you may not use HERE Maps for Asset Management. See the [AWS Service Terms](https://docs.aws.amazon.com/service-terms/) for Amazon Location Service." + "Style": "Specifies the map style selected from an available data provider.\n\nValid styles: `VectorEsriStreets` , `VectorEsriTopographic` , `VectorEsriNavigation` , `VectorEsriDarkGrayCanvas` , `VectorEsriLightGrayCanvas` , `VectorHereBerlin` .\n\n> When using HERE as your data provider, and selecting the Style `VectorHereBerlin` , you may not use HERE Technologies maps for Asset Management. See the [AWS Service Terms](https://docs.aws.amazon.com/service-terms/) for Amazon Location Service." } }, "AWS::Location::PlaceIndex": { - "attributes": {}, + "attributes": { + "Arn": "The Amazon Resource Name (ARN) for the place index resource. Used to specify a resource across AWS .\n\n- Format example: `arn:aws:geo:region:account-id:place-index/ExamplePlaceIndex`", + "CreateTime": "The timestamp for when the place index resource was created in [ISO 8601](https://docs.aws.amazon.com/https://www.iso.org/iso-8601-date-and-time-format.html) format: `YYYY-MM-DDThh:mm:ss.sssZ` .", + "IndexArn": "Synonym for `Arn` . The Amazon Resource Name (ARN) for the place index resource. Used to specify a resource across AWS .\n\n- Format example: `arn:aws:geo:region:account-id:place-index/ExamplePlaceIndex`", + "Ref": "`Ref` returns the `PlaceIndex` name.", + "UpdateTime": "The timestamp for when the place index resource was last updated in [ISO 8601](https://docs.aws.amazon.com/https://www.iso.org/iso-8601-date-and-time-format.html) format: `YYYY-MM-DDThh:mm:ss.sssZ` ." + }, "description": "The `AWS::Location::PlaceIndex` resource specifies a place index resource in your AWS account, which supports Places functions with geospatial data sourced from your chosen data provider.", "properties": { "DataSource": "Specifies the data provider of geospatial data.\n\n> This field is case-sensitive. Enter the valid values as shown. For example, entering `HERE` will return an error. \n\nValid values include:\n\n- `Esri`\n- `Here`\n\n> Place index resources using HERE as a data provider can't be used to [store](https://docs.aws.amazon.com/location-places/latest/APIReference/API_DataSourceConfiguration.html) results for locations in Japan. For more information, see the [AWS Service Terms](https://docs.aws.amazon.com/service-terms/) for Amazon Location Service.\n\nFor additional details on data providers, see the [Amazon Location Service data providers page](https://docs.aws.amazon.com/location/latest/developerguide/what-is-data-provider.html) .", "DataSourceConfiguration": "Specifies the data storage option for requesting Places.", "Description": "The optional description for the place index resource.", "IndexName": "The name of the place index resource.\n\nRequirements:\n\n- Contain only alphanumeric characters (A\u2013Z, a\u2013z, 0\u20139), hyphens (-), periods (.), and underscores (_).\n- Must be a unique place index resource name.\n- No spaces allowed. For example, `ExamplePlaceIndex` .", - "PricingPlan": "Specifies the pricing plan for your place index resource.\n\nFor additional details and restrictions on each pricing plan option, see the [Amazon Location Service pricing page](https://docs.aws.amazon.com/location/pricing/) ." + "PricingPlan": "No longer used. If included, the only allowed value is `RequestBasedUsage` .\n\n*Allowed Values* : `RequestBasedUsage`" } }, "AWS::Location::PlaceIndex.DataSourceConfiguration": { @@ -25518,32 +26231,46 @@ } }, "AWS::Location::RouteCalculator": { - "attributes": {}, + "attributes": { + "Arn": "The Amazon Resource Name (ARN) for the route calculator resource. Use the ARN when you specify a resource across all AWS .\n\n- Format example: `arn:aws:geo:region:account-id:route-calculator/ExampleCalculator`", + "CalculatorArn": "Synonym for `Arn` . The Amazon Resource Name (ARN) for the route calculator resource. Use the ARN when you specify a resource across all AWS .\n\n- Format example: `arn:aws:geo:region:account-id:route-calculator/ExampleCalculator`", + "CreateTime": "The timestamp for when the route calculator resource was created in [ISO 8601](https://docs.aws.amazon.com/https://www.iso.org/iso-8601-date-and-time-format.html) format: `YYYY-MM-DDThh:mm:ss.sssZ` .", + "Ref": "`Ref` returns the `RouteCalculator` name.", + "UpdateTime": "The timestamp for when the route calculator resource was last updated in [ISO 8601](https://docs.aws.amazon.com/https://www.iso.org/iso-8601-date-and-time-format.html) format: `YYYY-MM-DDThh:mm:ss.sssZ` ." + }, "description": "The `AWS::Location::RouteCalculator` resource specifies a route calculator resource in your AWS account.\n\nYou can send requests to a route calculator resource to estimate travel time, distance, and get directions. A route calculator sources traffic and road network data from your chosen data provider.", "properties": { "CalculatorName": "The name of the route calculator resource.\n\nRequirements:\n\n- Can use alphanumeric characters (A\u2013Z, a\u2013z, 0\u20139) , hyphens (-), periods (.), and underscores (_).\n- Must be a unique route calculator resource name.\n- No spaces allowed. For example, `ExampleRouteCalculator` .", "DataSource": "Specifies the data provider of traffic and road network data.\n\n> This field is case-sensitive. Enter the valid values as shown. For example, entering `HERE` returns an error. \n\nValid values include:\n\n- `Esri`\n- `Here`\n\nFor more information about data providers, see the [Amazon Location Service data providers page](https://docs.aws.amazon.com/location/latest/developerguide/what-is-data-provider.html) .", "Description": "The optional description for the route calculator resource.", - "PricingPlan": "Specifies the pricing plan for your route calculator resource.\n\nFor additional details and restrictions on each pricing plan option, see the [Amazon Location Service pricing page](https://docs.aws.amazon.com/location/pricing/) ." + "PricingPlan": "No longer used. If included, the only allowed value is `RequestBasedUsage` .\n\n*Allowed Values* : `RequestBasedUsage`" } }, "AWS::Location::Tracker": { - "attributes": {}, + "attributes": { + "Arn": "The Amazon Resource Name (ARN) for the tracker resource. Used when you need to specify a resource across all AWS .\n\n- Format example: `arn:aws:geo:region:account-id:tracker/ExampleTracker`", + "CreateTime": "The timestamp for when the tracker resource was created in [ISO 8601](https://docs.aws.amazon.com/https://www.iso.org/iso-8601-date-and-time-format.html) format: `YYYY-MM-DDThh:mm:ss.sssZ` .", + "Ref": "`Ref` returns the `Tracker` name.", + "TrackerArn": "Synonym for `Arn` . The Amazon Resource Name (ARN) for the tracker resource. Used when you need to specify a resource across all AWS .\n\n- Format example: `arn:aws:geo:region:account-id:tracker/ExampleTracker`", + "UpdateTime": "The timestamp for when the tracker resource was last updated in [ISO 8601](https://docs.aws.amazon.com/https://www.iso.org/iso-8601-date-and-time-format.html) format: `YYYY-MM-DDThh:mm:ss.sssZ` ." + }, "description": "The `AWS::Location::Tracker` resource specifies a tracker resource in your AWS account , which lets you receive current and historical location of devices.", "properties": { "Description": "An optional description for the tracker resource.", "KmsKeyId": "A key identifier for an [AWS KMS customer managed key](https://docs.aws.amazon.com/kms/latest/developerguide/create-keys.html) . Enter a key ID, key ARN, alias name, or alias ARN.", - "PositionFiltering": "Specifies the position filtering for the tracker resource.\n\nValid values:\n\n- `TimeBased` - Location updates are evaluated against linked geofence collections, but not every location update is stored. If your update frequency is more often than 30 seconds, only one update per 30 seconds is stored for each unique device ID.\n- `DistanceBased` - If the device has moved less than 30 m (98.4 ft), location updates are ignored. Location updates within this area are neither evaluated against linked geofence collections, nor stored. This helps control costs by reducing the number of geofence evaluations and historical device positions to paginate through. Distance-based filtering can also reduce the effects of GPS noise when displaying device trajectories on a map.\n\nThis field is optional. If not specified, the default value is `TimeBased` .", - "PricingPlan": "Specifies the pricing plan for the tracker resource.\n\nFor additional details and restrictions on each pricing plan option, see the [Amazon Location Service pricing page](https://docs.aws.amazon.com/location/pricing/) .", - "PricingPlanDataSource": "Specifies the data provider for the tracker resource.\n\n- Required value for the following pricing plans: `MobileAssetTracking` | `MobileAssetManagement`\n\nFor more information about [Data Providers](https://docs.aws.amazon.com/location/data-providers/) , and [Pricing plans](https://docs.aws.amazon.com/location/pricing/) , see the Amazon Location Service product page.\n\n> Amazon Location Service only uses `PricingPlanDataSource` to calculate billing for your tracker resource. Your data will not be shared with the data provider, and will remain in your AWS account or region unless you move it. \n\nValid Values: `Esri` | `Here`", + "PositionFiltering": "Specifies the position filtering for the tracker resource.\n\nValid values:\n\n- `TimeBased` - Location updates are evaluated against linked geofence collections, but not every location update is stored. If your update frequency is more often than 30 seconds, only one update per 30 seconds is stored for each unique device ID.\n- `DistanceBased` - If the device has moved less than 30 m (98.4 ft), location updates are ignored. Location updates within this area are neither evaluated against linked geofence collections, nor stored. This helps control costs by reducing the number of geofence evaluations and historical device positions to paginate through. Distance-based filtering can also reduce the effects of GPS noise when displaying device trajectories on a map.\n- `AccuracyBased` - If the device has moved less than the measured accuracy, location updates are ignored. For example, if two consecutive updates from a device have a horizontal accuracy of 5 m and 10 m, the second update is ignored if the device has moved less than 15 m. Ignored location updates are neither evaluated against linked geofence collections, nor stored. This can reduce the effects of GPS noise when displaying device trajectories on a map, and can help control your costs by reducing the number of geofence evaluations.\n\nThis field is optional. If not specified, the default value is `TimeBased` .", + "PricingPlan": "No longer used. If included, the only allowed value is `RequestBasedUsage` .", + "PricingPlanDataSource": "This parameter is no longer used.", "TrackerName": "The name for the tracker resource.\n\nRequirements:\n\n- Contain only alphanumeric characters (A-Z, a-z, 0-9) , hyphens (-), periods (.), and underscores (_).\n- Must be a unique tracker resource name.\n- No spaces allowed. For example, `ExampleTracker` ." } }, "AWS::Location::TrackerConsumer": { - "attributes": {}, - "description": "The `AWS::Location::TrackerConsumer` resource specifies an association between a geofence collection and a tracker resource. This allows the tracker resource to communicate location data to the linked geofence collection.\n\n> Currently not supported \u2014 Cross-account configurations, such as creating associations between a tracker resource in one account and a geofence collection in another account.", + "attributes": { + "Ref": "`Ref` returns the `GeofenceCollection` name." + }, + "description": "The `AWS::Location::TrackerConsumer` resource specifies an association between a geofence collection and a tracker resource. The geofence collection is referred to as the *consumer* of the tracker. This allows the tracker resource to communicate location data to the linked geofence collection.\n\n> Currently not supported \u2014 Cross-account configurations, such as creating associations between a tracker resource in one account and a geofence collection in another account.", "properties": { - "ConsumerArn": "The Amazon Resource Name (ARN) for the geofence collection to be disassociated from the tracker resource. Used when you need to specify a resource across all AWS .\n\n- Format example: `arn:aws:geo:region:account-id:geofence-collection/ExampleGeofenceCollectionConsumer`", + "ConsumerArn": "The Amazon Resource Name (ARN) for the geofence collection that consumes the tracker resource updates.\n\n- Format example: `arn:aws:geo:region:account-id:geofence-collection/ExampleGeofenceCollectionConsumer`", "TrackerName": "The name for the tracker resource.\n\nRequirements:\n\n- Contain only alphanumeric characters (A-Z, a-z, 0-9) , hyphens (-), periods (.), and underscores (_).\n- Must be a unique tracker resource name.\n- No spaces allowed. For example, `ExampleTracker` ." } }, @@ -25567,7 +26294,7 @@ }, "description": "The `AWS::Logs::LogGroup` resource specifies a log group. A log group defines common properties for log streams, such as their retention and access control rules. Each log stream must belong to one log group.\n\nYou can create up to 1,000,000 log groups per Region per account. You must use the following guidelines when naming a log group:\n\n- Log group names must be unique within a Region for an AWS account.\n- Log group names can be between 1 and 512 characters long.\n- Log group names consist of the following characters: a-z, A-Z, 0-9, '_' (underscore), '-' (hyphen), '/' (forward slash), and '.' (period).", "properties": { - "KmsKeyId": "The Amazon Resource Name (ARN) of the CMK to use when encrypting log data.", + "KmsKeyId": "The Amazon Resource Name (ARN) of the AWS KMS key to use when encrypting log data.\n\nTo associate an AWS KMS key with the log group, specify the ARN of that KMS key here. If you do so, ingested data is encrypted using this key. This association is stored as long as the data encrypted with the KMS key is still within CloudWatch Logs . This enables CloudWatch Logs to decrypt this data whenever it is requested.\n\nIf you attempt to associate a KMS key with the log group but the KMS key doesn't exist or is deactivated, you will receive an `InvalidParameterException` error.\n\nLog group data is always encrypted in CloudWatch Logs . If you omit this key, the encryption does not use AWS KMS . For more information, see [Encrypt log data in CloudWatch Logs using AWS Key Management Service](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/encrypt-log-data-kms.html)", "LogGroupName": "The name of the log group. If you don't specify a name, AWS CloudFormation generates a unique ID for the log group.", "RetentionInDays": "The number of days to retain the log events in the specified log group. Possible values are: 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, and 3653.\n\nTo set a log group to never have log events expire, use [DeleteRetentionPolicy](https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_DeleteRetentionPolicy.html) .", "Tags": "An array of key-value pairs to apply to the log group.\n\nFor more information, see [Tag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) ." @@ -25599,7 +26326,7 @@ "DefaultValue": "(Optional) The value to emit when a filter pattern does not match a log event. This value can be null.", "MetricName": "The name of the CloudWatch metric.", "MetricNamespace": "A custom namespace to contain your metric in CloudWatch. Use namespaces to group together metrics that are similar. For more information, see [Namespaces](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Namespace) .", - "MetricValue": "The value that is published to the CloudWatch metric. For example, if you're counting the occurrences of a particular term like `Error` , specify 1 for the metric value. If you're counting the number of bytes transferred, reference the value that is in the log event by using $ followed by the name of the field that you specified in the filter pattern, such as `$size` ." + "MetricValue": "The value that is published to the CloudWatch metric. For example, if you're counting the occurrences of a particular term like `Error` , specify 1 for the metric value. If you're counting the number of bytes transferred, reference the value that is in the log event by using $ followed by the name of the field that you specified in the filter pattern, such as `$.size` ." } }, "AWS::Logs::QueryDefinition": { @@ -25844,11 +26571,31 @@ "SubnetIdList": "An array of strings containing the Amazon VPC subnet IDs (e.g., `subnet-0bb1c79de3EXAMPLE` ." } }, + "AWS::MSK::BatchScramSecret": { + "attributes": { + "Ref": "" + }, + "description": "", + "properties": { + "ClusterArn": "", + "SecretArnList": "", + "UnprocessedScramSecrets": "" + } + }, + "AWS::MSK::BatchScramSecret.UnprocessedScramSecret": { + "attributes": {}, + "description": "Error info for scram secret associate/disassociate failure.", + "properties": { + "ErrorCode": "Error code for associate/disassociate failure.", + "ErrorMessage": "Error message for associate/disassociate failure.", + "SecretArn": "Amazon Secrets Manager secret ARN." + } + }, "AWS::MSK::Cluster": { "attributes": { "Ref": "`Ref` returns the Amazon MSK cluster ARN. For example:\n\n`REF MyTestCluster`\n\nFor the Amazon MSK cluster `MyTestCluster` , Ref returns the ARN of the cluster." }, - "description": "The `AWS::MSK::Cluster` resource creates an Amazon MSK cluster. For more information, see [What Is Amazon MSK?](https://docs.aws.amazon.com/msk/latest/developerguide/what-is-msk.html) in the *Amazon MSK Developer Guide* .", + "description": "The `AWS::MSK::Cluster` resource creates an Amazon MSK cluster . For more information, see [What Is Amazon MSK?](https://docs.aws.amazon.com/msk/latest/developerguide/what-is-msk.html) in the *Amazon MSK Developer Guide* .", "properties": { "BrokerNodeGroupInfo": "The setup to be used for brokers in the cluster.", "ClientAuthentication": "Includes information related to client authentication.", @@ -25856,8 +26603,8 @@ "ConfigurationInfo": "The Amazon MSK configuration to use for the cluster.", "EncryptionInfo": "Includes all encryption-related information.", "EnhancedMonitoring": "Specifies the level of monitoring for the MSK cluster. The possible values are `DEFAULT` , `PER_BROKER` , and `PER_TOPIC_PER_BROKER` .", - "KafkaVersion": "The version of Apache Kafka. You can use Amazon MSK to create clusters that use Apache Kafka versions 1.1.1 and 2.2.1.", - "LoggingInfo": "You can configure your MSK cluster to send broker logs to different destination types. This is a container for the configuration details related to broker logs.", + "KafkaVersion": "The version of Apache Kafka. For more information, see [Supported Apache Kafka versions](https://docs.aws.amazon.com/msk/latest/developerguide/supported-kafka-versions.html) in the Amazon MSK Developer Guide.", + "LoggingInfo": "You can configure your Amazon MSK cluster to send broker logs to different destination types. This is a container for the configuration details related to broker logs.", "NumberOfBrokerNodes": "The number of broker nodes you want in the Amazon MSK cluster. You can submit an update to increase the number of broker nodes in a cluster.", "OpenMonitoring": "The settings for open monitoring.", "Tags": "A map of key:value pairs to apply to this resource. Both key and value are of type String." @@ -25865,11 +26612,11 @@ }, "AWS::MSK::Cluster.BrokerLogs": { "attributes": {}, - "description": "You can configure your MSK cluster to send broker logs to different destination types. This configuration specifies the details of these destinations.", + "description": "You can configure your Amazon MSK cluster to send broker logs to different destination types. This configuration specifies the details of these destinations.", "properties": { "CloudWatchLogs": "Details of the CloudWatch Logs destination for broker logs.", "Firehose": "Details of the Kinesis Data Firehose delivery stream that is the destination for broker logs.", - "S3": "Details of the Amazon S3 destination for broker logs." + "S3": "Details of the Amazon MSK destination for broker logs." } }, "AWS::MSK::Cluster.BrokerNodeGroupInfo": { @@ -25898,7 +26645,7 @@ "description": "Details of the CloudWatch Logs destination for broker logs.", "properties": { "Enabled": "Specifies whether broker logs get sent to the specified CloudWatch Logs destination.", - "LogGroup": "The CloudWatch log group that is the destination for broker logs." + "LogGroup": "The CloudWatch Logs group that is the destination for broker logs." } }, "AWS::MSK::Cluster.ConfigurationInfo": { @@ -25920,6 +26667,7 @@ "attributes": {}, "description": "Contains information about the EBS storage volumes attached to brokers.", "properties": { + "ProvisionedThroughput": "", "VolumeSize": "The size in GiB of the EBS volume for the data drive on each broker node." } }, @@ -25934,8 +26682,8 @@ "attributes": {}, "description": "The settings for encrypting data in transit.", "properties": { - "ClientBroker": "Indicates the encryption setting for data in transit between clients and brokers. The following are the possible values.\n\n- `TLS` means that client-broker communication is enabled with TLS only.\n- `TLS_PLAINTEXT` means that client-broker communication is enabled for both TLS-encrypted, as well as plaintext data.\n- `PLAINTEXT` means that client-broker communication is enabled in plaintext only.\n\nThe default value is `TLS` .", - "InCluster": "When set to true, it indicates that data communication among the broker nodes of the cluster is encrypted. When set to false, the communication happens in plaintext. The default value is true." + "ClientBroker": "Indicates the encryption setting for data in transit between clients and brokers. The following are the possible values.\n\n- `TLS` means that client-broker communication is enabled with TLS only.\n- `TLS_PLAINTEXT` means that client-broker communication is enabled for both TLS-encrypted, as well as plain text data.\n- `PLAINTEXT` means that client-broker communication is enabled in plain text only.\n\nThe default value is `TLS` .", + "InCluster": "When set to true, it indicates that data communication among the broker nodes of the cluster is encrypted. When set to false, the communication happens in plain text. The default value is true." } }, "AWS::MSK::Cluster.EncryptionInfo": { @@ -25970,9 +26718,9 @@ }, "AWS::MSK::Cluster.LoggingInfo": { "attributes": {}, - "description": "You can configure your MSK cluster to send broker logs to different destination types. This is a container for the configuration details related to broker logs.", + "description": "You can configure your Amazon MSK cluster to send broker logs to different destination types. This is a container for the configuration details related to broker logs.", "properties": { - "BrokerLogs": "You can configure your MSK cluster to send broker logs to different destination types. This configuration specifies the details of these destinations." + "BrokerLogs": "You can configure your Amazon MSK cluster to send broker logs to different destination types. This configuration specifies the details of these destinations." } }, "AWS::MSK::Cluster.NodeExporter": { @@ -25997,11 +26745,19 @@ "NodeExporter": "Indicates whether you want to enable or disable the Node Exporter." } }, + "AWS::MSK::Cluster.ProvisionedThroughput": { + "attributes": {}, + "description": "", + "properties": { + "Enabled": "", + "VolumeThroughput": "" + } + }, "AWS::MSK::Cluster.PublicAccess": { "attributes": {}, "description": "Specifies whether the cluster's brokers are accessible from the internet. Public access is off by default.", "properties": { - "Type": "Set to DISABLED to turn off public access or to SERVICE_PROVIDED_EIPS to turn it on. Public access if off by default." + "Type": "Set to `DISABLED` to turn off public access or to `SERVICE_PROVIDED_EIPS` to turn it on. Public access if off by default." } }, "AWS::MSK::Cluster.S3": { @@ -26050,6 +26806,31 @@ "Enabled": "Unauthenticated is enabled or not." } }, + "AWS::MSK::Configuration": { + "attributes": { + "Arn": "", + "Ref": "" + }, + "description": "Creates a new MSK configuration. To see an example of how to use this operation, first save the following text to a file and name the file config-file.txt .\n\n`auto.create.topics.enable = true zookeeper.connection.timeout.ms = 1000 log.roll.ms = 604800000` \n\nNow run the following Python 3.6 script in the folder where you saved config-file.txt . This script uses the properties specified in config-file.txt to create a configuration named `SalesClusterConfiguration` . This configuration can work with Apache Kafka versions 1.1.1 and 2.1.0.\n\n```PYTHON\nimport boto3 client = boto3.client('kafka') config_file = open('config-file.txt', 'r') server_properties = config_file.read() response = client.create_configuration( Name='SalesClusterConfiguration', Description='The configuration to use on all sales clusters.', KafkaVersions=['1.1.1', '2.1.0'], ServerProperties=server_properties\n) print(response)\n```", + "properties": { + "CreationTime": "", + "Description": "The description of the configuration.", + "KafkaVersionsList": "", + "LatestRevision": "", + "Name": "The name of the configuration. Configuration names are strings that match the regex \"^[0-9A-Za-z][0-9A-Za-z-]{0,}$\".", + "ServerProperties": "Contents of the server.properties file. When using the API, you must ensure that the contents of the file are base64 encoded. When using the console, the SDK, or the CLI, the contents of server.properties can be in plaintext.", + "State": "" + } + }, + "AWS::MSK::Configuration.ConfigurationRevision": { + "attributes": {}, + "description": "Describes a configuration revision.", + "properties": { + "CreationTime": "The time when the configuration revision was created.", + "Description": "The description of the configuration revision.", + "Revision": "The revision number." + } + }, "AWS::MWAA::Environment": { "attributes": { "Arn": "The ARN for the Amazon MWAA environment.", @@ -27331,6 +28112,7 @@ "Mode": "If \"vod,\" all segments are indexed and kept permanently in the destination and manifest. If \"live,\" only the number segments specified in keepSegments and indexNSegments are kept. Newer segments replace older segments, which might prevent players from rewinding all the way to the beginning of the channel. VOD mode uses HLS EXT-X-PLAYLIST-TYPE of EVENT while the channel is running, converting it to a \"VOD\" type manifest on completion of the stream.", "OutputSelection": "MANIFESTSANDSEGMENTS: Generates manifests (the master manifest, if applicable, and media manifests) for this output group. SEGMENTSONLY: Doesn't generate any manifests for this output group.", "ProgramDateTime": "Includes or excludes the EXT-X-PROGRAM-DATE-TIME tag in .m3u8 manifest files. The value is calculated as follows: Either the program date and time are initialized using the input timecode source, or the time is initialized using the input timecode source and the date is initialized using the timestampOffset.", + "ProgramDateTimeClock": "", "ProgramDateTimePeriod": "The period of insertion of the EXT-X-PROGRAM-DATE-TIME entry, in seconds.", "RedundantManifest": "ENABLED: The master manifest (.m3u8 file) for each pipeline includes information about both pipelines: first its own media files, then the media files of the other pipeline. This feature allows a playout device that supports stale manifest detection to switch from one manifest to the other, when the current manifest seems to be stale. There are still two destinations and two master manifests, but both master manifests reference the media files from both pipelines. DISABLED: The master manifest (.m3u8 file) for each pipeline includes information about its own pipeline only. For an HLS output group with MediaPackage as the destination, the DISABLED behavior is always followed. MediaPackage regenerates the manifests it serves to players, so a redundant manifest from MediaLive is irrelevant.", "SegmentLength": "The length of the MPEG-2 Transport Stream segments to create, in seconds. Note that segments will end on the next keyframe after this number of seconds, so the actual segment length might be longer.", @@ -27464,6 +28246,7 @@ "FilterStrength": "Adjusts the magnitude of filtering from 1 (minimal) to 5 (strongest).", "InputFilter": "Turns on the filter for this input. MPEG-2 inputs have the deblocking filter enabled by default. 1) auto - filtering is applied depending on input type/quality 2) disabled - no filtering is applied to the input 3) forced - filtering is applied regardless of the input type.", "NetworkInputSettings": "Information about how to connect to the upstream system.", + "Scte35Pid": "", "Smpte2038DataPreference": "Specifies whether to extract applicable ancillary data from a SMPTE-2038 source in this input. Applicable data types are captions, timecode, AFD, and SCTE-104 messages.\n- PREFER: Extract from SMPTE-2038 if present in this input, otherwise extract from another source (if any).\n- IGNORE: Never extract any ancillary data from SMPTE-2038.", "SourceEndBehavior": "The loop input if it is a file.", "VideoSelector": "Information about one video to extract from the input." @@ -29243,9 +30026,27 @@ "ClipboardMode": "Enable or disable the use of the system clipboard to copy and paste between the streaming session and streaming client.", "Ec2InstanceTypes": "The EC2 instance types that users can select from when launching a streaming session with this launch profile.", "MaxSessionLengthInMinutes": "The length of time, in minutes, that a streaming session can be active before it is stopped or terminated. After this point, Nimble Studio automatically terminates or stops the session. The default length of time is 690 minutes, and the maximum length of time is 30 days.", + "MaxStoppedSessionLengthInMinutes": "Integer that determines if you can start and stop your sessions and how long a session can stay in the STOPPED state. The default value is 0. The maximum value is 5760.\n\nIf the value is missing or set to 0, your sessions can\u2019t be stopped. If you then call `StopStreamingSession` , the session fails. If the time that a session stays in the READY state exceeds the `maxSessionLengthInMinutes` value, the session will automatically be terminated (instead of stopped).\n\nIf the value is set to a positive number, the session can be stopped. You can call `StopStreamingSession` to stop sessions in the READY state. If the time that a session stays in the READY state exceeds the `maxSessionLengthInMinutes` value, the session will automatically be stopped (instead of terminated).", + "SessionStorage": "(Optional) The upload storage for a streaming session.", "StreamingImageIds": "The streaming images that users can select from when launching a streaming session with this launch profile." } }, + "AWS::NimbleStudio::LaunchProfile.StreamConfigurationSessionStorage": { + "attributes": {}, + "description": "The configuration for a streaming session\u2019s upload storage.", + "properties": { + "Mode": "Allows artists to upload files to their workstations. The only valid option is `UPLOAD` .", + "Root": "The configuration for the upload storage root of the streaming session." + } + }, + "AWS::NimbleStudio::LaunchProfile.StreamingSessionStorageRoot": { + "attributes": {}, + "description": "The upload storage root location (folder) on streaming workstations where files are uploaded.", + "properties": { + "Linux": "The folder path in Linux workstations where files are uploaded.", + "Windows": "The folder path in Windows workstations where files are uploaded." + } + }, "AWS::NimbleStudio::StreamingImage": { "attributes": { "EulaIds": "The list of IDs of EULAs that must be accepted before a streaming session can be started using this streaming image.", @@ -29378,16 +30179,16 @@ }, "AWS::OpenSearchService::Domain": { "attributes": { - "Arn": "The Amazon Resource Name (ARN) of the domain, such as `arn:aws:es:us-west-2:123456789012:domain/mystack-1ab2cdefghij` . This returned value is the same as the one returned by `AWS::OpenSearchService::Domain.Arn` .", + "Arn": "The Amazon Resource Name (ARN) of the domain, such as `arn:aws:es:us-west-2:123456789012:domain/mystack-1ab2cdefghij` .", "DomainArn": "", "DomainEndpoint": "The domain-specific endpoint used for requests to the OpenSearch APIs, such as `search-mystack-1ab2cdefghij-ab1c2deckoyb3hofw7wpqa3cm.us-west-1.es.amazonaws.com` .", - "Id": "", + "Id": "The resource ID. For example, `123456789012/my-domain` .", "Ref": "When the logical ID of this resource is provided to the Ref intrinsic function, Ref returns the resource name, such as `mystack-abc1d2efg3h4.` For more information about using the Ref function, see [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) ." }, - "description": "The AWS::OpenSearchService::Domain resource creates an Amazon OpenSearch Service (successor to Amazon Elasticsearch Service) domain.", + "description": "The AWS::OpenSearchService::Domain resource creates an Amazon OpenSearch Service (successor to Amazon Elasticsearch Service) domain.\n\n> The `AWS::OpenSearchService::Domain` resource replaces the legacy [AWS::Elasticsearch::Domain](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticsearch-domain.html) resource. While the Elasticsearch resource and options are still supported, we recommend modifying your existing Cloudformation templates to use the new OpenSearch Service resource, which supports both OpenSearch and legacy Elasticsearch engines. For instructions to upgrade domains defined within CloudFormation from Elasticsearch to OpenSearch, see [Remarks](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html#aws-resource-opensearchservice-domain--remarks) .", "properties": { "AccessPolicies": "An AWS Identity and Access Management ( IAM ) policy document that specifies who can access the OpenSearch Service domain and their permissions. For more information, see [Configuring access policies](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/ac.html#ac-creating) in the *Amazon OpenSearch Service Developer Guide* .", - "AdvancedOptions": "Additional options to specify for the OpenSearch Service domain. For more information, see [Advanced cluster parameters](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/createupdatedomains.html#createdomain-configure-advanced-options) in the *Amazon OpenSearch Service Developer Guide* .", + "AdvancedOptions": "Additional options to specify for the OpenSearch Service domain. For more information, see [AdvancedOptions](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/configuration-api.html#configuration-api-datatypes-advancedoptions) in the OpenSearch Service configuration API reference.", "AdvancedSecurityOptions": "Specifies options for fine-grained access control.", "ClusterConfig": "`ClusterConfig` is a property of the AWS::OpenSearchService::Domain resource that configures an Amazon OpenSearch Service cluster.", "CognitoOptions": "Configures OpenSearch Service to use Amazon Cognito authentication for OpenSearch Dashboards.", @@ -29469,7 +30270,7 @@ }, "AWS::OpenSearchService::Domain.LogPublishingOption": { "attributes": {}, - "description": "Specifies whether the OpenSearch Service domain publishes the OpenSearch application, search slow logs, or index slow logs to Amazon CloudWatch. Each option must be an object of name `SEARCH_SLOW_LOGS` , `ES_APPLICATION_LOGS` , `INDEX_SLOW_LOGS` , or `AUDIT_LOGS` depending on the type of logs you want to publish. For the full syntax, see the [examples](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html#aws-resource-opensearchservice-domain--examples) .\n\nIf you enable a slow log, you still have to enable the *collection* of slow logs using the OpenSearch REST API. To learn more, see [Enabling log publishing ( AWS CLI)](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/createdomain-configure-slow-logs.html#createdomain-configure-slow-logs-cli) .", + "description": "Specifies whether the OpenSearch Service domain publishes application, search slow logs, or index slow logs to Amazon CloudWatch. Each option must be an object of name `SEARCH_SLOW_LOGS` , `ES_APPLICATION_LOGS` , `INDEX_SLOW_LOGS` , or `AUDIT_LOGS` depending on the type of logs you want to publish. For the full syntax, see the [examples](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html#aws-resource-opensearchservice-domain--examples) .\n\nBefore you enable log publishing, you need to create a CloudWatch log group and provide OpenSearch Service the correct permissions to write to it. To learn more, see [Enabling log publishing ( AWS CloudFormation)](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/createdomain-configure-slow-logs.html#createdomain-configure-slow-logs-cfn) .", "properties": { "CloudWatchLogsLogGroupArn": "Specifies the CloudWatch log group to publish to.", "Enabled": "If `true` , enables the publishing of logs to CloudWatch.\n\nDefault: `false` ." @@ -29480,8 +30281,8 @@ "description": "Specifies information about the master user.", "properties": { "MasterUserARN": "ARN for the master user. Only specify if `InternalUserDatabaseEnabled` is false in `AdvancedSecurityOptions` .", - "MasterUserName": "Username for the master user. Only specify if `InternalUserDatabaseEnabled` is true in `AdvancedSecurityOptions` .", - "MasterUserPassword": "Password for the master user. Only specify if `InternalUserDatabaseEnabled` is true in `AdvancedSecurityOptions` ." + "MasterUserName": "Username for the master user. Only specify if `InternalUserDatabaseEnabled` is true in `AdvancedSecurityOptions` . If you don't want to specify this value directly within the template, you can use a [dynamic reference](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html) instead.", + "MasterUserPassword": "Password for the master user. Only specify if `InternalUserDatabaseEnabled` is true in `AdvancedSecurityOptions` . If you don't want to specify this value directly within the template, you can use a [dynamic reference](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html) instead." } }, "AWS::OpenSearchService::Domain.NodeToNodeEncryptionOptions": { @@ -31116,7 +31917,7 @@ "LastUpdatedTime": "The time this dataset version was last updated.", "OutputColumns": "" }, - "description": "Creates a dataset.", + "description": "Creates a dataset. This operation doesn't support datasets that include uploaded files as a source.", "properties": { "AwsAccountId": "The AWS account ID.", "ColumnGroups": "Groupings of columns that work together in certain Amazon QuickSight features. Currently, only geospatial hierarchy is supported.", @@ -31825,7 +32626,7 @@ "ReadEndpoint.Address": "The reader endpoint for the DB cluster. For example: `mystack-mydbcluster-ro-123456789012.us-east-2.rds.amazonaws.com`", "Ref": "`Ref` returns the name of the DB cluster." }, - "description": "The `AWS::RDS::DBCluster` resource creates an Amazon Aurora DB cluster. For more information, see [Managing an Amazon Aurora DB Cluster](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/CHAP_Aurora.html) in the *Amazon Aurora User Guide* .\n\n> You can only create this resource in AWS Regions where Amazon Aurora is supported. \n\n*Updating DB clusters*\n\nWhen properties labeled \" *Update requires:* [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) \" are updated, AWS CloudFormation first creates a replacement DB cluster, then changes references from other dependent resources to point to the replacement DB cluster, and finally deletes the old DB cluster.\n\n> We highly recommend that you take a snapshot of the database before updating the stack. If you don't, you lose the data when AWS CloudFormation replaces your DB cluster. To preserve your data, perform the following procedure:\n> \n> - Deactivate any applications that are using the DB cluster so that there's no activity on the DB instance.\n> - Create a snapshot of the DB cluster. For more information about creating DB snapshots, see [Creating a DB Cluster Snapshot](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_CreateSnapshotCluster.html) .\n> - If you want to restore your DB cluster using a DB cluster snapshot, modify the updated template with your DB cluster changes and add the `SnapshotIdentifier` property with the ID of the DB cluster snapshot that you want to use.\n> \n> After you restore a DB cluster with a `SnapshotIdentifier` property, you must specify the same `SnapshotIdentifier` property for any future updates to the DB cluster. When you specify this property for an update, the DB cluster is not restored from the DB cluster snapshot again, and the data in the database is not changed. However, if you don't specify the `SnapshotIdentifier` property, an empty DB cluster is created, and the original DB cluster is deleted. If you specify a property that is different from the previous snapshot restore property, a new DB cluster is restored from the specified `SnapshotIdentifier` property, and the original DB cluster is deleted.\n> - Update the stack. \n\nCurrently, when you are updating the stack for an Aurora Serverless DB cluster, you can't include changes to any other properties when you specify one of the following properties: `PreferredBackupWindow` , `PreferredMaintenanceWindow` , and `Port` . This limitation doesn't apply to provisioned DB clusters.\n\nFor more information about updating other properties of this resource, see `[ModifyDBCluster](https://docs.aws.amazon.com//AmazonRDS/latest/APIReference/API_ModifyDBCluster.html)` . For more information about updating stacks, see [AWS CloudFormation Stacks Updates](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks.html) .\n\n*Deleting DB clusters*\n\nThe default `DeletionPolicy` for `AWS::RDS::DBCluster` resources is `Snapshot` . For more information about how AWS CloudFormation deletes resources, see [DeletionPolicy Attribute](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-deletionpolicy.html) .", + "description": "The `AWS::RDS::DBCluster` resource creates an Amazon Aurora DB cluster. For more information, see [Managing an Amazon Aurora DB Cluster](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/CHAP_Aurora.html) in the *Amazon Aurora User Guide* .\n\n> You can only create this resource in AWS Regions where Amazon Aurora is supported. \n\nThis topic covers the resource for Amazon Aurora DB clusters. For the documentation on the resource for Amazon RDS DB instances, see [AWS::RDS::DBInstance](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-database-instance.html) .\n\n*Updating DB clusters*\n\nWhen properties labeled \" *Update requires:* [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) \" are updated, AWS CloudFormation first creates a replacement DB cluster, then changes references from other dependent resources to point to the replacement DB cluster, and finally deletes the old DB cluster.\n\n> We highly recommend that you take a snapshot of the database before updating the stack. If you don't, you lose the data when AWS CloudFormation replaces your DB cluster. To preserve your data, perform the following procedure:\n> \n> - Deactivate any applications that are using the DB cluster so that there's no activity on the DB instance.\n> - Create a snapshot of the DB cluster. For more information about creating DB snapshots, see [Creating a DB Cluster Snapshot](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_CreateSnapshotCluster.html) .\n> - If you want to restore your DB cluster using a DB cluster snapshot, modify the updated template with your DB cluster changes and add the `SnapshotIdentifier` property with the ID of the DB cluster snapshot that you want to use.\n> \n> After you restore a DB cluster with a `SnapshotIdentifier` property, you must specify the same `SnapshotIdentifier` property for any future updates to the DB cluster. When you specify this property for an update, the DB cluster is not restored from the DB cluster snapshot again, and the data in the database is not changed. However, if you don't specify the `SnapshotIdentifier` property, an empty DB cluster is created, and the original DB cluster is deleted. If you specify a property that is different from the previous snapshot restore property, a new DB cluster is restored from the specified `SnapshotIdentifier` property, and the original DB cluster is deleted.\n> - Update the stack. \n\nCurrently, when you are updating the stack for an Aurora Serverless DB cluster, you can't include changes to any other properties when you specify one of the following properties: `PreferredBackupWindow` , `PreferredMaintenanceWindow` , and `Port` . This limitation doesn't apply to provisioned DB clusters.\n\nFor more information about updating other properties of this resource, see `[ModifyDBCluster](https://docs.aws.amazon.com//AmazonRDS/latest/APIReference/API_ModifyDBCluster.html)` . For more information about updating stacks, see [AWS CloudFormation Stacks Updates](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks.html) .\n\n*Deleting DB clusters*\n\nThe default `DeletionPolicy` for `AWS::RDS::DBCluster` resources is `Snapshot` . For more information about how AWS CloudFormation deletes resources, see [DeletionPolicy Attribute](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-deletionpolicy.html) .", "properties": { "AssociatedRoles": "Provides a list of the AWS Identity and Access Management (IAM) roles that are associated with the DB cluster. IAM roles that are associated with a DB cluster grant permission for the DB cluster to access other Amazon Web Services on your behalf.", "AvailabilityZones": "A list of Availability Zones (AZs) where instances in the DB cluster can be created. For information on AWS Regions and Availability Zones, see [Choosing the Regions and Availability Zones](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.RegionsAndAvailabilityZones.html) in the *Amazon Aurora User Guide* .", @@ -31835,7 +32636,7 @@ "DBClusterIdentifier": "The DB cluster identifier. This parameter is stored as a lowercase string.\n\nConstraints:\n\n- Must contain from 1 to 63 letters, numbers, or hyphens.\n- First character must be a letter.\n- Can't end with a hyphen or contain two consecutive hyphens.\n\nExample: `my-cluster1`", "DBClusterParameterGroupName": "The name of the DB cluster parameter group to associate with this DB cluster.\n\n> If you apply a parameter group to an existing DB cluster, then its DB instances might need to reboot. This can result in an outage while the DB instances are rebooting.\n> \n> If you apply a change to parameter group associated with a stopped DB cluster, then the update stack waits until the DB cluster is started. \n\nTo list all of the available DB cluster parameter group names, use the following command:\n\n`aws rds describe-db-cluster-parameter-groups --query \"DBClusterParameterGroups[].DBClusterParameterGroupName\" --output text`", "DBSubnetGroupName": "A DB subnet group that you want to associate with this DB cluster.\n\nIf you are restoring a DB cluster to a point in time with `RestoreType` set to `copy-on-write` , and don't specify a DB subnet group name, then the DB cluster is restored with a default DB subnet group.", - "DatabaseName": "The name of your database. If you don't provide a name, then Amazon RDS won't create a database in this DB cluster. For naming constraints, see [Naming Constraints](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Limits.html#RDS_Limits.Constraints) in the *Amazon RDS User Guide* .", + "DatabaseName": "The name of your database. If you don't provide a name, then Amazon RDS won't create a database in this DB cluster. For naming constraints, see [Naming Constraints](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/CHAP_Limits.html#RDS_Limits.Constraints) in the *Amazon Aurora User Guide* .", "DeletionProtection": "A value that indicates whether the DB cluster has deletion protection enabled. The database can't be deleted when deletion protection is enabled. By default, deletion protection is disabled.", "EnableCloudwatchLogsExports": "The list of log types that need to be enabled for exporting to CloudWatch Logs. The values in the list depend on the DB engine being used. For more information, see [Publishing Database Logs to Amazon CloudWatch Logs](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_LogAccess.html#USER_LogAccess.Procedural.UploadtoCloudWatch) in the *Amazon Aurora User Guide* .\n\n*Aurora MySQL*\n\nValid values: `audit` , `error` , `general` , `slowquery`\n\n*Aurora PostgreSQL*\n\nValid values: `postgresql`", "EnableHttpEndpoint": "A value that indicates whether to enable the HTTP endpoint for an Aurora Serverless DB cluster. By default, the HTTP endpoint is disabled.\n\nWhen enabled, the HTTP endpoint provides a connectionless web service API for running SQL queries on the Aurora Serverless DB cluster. You can also query your database from inside the RDS console with the query editor.\n\nFor more information, see [Using the Data API for Aurora Serverless](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html) in the *Amazon Aurora User Guide* .", @@ -31844,10 +32645,10 @@ "EngineMode": "The DB engine mode of the DB cluster, either `provisioned` , `serverless` , `parallelquery` , `global` , or `multimaster` .\n\nThe `parallelquery` engine mode isn't required for Aurora MySQL version 1.23 and higher 1.x versions, and version 2.09 and higher 2.x versions.\n\nThe `global` engine mode isn't required for Aurora MySQL version 1.22 and higher 1.x versions, and `global` engine mode isn't required for any 2.x versions.\n\nThe `multimaster` engine mode only applies for DB clusters created with Aurora MySQL version 5.6.10a.\n\nFor Aurora PostgreSQL, the `global` engine mode isn't required, and both the `parallelquery` and the `multimaster` engine modes currently aren't supported.\n\nLimitations and requirements apply to some DB engine modes. For more information, see the following sections in the *Amazon Aurora User Guide* :\n\n- [Limitations of Aurora Serverless](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-serverless.html#aurora-serverless.limitations)\n- [Limitations of Parallel Query](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-mysql-parallel-query.html#aurora-mysql-parallel-query-limitations)\n- [Limitations of Aurora Global Databases](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-global-database.html#aurora-global-database.limitations)\n- [Limitations of Multi-Master Clusters](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-multi-master.html#aurora-multi-master-limitations)", "EngineVersion": "The version number of the database engine to use.\n\nTo list all of the available engine versions for `aurora` (for MySQL 5.6-compatible Aurora), use the following command:\n\n`aws rds describe-db-engine-versions --engine aurora --query \"DBEngineVersions[].EngineVersion\"`\n\nTo list all of the available engine versions for `aurora-mysql` (for MySQL 5.7-compatible Aurora), use the following command:\n\n`aws rds describe-db-engine-versions --engine aurora-mysql --query \"DBEngineVersions[].EngineVersion\"`\n\nTo list all of the available engine versions for `aurora-postgresql` , use the following command:\n\n`aws rds describe-db-engine-versions --engine aurora-postgresql --query \"DBEngineVersions[].EngineVersion\"`", "GlobalClusterIdentifier": "If you are configuring an Aurora global database cluster and want your Aurora DB cluster to be a secondary member in the global database cluster, specify the global cluster ID of the global database cluster. To define the primary database cluster of the global cluster, use the [AWS::RDS::GlobalCluster](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-globalcluster.html) resource.\n\nIf you aren't configuring a global database cluster, don't specify this property.\n\n> To remove the DB cluster from a global database cluster, specify an empty value for the `GlobalClusterIdentifier` property. \n\nFor information about Aurora global databases, see [Working with Amazon Aurora Global Databases](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-global-database.html) in the *Amazon Aurora User Guide* .", - "KmsKeyId": "The Amazon Resource Name (ARN) of the AWS Key Management Service master key that is used to encrypt the database instances in the DB cluster, such as `arn:aws:kms:us-east-1:012345678910:key/abcd1234-a123-456a-a12b-a123b4cd56ef` . If you enable the `StorageEncrypted` property but don't specify this property, the default master key is used. If you specify this property, you must set the `StorageEncrypted` property to `true` .\n\nIf you specify the `SnapshotIdentifier` property, the `StorageEncrypted` property value is inherited from the snapshot, and if the DB cluster is encrypted, the specified `KmsKeyId` property is used.", + "KmsKeyId": "The Amazon Resource Name (ARN) of the AWS KMS key that is used to encrypt the database instances in the DB cluster, such as `arn:aws:kms:us-east-1:012345678910:key/abcd1234-a123-456a-a12b-a123b4cd56ef` . If you enable the `StorageEncrypted` property but don't specify this property, the default KMS key is used. If you specify this property, you must set the `StorageEncrypted` property to `true` .\n\nIf you specify the `SnapshotIdentifier` property, the `StorageEncrypted` property value is inherited from the snapshot, and if the DB cluster is encrypted, the specified `KmsKeyId` property is used.", "MasterUserPassword": "The master password for the DB instance.\n\n> If you specify the `SourceDBClusterIdentifier` or `SnapshotIdentifier` property, don't specify this property. The value is inherited from the source DB instance or snapshot.", "MasterUsername": "The name of the master user for the DB cluster.\n\n> If you specify the `SourceDBClusterIdentifier` or `SnapshotIdentifier` property, don't specify this property. The value is inherited from the source DB instance or snapshot.", - "Port": "The port number on which the DB instances in the DB cluster accept connections.\n\nDefault:\n\n- When `EngineMode` is `provisioned` , `3306` (for both Aurora MySQL and Aurora PostgreSQL)\n- When `EngineMode` is `serverless` :\n\n- `3306` when `Engine` is `aurora` or `aurora-mysql`\n- `5432` when `Engine` is `aurora-postgresql`", + "Port": "The port number on which the DB instances in the DB cluster accept connections.\n\nDefault:\n\n- When `EngineMode` is `provisioned` , `3306` (for both Aurora MySQL and Aurora PostgreSQL)\n- When `EngineMode` is `serverless` :\n\n- `3306` when `Engine` is `aurora` or `aurora-mysql`\n- `5432` when `Engine` is `aurora-postgresql`\n\n> The `No interruption` on update behavior only applies to DB clusters. If you are updating a DB instance, see [Port](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-database-instance.html#cfn-rds-dbinstance-port) for the AWS::RDS::DBInstance resource.", "PreferredBackupWindow": "The daily time range during which automated backups are created. For more information, see [Backup Window](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Managing.Backups.html#Aurora.Managing.Backups.BackupWindow) in the *Amazon Aurora User Guide.*\n\nConstraints:\n\n- Must be in the format `hh24:mi-hh24:mi` .\n- Must be in Universal Coordinated Time (UTC).\n- Must not conflict with the preferred maintenance window.\n- Must be at least 30 minutes.", "PreferredMaintenanceWindow": "The weekly time range during which system maintenance can occur, in Universal Coordinated Time (UTC).\n\nFormat: `ddd:hh24:mi-ddd:hh24:mi`\n\nThe default is a 30-minute window selected at random from an 8-hour block of time for each AWS Region, occurring on a random day of the week. To see the time blocks available, see [Adjusting the Preferred DB Cluster Maintenance Window](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_UpgradeDBInstance.Maintenance.html#AdjustingTheMaintenanceWindow.Aurora) in the *Amazon Aurora User Guide.*\n\nValid Days: Mon, Tue, Wed, Thu, Fri, Sat, Sun.\n\nConstraints: Minimum 30-minute window.", "ReplicationSourceIdentifier": "The Amazon Resource Name (ARN) of the source DB instance or DB cluster if this DB cluster is created as a read replica.", @@ -31898,7 +32699,7 @@ "Endpoint.Port": "The port number on which the database accepts connections. For example: `3306`", "Ref": "`Ref` returns the DB instance name." }, - "description": "The `AWS::RDS::DBInstance` resource creates an Amazon RDS DB instance.\n\nIf you import an existing DB instance, and the template configuration doesn't match the actual configuration of the DB instance, AWS CloudFormation applies the changes in the template during the import operation.\n\n> If a DB instance is deleted or replaced during an update, AWS CloudFormation deletes all automated snapshots. However, it retains manual DB snapshots. During an update that requires replacement, you can apply a stack policy to prevent DB instances from being replaced. For more information, see [Prevent Updates to Stack Resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html) . \n\n*Updating DB instances*\n\nWhen properties labeled \" *Update requires:* [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) \" are updated, AWS CloudFormation first creates a replacement DB instance, then changes references from other dependent resources to point to the replacement DB instance, and finally deletes the old DB instance.\n\n> We highly recommend that you take a snapshot of the database before updating the stack. If you don't, you lose the data when AWS CloudFormation replaces your DB instance. To preserve your data, perform the following procedure:\n> \n> - Deactivate any applications that are using the DB instance so that there's no activity on the DB instance.\n> - Create a snapshot of the DB instance. For more information about creating DB snapshots, see [Creating a DB Snapshot](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_CreateSnapshot.html) .\n> - If you want to restore your instance using a DB snapshot, modify the updated template with your DB instance changes and add the `DBSnapshotIdentifier` property with the ID of the DB snapshot that you want to use.\n> \n> After you restore a DB instance with a `DBSnapshotIdentifier` property, you must specify the same `DBSnapshotIdentifier` property for any future updates to the DB instance. When you specify this property for an update, the DB instance is not restored from the DB snapshot again, and the data in the database is not changed. However, if you don't specify the `DBSnapshotIdentifier` property, an empty DB instance is created, and the original DB instance is deleted. If you specify a property that is different from the previous snapshot restore property, a new DB instance is restored from the specified `DBSnapshotIdentifier` property, and the original DB instance is deleted.\n> - Update the stack. \n\nFor more information about updating other properties of this resource, see `[ModifyDBInstance](https://docs.aws.amazon.com//AmazonRDS/latest/APIReference/API_ModifyDBInstance.html)` . For more information about updating stacks, see [AWS CloudFormation Stacks Updates](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks.html) .\n\n*Deleting DB instances*\n\nFor DB instances that are part of an Aurora DB cluster, you can set a deletion policy for your DB instance to control how AWS CloudFormation handles the DB instance when the stack is deleted. For Amazon RDS DB instances, you can choose to *retain* the DB instance, to *delete* the DB instance, or to *create a snapshot* of the DB instance. The default AWS CloudFormation behavior depends on the `DBClusterIdentifier` property:\n\n- For `AWS::RDS::DBInstance` resources that don't specify the `DBClusterIdentifier` property, AWS CloudFormation saves a snapshot of the DB instance.\n- For `AWS::RDS::DBInstance` resources that do specify the `DBClusterIdentifier` property, AWS CloudFormation deletes the DB instance.\n\nFor more information, see [DeletionPolicy Attribute](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-deletionpolicy.html) .", + "description": "The `AWS::RDS::DBInstance` resource creates an Amazon RDS DB instance.\n\nIf you import an existing DB instance, and the template configuration doesn't match the actual configuration of the DB instance, AWS CloudFormation applies the changes in the template during the import operation.\n\n> If a DB instance is deleted or replaced during an update, AWS CloudFormation deletes all automated snapshots. However, it retains manual DB snapshots. During an update that requires replacement, you can apply a stack policy to prevent DB instances from being replaced. For more information, see [Prevent Updates to Stack Resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html) . \n\nThis topic covers the resource for Amazon RDS DB instances. For the documentation on the resource for Amazon Aurora DB clusters, see [AWS::RDS::DBCluster](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbcluster.html) .\n\n*Updating DB instances*\n\nWhen properties labeled \" *Update requires:* [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) \" are updated, AWS CloudFormation first creates a replacement DB instance, then changes references from other dependent resources to point to the replacement DB instance, and finally deletes the old DB instance.\n\n> We highly recommend that you take a snapshot of the database before updating the stack. If you don't, you lose the data when AWS CloudFormation replaces your DB instance. To preserve your data, perform the following procedure:\n> \n> - Deactivate any applications that are using the DB instance so that there's no activity on the DB instance.\n> - Create a snapshot of the DB instance. For more information about creating DB snapshots, see [Creating a DB Snapshot](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_CreateSnapshot.html) .\n> - If you want to restore your instance using a DB snapshot, modify the updated template with your DB instance changes and add the `DBSnapshotIdentifier` property with the ID of the DB snapshot that you want to use.\n> \n> After you restore a DB instance with a `DBSnapshotIdentifier` property, you must specify the same `DBSnapshotIdentifier` property for any future updates to the DB instance. When you specify this property for an update, the DB instance is not restored from the DB snapshot again, and the data in the database is not changed. However, if you don't specify the `DBSnapshotIdentifier` property, an empty DB instance is created, and the original DB instance is deleted. If you specify a property that is different from the previous snapshot restore property, a new DB instance is restored from the specified `DBSnapshotIdentifier` property, and the original DB instance is deleted.\n> - Update the stack. \n\nFor more information about updating other properties of this resource, see `[ModifyDBInstance](https://docs.aws.amazon.com//AmazonRDS/latest/APIReference/API_ModifyDBInstance.html)` . For more information about updating stacks, see [AWS CloudFormation Stacks Updates](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks.html) .\n\n*Deleting DB instances*\n\nFor DB instances that are part of an Aurora DB cluster, you can set a deletion policy for your DB instance to control how AWS CloudFormation handles the DB instance when the stack is deleted. For Amazon RDS DB instances, you can choose to *retain* the DB instance, to *delete* the DB instance, or to *create a snapshot* of the DB instance. The default AWS CloudFormation behavior depends on the `DBClusterIdentifier` property:\n\n- For `AWS::RDS::DBInstance` resources that don't specify the `DBClusterIdentifier` property, AWS CloudFormation saves a snapshot of the DB instance.\n- For `AWS::RDS::DBInstance` resources that do specify the `DBClusterIdentifier` property, AWS CloudFormation deletes the DB instance.\n\nFor more information, see [DeletionPolicy Attribute](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-deletionpolicy.html) .", "properties": { "AllocatedStorage": "The amount of storage (in gigabytes) to be initially allocated for the database instance.\n\n> If any value is set in the `Iops` parameter, `AllocatedStorage` must be at least 100 GiB, which corresponds to the minimum Iops value of 1,000. If you increase the `Iops` value (in 1,000 IOPS increments), then you must also increase the `AllocatedStorage` value (in 100-GiB increments). \n\n*Amazon Aurora*\n\nNot applicable. Aurora cluster volumes automatically grow as the amount of data in your database increases, though you are only charged for the space that you use in an Aurora cluster volume.\n\n*MySQL*\n\nConstraints to the amount of storage for each storage type are the following:\n\n- General Purpose (SSD) storage (gp2): Must be an integer from 20 to 65536.\n- Provisioned IOPS storage (io1): Must be an integer from 100 to 65536.\n- Magnetic storage (standard): Must be an integer from 5 to 3072.\n\n*MariaDB*\n\nConstraints to the amount of storage for each storage type are the following:\n\n- General Purpose (SSD) storage (gp2): Must be an integer from 20 to 65536.\n- Provisioned IOPS storage (io1): Must be an integer from 100 to 65536.\n- Magnetic storage (standard): Must be an integer from 5 to 3072.\n\n*PostgreSQL*\n\nConstraints to the amount of storage for each storage type are the following:\n\n- General Purpose (SSD) storage (gp2): Must be an integer from 20 to 65536.\n- Provisioned IOPS storage (io1): Must be an integer from 100 to 65536.\n- Magnetic storage (standard): Must be an integer from 5 to 3072.\n\n*Oracle*\n\nConstraints to the amount of storage for each storage type are the following:\n\n- General Purpose (SSD) storage (gp2): Must be an integer from 20 to 65536.\n- Provisioned IOPS storage (io1): Must be an integer from 100 to 65536.\n- Magnetic storage (standard): Must be an integer from 10 to 3072.\n\n*SQL Server*\n\nConstraints to the amount of storage for each storage type are the following:\n\n- General Purpose (SSD) storage (gp2):\n\n- Enterprise and Standard editions: Must be an integer from 20 to 16384.\n- Web and Express editions: Must be an integer from 20 to 16384.\n- Provisioned IOPS storage (io1):\n\n- Enterprise and Standard editions: Must be an integer from 20 to 16384.\n- Web and Express editions: Must be an integer from 20 to 16384.\n- Magnetic storage (standard):\n\n- Enterprise and Standard editions: Must be an integer from 20 to 1024.\n- Web and Express editions: Must be an integer from 20 to 1024.", "AllowMajorVersionUpgrade": "A value that indicates whether major version upgrades are allowed. Changing this parameter doesn't result in an outage and the change is asynchronously applied as soon as possible.\n\nConstraints: Major version upgrades must be allowed when specifying a value for the `EngineVersion` parameter that is a different major version than the DB instance's current version.", @@ -31911,12 +32712,12 @@ "CopyTagsToSnapshot": "A value that indicates whether to copy tags from the DB instance to snapshots of the DB instance. By default, tags are not copied.\n\n*Amazon Aurora*\n\nNot applicable. Copying tags to snapshots is managed by the DB cluster. Setting this value for an Aurora DB instance has no effect on the DB cluster setting.", "DBClusterIdentifier": "The identifier of the DB cluster that the instance will belong to.", "DBInstanceClass": "The compute and memory capacity of the DB instance, for example, `db.m4.large` . Not all DB instance classes are available in all AWS Regions, or for all database engines.\n\nFor the full list of DB instance classes, and availability for your engine, see [DB Instance Class](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.DBInstanceClass.html) in the *Amazon RDS User Guide.* For more information about DB instance class pricing and AWS Region support for DB instance classes, see [Amazon RDS Pricing](https://docs.aws.amazon.com/rds/pricing/) .", - "DBInstanceIdentifier": "A name for the DB instance. If you specify a name, AWS CloudFormation converts it to lowercase. If you don't specify a name, AWS CloudFormation generates a unique physical ID and uses that ID for the DB instance. For more information, see [Name Type](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-name.html) .\n\n> If you specify a name, you cannot perform updates that require replacement of this resource. You can perform updates that require no or some interruption. If you must replace the resource, specify a new name.", + "DBInstanceIdentifier": "A name for the DB instance. If you specify a name, AWS CloudFormation converts it to lowercase. If you don't specify a name, AWS CloudFormation generates a unique physical ID and uses that ID for the DB instance. For more information, see [Name Type](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-name.html) .\n\nFor information about constraints that apply to DB instance identifiers, see [Naming constraints in Amazon RDS](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Limits.html#RDS_Limits.Constraints) in the *Amazon RDS User Guide* .\n\n> If you specify a name, you can't perform updates that require replacement of this resource. You can perform updates that require no or some interruption. If you must replace the resource, specify a new name.", "DBName": "The meaning of this parameter differs according to the database engine you use.\n\n> If you specify the `[DBSnapshotIdentifier](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-database-instance.html#cfn-rds-dbinstance-dbsnapshotidentifier)` property, AWS CloudFormation ignores this property.\n> \n> If you restore DB instances from snapshots, this property doesn't apply to the MySQL, PostgreSQL, or MariaDB engines. \n\n*MySQL*\n\nThe name of the database to create when the DB instance is created. If this parameter is not specified, no database is created in the DB instance.\n\nConstraints:\n\n- Must contain 1 to 64 letters or numbers.\n- Can't be a word reserved by the specified database engine\n\n*MariaDB*\n\nThe name of the database to create when the DB instance is created. If this parameter is not specified, no database is created in the DB instance.\n\nConstraints:\n\n- Must contain 1 to 64 letters or numbers.\n- Can't be a word reserved by the specified database engine\n\n*PostgreSQL*\n\nThe name of the database to create when the DB instance is created. If this parameter is not specified, the default `postgres` database is created in the DB instance.\n\nConstraints:\n\n- Must contain 1 to 63 letters, numbers, or underscores.\n- Must begin with a letter or an underscore. Subsequent characters can be letters, underscores, or digits (0-9).\n- Can't be a word reserved by the specified database engine\n\n*Oracle*\n\nThe Oracle System ID (SID) of the created DB instance. If you specify `null` , the default value `ORCL` is used. You can't specify the string NULL, or any other reserved word, for `DBName` .\n\nDefault: `ORCL`\n\nConstraints:\n\n- Can't be longer than 8 characters\n\n*SQL Server*\n\nNot applicable. Must be null.\n\n*Amazon Aurora MySQL*\n\nThe name of the database to create when the primary DB instance of the Aurora MySQL DB cluster is created. If this parameter isn't specified for an Aurora MySQL DB cluster, no database is created in the DB cluster.\n\nConstraints:\n\n- It must contain 1 to 64 alphanumeric characters.\n- It can't be a word reserved by the database engine.\n\n*Amazon Aurora PostgreSQL*\n\nThe name of the database to create when the primary DB instance of the Aurora PostgreSQL DB cluster is created. If this parameter isn't specified for an Aurora PostgreSQL DB cluster, a database named `postgres` is created in the DB cluster.\n\nConstraints:\n\n- It must contain 1 to 63 alphanumeric characters.\n- It must begin with a letter or an underscore. Subsequent characters can be letters, underscores, or digits (0 to 9).\n- It can't be a word reserved by the database engine.", "DBParameterGroupName": "The name of an existing DB parameter group or a reference to an [AWS::RDS::DBParameterGroup](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-dbparametergroup.html) resource created in the template.\n\nTo list all of the available DB parameter group names, use the following command:\n\n`aws rds describe-db-parameter-groups --query \"DBParameterGroups[].DBParameterGroupName\" --output text`\n\n> If any of the data members of the referenced parameter group are changed during an update, the DB instance might need to be restarted, which causes some interruption. If the parameter group contains static parameters, whether they were changed or not, an update triggers a reboot. \n\nIf you don't specify a value for the `DBParameterGroupName` property, the default DB parameter group for the specified engine and engine version is used.", "DBSecurityGroups": "A list of the DB security groups to assign to the DB instance. The list can include both the name of existing DB security groups or references to AWS::RDS::DBSecurityGroup resources created in the template.\n\nIf you set DBSecurityGroups, you must not set VPCSecurityGroups, and vice versa. Also, note that the DBSecurityGroups property exists only for backwards compatibility with older regions and is no longer recommended for providing security information to an RDS DB instance. Instead, use VPCSecurityGroups.\n\n> If you specify this property, AWS CloudFormation sends only the following properties (if specified) to Amazon RDS during create operations:\n> \n> - `AllocatedStorage`\n> - `AutoMinorVersionUpgrade`\n> - `AvailabilityZone`\n> - `BackupRetentionPeriod`\n> - `CharacterSetName`\n> - `DBInstanceClass`\n> - `DBName`\n> - `DBParameterGroupName`\n> - `DBSecurityGroups`\n> - `DBSubnetGroupName`\n> - `Engine`\n> - `EngineVersion`\n> - `Iops`\n> - `LicenseModel`\n> - `MasterUsername`\n> - `MasterUserPassword`\n> - `MultiAZ`\n> - `OptionGroupName`\n> - `PreferredBackupWindow`\n> - `PreferredMaintenanceWindow`\n> \n> All other properties are ignored. Specify a virtual private cloud (VPC) security group if you want to submit other properties, such as `StorageType` , `StorageEncrypted` , or `KmsKeyId` . If you're already using the `DBSecurityGroups` property, you can't use these other properties by updating your DB instance to use a VPC security group. You must recreate the DB instance.", "DBSnapshotIdentifier": "The name or Amazon Resource Name (ARN) of the DB snapshot that's used to restore the DB instance. If you're restoring from a shared manual DB snapshot, you must specify the ARN of the snapshot.\n\nBy specifying this property, you can create a DB instance from the specified DB snapshot. If the `DBSnapshotIdentifier` property is an empty string or the `AWS::RDS::DBInstance` declaration has no `DBSnapshotIdentifier` property, AWS CloudFormation creates a new database. If the property contains a value (other than an empty string), AWS CloudFormation creates a database from the specified snapshot. If a snapshot with the specified name doesn't exist, AWS CloudFormation can't create the database and it rolls back the stack.\n\nSome DB instance properties aren't valid when you restore from a snapshot, such as the `MasterUsername` and `MasterUserPassword` properties. For information about the properties that you can specify, see the `RestoreDBInstanceFromDBSnapshot` action in the *Amazon RDS API Reference* .\n\nAfter you restore a DB instance with a `DBSnapshotIdentifier` property, you must specify the same `DBSnapshotIdentifier` property for any future updates to the DB instance. When you specify this property for an update, the DB instance is not restored from the DB snapshot again, and the data in the database is not changed. However, if you don't specify the `DBSnapshotIdentifier` property, an empty DB instance is created, and the original DB instance is deleted. If you specify a property that is different from the previous snapshot restore property, a new DB instance is restored from the specified `DBSnapshotIdentifier` property, and the original DB instance is deleted.\n\nIf you specify the `DBSnapshotIdentifier` property to restore a DB instance (as opposed to specifying it for DB instance updates), then don't specify the following properties:\n\n- `CharacterSetName`\n- `DBClusterIdentifier`\n- `DBName`\n- `DeleteAutomatedBackups`\n- `EnablePerformanceInsights`\n- `KmsKeyId`\n- `MasterUsername`\n- `MonitoringInterval`\n- `MonitoringRoleArn`\n- `PerformanceInsightsKMSKeyId`\n- `PerformanceInsightsRetentionPeriod`\n- `PromotionTier`\n- `SourceDBInstanceIdentifier`\n- `SourceRegion`\n- `StorageEncrypted`\n- `Timezone`", - "DBSubnetGroupName": "A DB subnet group to associate with the DB instance. If you update this value, the new subnet group must be a subnet group in a new VPC.\n\nIf there's no DB subnet group, then the DB instance isn't a VPC DB instance.\n\nFor more information about using Amazon RDS in a VPC, see [Using Amazon RDS with Amazon Virtual Private Cloud (VPC)](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_VPC.html) in the *Amazon Relational Database Service Developer Guide* .\n\n*Amazon Aurora*\n\nNot applicable. The DB subnet group is managed by the DB cluster. If specified, the setting must match the DB cluster setting.", + "DBSubnetGroupName": "A DB subnet group to associate with the DB instance. If you update this value, the new subnet group must be a subnet group in a new VPC.\n\nIf there's no DB subnet group, then the DB instance isn't a VPC DB instance.\n\nFor more information about using Amazon RDS in a VPC, see [Using Amazon RDS with Amazon Virtual Private Cloud (VPC)](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_VPC.html) in the *Amazon RDS User Guide* .\n\n*Amazon Aurora*\n\nNot applicable. The DB subnet group is managed by the DB cluster. If specified, the setting must match the DB cluster setting.", "DeleteAutomatedBackups": "A value that indicates whether to remove automated backups immediately after the DB instance is deleted. This parameter isn't case-sensitive. The default is to remove automated backups immediately after the DB instance is deleted.", "DeletionProtection": "A value that indicates whether the DB instance has deletion protection enabled. The database can't be deleted when deletion protection is enabled. By default, deletion protection is disabled. For more information, see [Deleting a DB Instance](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_DeleteInstance.html) .\n\n*Amazon Aurora*\n\nNot applicable. You can enable or disable deletion protection for the DB cluster. For more information, see `CreateDBCluster` . DB instances in a DB cluster can be deleted even when deletion protection is enabled for the DB cluster.", "Domain": "The Active Directory directory ID to create the DB instance in. Currently, only Microsoft SQL Server, Oracle, and PostgreSQL DB instances can be created in an Active Directory Domain.\n\nFor more information, see [Kerberos Authentication](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/kerberos-authentication.html) in the *Amazon RDS User Guide* .", @@ -31927,8 +32728,8 @@ "Engine": "The name of the database engine that you want to use for this DB instance.\n\n> When you are creating a DB instance, the `Engine` property is required. \n\nValid Values:\n\n- `aurora` (for MySQL 5.6-compatible Aurora)\n- `aurora-mysql` (for MySQL 5.7-compatible Aurora)\n- `aurora-postgresql`\n- `mariadb`\n- `mysql`\n- `oracle-ee`\n- `oracle-se2`\n- `oracle-se1`\n- `oracle-se`\n- `postgres`\n- `sqlserver-ee`\n- `sqlserver-se`\n- `sqlserver-ex`\n- `sqlserver-web`", "EngineVersion": "The version number of the database engine to use.\n\nFor a list of valid engine versions, use the `DescribeDBEngineVersions` action.\n\nThe following are the database engines and links to information about the major and minor versions that are available with Amazon RDS. Not every database engine is available for every AWS Region.\n\n*Amazon Aurora*\n\nNot applicable. The version number of the database engine to be used by the DB instance is managed by the DB cluster.\n\n*MariaDB*\n\nSee [MariaDB on Amazon RDS Versions](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_MariaDB.html#MariaDB.Concepts.VersionMgmt) in the *Amazon RDS User Guide.*\n\n*Microsoft SQL Server*\n\nSee [Microsoft SQL Server Versions on Amazon RDS](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_SQLServer.html#SQLServer.Concepts.General.VersionSupport) in the *Amazon RDS User Guide.*\n\n*MySQL*\n\nSee [MySQL on Amazon RDS Versions](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_MySQL.html#MySQL.Concepts.VersionMgmt) in the *Amazon RDS User Guide.*\n\n*Oracle*\n\nSee [Oracle Database Engine Release Notes](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Appendix.Oracle.PatchComposition.html) in the *Amazon RDS User Guide.*\n\n*PostgreSQL*\n\nSee [Supported PostgreSQL Database Versions](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_PostgreSQL.html#PostgreSQL.Concepts.General.DBVersions) in the *Amazon RDS User Guide.*", "Iops": "The number of I/O operations per second (IOPS) that the database provisions. The value must be equal to or greater than 1000.\n\nIf you specify this property, you must follow the range of allowed ratios of your requested IOPS rate to the amount of storage that you allocate (IOPS to allocated storage). For example, you can provision an Oracle database instance with 1000 IOPS and 200 GiB of storage (a ratio of 5:1), or specify 2000 IOPS with 200 GiB of storage (a ratio of 10:1). For more information, see [Amazon RDS Provisioned IOPS Storage to Improve Performance](https://docs.aws.amazon.com/AmazonRDS/latest/DeveloperGuide/CHAP_Storage.html#USER_PIOPS) in the *Amazon RDS User Guide* .\n\n> If you specify `io1` for the `StorageType` property, then you must also specify the `Iops` property.", - "KmsKeyId": "The ARN of the AWS Key Management Service ( AWS KMS) master key that's used to encrypt the DB instance, such as `arn:aws:kms:us-east-1:012345678910:key/abcd1234-a123-456a-a12b-a123b4cd56ef` . If you enable the StorageEncrypted property but don't specify this property, AWS CloudFormation uses the default master key. If you specify this property, you must set the StorageEncrypted property to true.\n\nIf you specify the `SourceDBInstanceIdentifier` property, the value is inherited from the source DB instance if the read replica is created in the same region.\n\nIf you create an encrypted read replica in a different AWS Region, then you must specify a KMS key for the destination AWS Region. KMS encryption keys are specific to the region that they're created in, and you can't use encryption keys from one region in another region.\n\nIf you specify the `SnapshotIdentifier` property, the `StorageEncrypted` property value is inherited from the snapshot, and if the DB instance is encrypted, the specified `KmsKeyId` property is used.\n\nIf you specify `DBSecurityGroups` , AWS CloudFormation ignores this property. To specify both a security group and this property, you must use a VPC security group. For more information about Amazon RDS and VPC, see [Using Amazon RDS with Amazon VPC](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-database-instance.html) in the *Amazon RDS User Guide* .\n\n*Amazon Aurora*\n\nNot applicable. The KMS key identifier is managed by the DB cluster.", - "LicenseModel": "License model information for this DB instance.\n\nValid values: `license-included` | `bring-your-own-license` | `general-public-license`\n\n> If you've specified `DBSecurityGroups` and then you update the license model, AWS CloudFormation replaces the underlying DB instance. This will incur some interruptions to database availability.", + "KmsKeyId": "The ARN of the AWS KMS key that's used to encrypt the DB instance, such as `arn:aws:kms:us-east-1:012345678910:key/abcd1234-a123-456a-a12b-a123b4cd56ef` . If you enable the StorageEncrypted property but don't specify this property, AWS CloudFormation uses the default KMS key. If you specify this property, you must set the StorageEncrypted property to true.\n\nIf you specify the `SourceDBInstanceIdentifier` property, the value is inherited from the source DB instance if the read replica is created in the same region.\n\nIf you create an encrypted read replica in a different AWS Region, then you must specify a KMS key for the destination AWS Region. KMS encryption keys are specific to the region that they're created in, and you can't use encryption keys from one region in another region.\n\nIf you specify the `SnapshotIdentifier` property, the `StorageEncrypted` property value is inherited from the snapshot, and if the DB instance is encrypted, the specified `KmsKeyId` property is used.\n\nIf you specify `DBSecurityGroups` , AWS CloudFormation ignores this property. To specify both a security group and this property, you must use a VPC security group. For more information about Amazon RDS and VPC, see [Using Amazon RDS with Amazon VPC](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-database-instance.html) in the *Amazon RDS User Guide* .\n\n*Amazon Aurora*\n\nNot applicable. The KMS key identifier is managed by the DB cluster.", + "LicenseModel": "License model information for this DB instance.\n\nValid values:\n\n- Aurora MySQL - `general-public-license`\n- Aurora PostgreSQL - `postgresql-license`\n- MariaDB - `general-public-license`\n- Microsoft SQL Server - `license-included`\n- MySQL - `general-public-license`\n- Oracle - `bring-your-own-license` or `license-included`\n- PostgreSQL - `postgresql-license`\n\n> If you've specified `DBSecurityGroups` and then you update the license model, AWS CloudFormation replaces the underlying DB instance. This will incur some interruptions to database availability.", "MasterUserPassword": "The password for the master user. The password can include any printable ASCII character except \"/\", \"\"\", or \"@\".\n\n*Amazon Aurora*\n\nNot applicable. The password for the master user is managed by the DB cluster.\n\n*MariaDB*\n\nConstraints: Must contain from 8 to 41 characters.\n\n*Microsoft SQL Server*\n\nConstraints: Must contain from 8 to 128 characters.\n\n*MySQL*\n\nConstraints: Must contain from 8 to 41 characters.\n\n*Oracle*\n\nConstraints: Must contain from 8 to 30 characters.\n\n*PostgreSQL*\n\nConstraints: Must contain from 8 to 128 characters.", "MasterUsername": "The master user name for the DB instance.\n\n> If you specify the `SourceDBInstanceIdentifier` or `DBSnapshotIdentifier` property, don't specify this property. The value is inherited from the source DB instance or snapshot. \n\n*Amazon Aurora*\n\nNot applicable. The name for the master user is managed by the DB cluster.\n\n*MariaDB*\n\nConstraints:\n\n- Required for MariaDB.\n- Must be 1 to 16 letters or numbers.\n- Can't be a reserved word for the chosen database engine.\n\n*Microsoft SQL Server*\n\nConstraints:\n\n- Required for SQL Server.\n- Must be 1 to 128 letters or numbers.\n- The first character must be a letter.\n- Can't be a reserved word for the chosen database engine.\n\n*MySQL*\n\nConstraints:\n\n- Required for MySQL.\n- Must be 1 to 16 letters or numbers.\n- First character must be a letter.\n- Can't be a reserved word for the chosen database engine.\n\n*Oracle*\n\nConstraints:\n\n- Required for Oracle.\n- Must be 1 to 30 letters or numbers.\n- First character must be a letter.\n- Can't be a reserved word for the chosen database engine.\n\n*PostgreSQL*\n\nConstraints:\n\n- Required for PostgreSQL.\n- Must be 1 to 63 letters or numbers.\n- First character must be a letter.\n- Can't be a reserved word for the chosen database engine.", "MaxAllocatedStorage": "The upper limit in gibibytes (GiB) to which Amazon RDS can automatically scale the storage of the DB instance.\n\nFor more information about this setting, including limitations that apply to it, see [Managing capacity automatically with Amazon RDS storage autoscaling](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_PIOPS.StorageTypes.html#USER_PIOPS.Autoscaling) in the *Amazon RDS User Guide* .\n\nThis setting doesn't apply to RDS Custom.", @@ -31936,7 +32737,7 @@ "MonitoringRoleArn": "The ARN for the IAM role that permits RDS to send enhanced monitoring metrics to Amazon CloudWatch Logs. For example, `arn:aws:iam:123456789012:role/emaccess` . For information on creating a monitoring role, see [Setting Up and Enabling Enhanced Monitoring](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Monitoring.OS.html#USER_Monitoring.OS.Enabling) in the *Amazon RDS User Guide* .\n\nIf `MonitoringInterval` is set to a value other than 0, then you must supply a `MonitoringRoleArn` value.\n\nThis setting doesn't apply to RDS Custom.", "MultiAZ": "Specifies whether the database instance is a multiple Availability Zone deployment. You can't set the `AvailabilityZone` parameter if the `MultiAZ` parameter is set to true.\n\n*Amazon Aurora*\n\nNot applicable. Amazon Aurora storage is replicated across all of the Availability Zones and doesn't require the `MultiAZ` option to be set.", "OptionGroupName": "Indicates that the DB instance should be associated with the specified option group.\n\nPermanent options, such as the TDE option for Oracle Advanced Security TDE, can't be removed from an option group. Also, that option group can't be removed from a DB instance once it is associated with a DB instance.", - "PerformanceInsightsKMSKeyId": "The AWS KMS key identifier for encryption of Performance Insights data.\n\nThe AWS KMS key identifier is the key ARN, key ID, alias ARN, or alias name for the AWS KMS customer master key (CMK).\n\nIf you do not specify a value for `PerformanceInsightsKMSKeyId` , then Amazon RDS uses your default CMK. There is a default CMK for your AWS account. Your AWS account has a different default CMK for each AWS Region.\n\nFor information about enabling Performance Insights, see [EnablePerformanceInsights](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-database-instance.html#cfn-rds-dbinstance-enableperformanceinsights) .", + "PerformanceInsightsKMSKeyId": "The AWS KMS key identifier for encryption of Performance Insights data.\n\nThe KMS key identifier is the key ARN, key ID, alias ARN, or alias name for the KMS key.\n\nIf you do not specify a value for `PerformanceInsightsKMSKeyId` , then Amazon RDS uses your default KMS key. There is a default KMS key for your AWS account. Your AWS account has a different default KMS key for each AWS Region.\n\nFor information about enabling Performance Insights, see [EnablePerformanceInsights](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-database-instance.html#cfn-rds-dbinstance-enableperformanceinsights) .", "PerformanceInsightsRetentionPeriod": "The amount of time, in days, to retain Performance Insights data. Valid values are 7 or 731 (2 years).\n\nFor information about enabling Performance Insights, see [EnablePerformanceInsights](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-database-instance.html#cfn-rds-dbinstance-enableperformanceinsights) .", "Port": "The port number on which the database accepts connections.", "PreferredBackupWindow": "The daily time range during which automated backups are created if automated backups are enabled, using the `BackupRetentionPeriod` parameter. For more information, see [Backup Window](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithAutomatedBackups.html#USER_WorkingWithAutomatedBackups.BackupWindow) in the *Amazon RDS User Guide.*\n\nConstraints:\n\n- Must be in the format `hh24:mi-hh24:mi` .\n- Must be in Universal Coordinated Time (UTC).\n- Must not conflict with the preferred maintenance window.\n- Must be at least 30 minutes.\n\n*Amazon Aurora*\n\nNot applicable. The daily time range for creating automated backups is managed by the DB cluster.", @@ -31944,7 +32745,7 @@ "ProcessorFeatures": "The number of CPU cores and the number of threads per core for the DB instance class of the DB instance.\n\nThis setting doesn't apply to RDS Custom.", "PromotionTier": "A value that specifies the order in which an Aurora Replica is promoted to the primary instance after a failure of the existing primary instance. For more information, see [Fault Tolerance for an Aurora DB Cluster](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Managing.Backups.html#Aurora.Managing.FaultTolerance) in the *Amazon Aurora User Guide* .\n\nThis setting doesn't apply to RDS Custom.\n\nDefault: 1\n\nValid Values: 0 - 15", "PubliclyAccessible": "Indicates whether the DB instance is an internet-facing instance. If you specify `true` , AWS CloudFormation creates an instance with a publicly resolvable DNS name, which resolves to a public IP address. If you specify false, AWS CloudFormation creates an internal instance with a DNS name that resolves to a private IP address.\n\nThe default behavior value depends on your VPC setup and the database subnet group. For more information, see the `PubliclyAccessible` parameter in [`CreateDBInstance`](https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_CreateDBInstance.html) in the *Amazon RDS API Reference* .\n\nIf this resource has a public IP address and is also in a VPC that is defined in the same template, you must use the *DependsOn* attribute to declare a dependency on the VPC-gateway attachment. For more information, see [DependsOn Attribute](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-dependson.html) .\n\n> If you specify DBSecurityGroups, AWS CloudFormation ignores this property. To specify a security group and this property, you must use a VPC security group. For more information about Amazon RDS and VPC, see [Using Amazon RDS with Amazon VPC](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_VPC.html) in the *Amazon RDS User Guide* .", - "SourceDBInstanceIdentifier": "If you want to create a read replica DB instance, specify the ID of the source DB instance. Each DB instance can have a limited number of read replicas. For more information, see [Working with Read Replicas](https://docs.aws.amazon.com/AmazonRDS/latest/DeveloperGuide/USER_ReadRepl.html) in the *Amazon Relational Database Service Developer Guide* .\n\nThe `SourceDBInstanceIdentifier` property determines whether a DB instance is a read replica. If you remove the `SourceDBInstanceIdentifier` property from your template and then update your stack, AWS CloudFormation deletes the Read Replica and creates a new DB instance (not a read replica).\n\n> - If you specify a source DB instance that uses VPC security groups, we recommend that you specify the `VPCSecurityGroups` property. If you don't specify the property, the read replica inherits the value of the `VPCSecurityGroups` property from the source DB when you create the replica. However, if you update the stack, AWS CloudFormation reverts the replica's `VPCSecurityGroups` property to the default value because it's not defined in the stack's template. This change might cause unexpected issues.\n> - Read replicas don't support deletion policies. AWS CloudFormation ignores any deletion policy that's associated with a read replica.\n> - If you specify `SourceDBInstanceIdentifier` , don't specify the `DBSnapshotIdentifier` property. You can't create a read replica from a snapshot.\n> - Don't set the `BackupRetentionPeriod` , `DBName` , `MasterUsername` , `MasterUserPassword` , and `PreferredBackupWindow` properties. The database attributes are inherited from the source DB instance, and backups are disabled for read replicas.\n> - If the source DB instance is in a different region than the read replica, specify the source region in `SourceRegion` , and specify an ARN for a valid DB instance in `SourceDBInstanceIdentifier` . For more information, see [Constructing a Amazon RDS Amazon Resource Name (ARN)](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Tagging.html#USER_Tagging.ARN) in the *Amazon RDS User Guide* .\n> - For DB instances in Amazon Aurora clusters, don't specify this property. Amazon RDS automatically assigns writer and reader DB instances.", + "SourceDBInstanceIdentifier": "If you want to create a read replica DB instance, specify the ID of the source DB instance. Each DB instance can have a limited number of read replicas. For more information, see [Working with Read Replicas](https://docs.aws.amazon.com/AmazonRDS/latest/DeveloperGuide/USER_ReadRepl.html) in the *Amazon RDS User Guide* .\n\nFor information about constraints that apply to DB instance identifiers, see [Naming constraints in Amazon RDS](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Limits.html#RDS_Limits.Constraints) in the *Amazon RDS User Guide* .\n\nThe `SourceDBInstanceIdentifier` property determines whether a DB instance is a read replica. If you remove the `SourceDBInstanceIdentifier` property from your template and then update your stack, AWS CloudFormation deletes the Read Replica and creates a new DB instance (not a read replica).\n\n> - If you specify a source DB instance that uses VPC security groups, we recommend that you specify the `VPCSecurityGroups` property. If you don't specify the property, the read replica inherits the value of the `VPCSecurityGroups` property from the source DB when you create the replica. However, if you update the stack, AWS CloudFormation reverts the replica's `VPCSecurityGroups` property to the default value because it's not defined in the stack's template. This change might cause unexpected issues.\n> - Read replicas don't support deletion policies. AWS CloudFormation ignores any deletion policy that's associated with a read replica.\n> - If you specify `SourceDBInstanceIdentifier` , don't specify the `DBSnapshotIdentifier` property. You can't create a read replica from a snapshot.\n> - Don't set the `BackupRetentionPeriod` , `DBName` , `MasterUsername` , `MasterUserPassword` , and `PreferredBackupWindow` properties. The database attributes are inherited from the source DB instance, and backups are disabled for read replicas.\n> - If the source DB instance is in a different region than the read replica, specify the source region in `SourceRegion` , and specify an ARN for a valid DB instance in `SourceDBInstanceIdentifier` . For more information, see [Constructing a Amazon RDS Amazon Resource Name (ARN)](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Tagging.html#USER_Tagging.ARN) in the *Amazon RDS User Guide* .\n> - For DB instances in Amazon Aurora clusters, don't specify this property. Amazon RDS automatically assigns writer and reader DB instances.", "SourceRegion": "The ID of the region that contains the source DB instance for the read replica.", "StorageEncrypted": "A value that indicates whether the DB instance is encrypted. By default, it isn't encrypted.\n\nIf you specify the `KmsKeyId` property, then you must enable encryption.\n\nIf you specify the `SnapshotIdentifier` or `SourceDBInstanceIdentifier` property, don't specify this property. The value is inherited from the snapshot or source DB instance, and if the DB instance is encrypted, the specified `KmsKeyId` property is used.\n\n*Amazon Aurora*\n\nNot applicable. The encryption for DB instances is managed by the DB cluster.", "StorageType": "Specifies the storage type to be associated with the DB instance.\n\nValid values: `standard | gp2 | io1`\n\nThe `standard` value is also known as magnetic.\n\nIf you specify `io1` , you must also include a value for the `Iops` parameter.\n\nDefault: `io1` if the `Iops` parameter is specified, otherwise `standard`\n\nFor more information, see [Amazon RDS DB Instance Storage](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html) in the *Amazon RDS User Guide* .\n\n*Amazon Aurora*\n\nNot applicable. Aurora data is stored in the cluster volume, which is a single, virtual volume that uses solid state drives (SSDs).", @@ -31986,8 +32787,7 @@ "attributes": { "DBProxyArn": "The Amazon Resource Name (ARN) representing the target group.", "Endpoint": "The writer endpoint for the RDS DB instance or Aurora DB cluster.", - "Ref": "`Ref` returns the name of the DB proxy.", - "VpcId": "The VPC ID to associate with the DB proxy." + "Ref": "`Ref` returns the name of the DB proxy." }, "description": "The `AWS::RDS::DBProxy` resource creates or updates a DB proxy.\n\nFor information about RDS Proxy for Amazon RDS, see [Managing Connections with Amazon RDS Proxy](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy.html) in the *Amazon RDS User Guide* .\n\nFor information about RDS Proxy for Amazon Aurora, see [Managing Connections with Amazon RDS Proxy](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/rds-proxy.html) in the *Amazon Aurora User Guide* .\n\n> Limitations apply to RDS Proxy, including DB engine version limitations and AWS Region limitations.\n> \n> For information about limitations that apply to RDS Proxy for Amazon RDS, see [Limitations for RDS Proxy](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy.html#rds-proxy.limitations) in the *Amazon RDS User Guide* .\n> \n> For information about that apply to RDS Proxy for Amazon Aurora, see [Limitations for RDS Proxy](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/rds-proxy.html#rds-proxy.limitations) in the *Amazon Aurora User Guide* .", "properties": { @@ -32068,8 +32868,8 @@ "properties": { "ConnectionBorrowTimeout": "The number of seconds for a proxy to wait for a connection to become available in the connection pool. Only applies when the proxy has opened its maximum number of connections and all connections are busy with client sessions.\n\nDefault: 120\n\nConstraints: between 1 and 3600, or 0 representing unlimited", "InitQuery": "One or more SQL statements for the proxy to run when opening each new database connection. Typically used with `SET` statements to make sure that each connection has identical settings such as time zone and character set. For multiple statements, use semicolons as the separator. You can also include multiple variables in a single `SET` statement, such as `SET x=1, y=2` .\n\nDefault: no initialization query", - "MaxConnectionsPercent": "The maximum size of the connection pool for each target in a target group. For Aurora MySQL, it is expressed as a percentage of the `max_connections` setting for the RDS DB instance or Aurora DB cluster used by the target group.\n\nDefault: 100\n\nConstraints: between 1 and 100", - "MaxIdleConnectionsPercent": "Controls how actively the proxy closes idle database connections in the connection pool. A high value enables the proxy to leave a high percentage of idle connections open. A low value causes the proxy to close idle client connections and return the underlying database connections to the connection pool. For Aurora MySQL, it is expressed as a percentage of the `max_connections` setting for the RDS DB instance or Aurora DB cluster used by the target group.\n\nDefault: 50\n\nConstraints: between 0 and `MaxConnectionsPercent`", + "MaxConnectionsPercent": "The maximum size of the connection pool for each target in a target group. The value is expressed as a percentage of the `max_connections` setting for the RDS DB instance or Aurora DB cluster used by the target group.\n\nDefault: 100\n\nConstraints: between 1 and 100", + "MaxIdleConnectionsPercent": "Controls how actively the proxy closes idle database connections in the connection pool. The value is expressed as a percentage of the `max_connections` setting for the RDS DB instance or Aurora DB cluster used by the target group. With a high value, the proxy leaves a high percentage of idle database connections open. A low value causes the proxy to close more idle connections and return them to the database.\n\nDefault: 50\n\nConstraints: between 0 and `MaxConnectionsPercent`", "SessionPinningFilters": "Each item in the list represents a class of SQL operations that normally cause all later statements in a session using a proxy to be pinned to the same underlying database connection. Including an item in the list exempts that class of SQL operations from the pinning behavior.\n\nDefault: no session pinning filters" } }, @@ -32186,7 +32986,7 @@ }, "description": "Creates a CloudWatch RUM app monitor, which you can use to collect telemetry data from your application and send it to CloudWatch RUM. The data includes performance and reliability information such as page load time, client-side errors, and user behavior.\n\nAfter you create an app monitor, sign in to the CloudWatch RUM console to get the JavaScript code snippet to add to your web application. For more information, see [How do I find a code snippet that I've already generated?](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-RUM-find-code-snippet.html)", "properties": { - "AppMonitorConfiguration": "A structure that contains much of the configuration data for the app monitor. If you are using Amazon Cognito for authorization, you must include this structure in your request, and it must include the ID of the Amazon Cognito identity pool to use for authorization. If you don't include `AppMonitorConfiguration` , you must set up your own authorization method. For more information, see [Authorize your application to send data to AWS](https://docs.aws.amazon.com/monitoring/CloudWatch-RUM-get-started-authorization.html) .\n\nIf you omit this argument, the sample rate used for CloudWatch RUM is set to 10% of the user sessions.", + "AppMonitorConfiguration": "A structure that contains much of the configuration data for the app monitor. If you are using Amazon Cognito for authorization, you must include this structure in your request, and it must include the ID of the Amazon Cognito identity pool to use for authorization. If you don't include `AppMonitorConfiguration` , you must set up your own authorization method. For more information, see [Authorize your application to send data to AWS](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-RUM-get-started-authorization.html) .\n\nIf you omit this argument, the sample rate used for CloudWatch RUM is set to 10% of the user sessions.", "CwLogEnabled": "Data collected by CloudWatch RUM is kept by RUM for 30 days and then deleted. This parameter specifies whether CloudWatch RUM sends a copy of this telemetry data to Amazon CloudWatch Logs in your account. This enables you to keep the telemetry data for more than 30 days, but it does incur Amazon CloudWatch Logs charges.\n\nIf you omit this parameter, the default is `false` .", "Domain": "The top-level internet domain name for which your application has administrative authority. This parameter is required.", "Name": "A name for the app monitor. This parameter is required.", @@ -32242,12 +33042,12 @@ "EnhancedVpcRouting": "An option that specifies whether to create the cluster with enhanced VPC routing enabled. To create a cluster that uses enhanced VPC routing, the cluster must be in a VPC. For more information, see [Enhanced VPC Routing](https://docs.aws.amazon.com/redshift/latest/mgmt/enhanced-vpc-routing.html) in the Amazon Redshift Cluster Management Guide.\n\nIf this option is `true` , enhanced VPC routing is enabled.\n\nDefault: false", "HsmClientCertificateIdentifier": "Specifies the name of the HSM client certificate the Amazon Redshift cluster uses to retrieve the data encryption keys stored in an HSM.", "HsmConfigurationIdentifier": "Specifies the name of the HSM configuration that contains the information the Amazon Redshift cluster can use to retrieve and store keys in an HSM.", - "IamRoles": "A list of AWS Identity and Access Management (IAM) roles that can be used by the cluster to access other AWS services. You must supply the IAM roles in their Amazon Resource Name (ARN) format. You can supply up to 10 IAM roles in a single request.\n\nA cluster can have up to 10 IAM roles associated with it at any time.", + "IamRoles": "A list of AWS Identity and Access Management (IAM) roles that can be used by the cluster to access other AWS services. You must supply the IAM roles in their Amazon Resource Name (ARN) format.\n\nThe maximum number of IAM roles that you can associate is subject to a quota. For more information, go to [Quotas and limits](https://docs.aws.amazon.com/redshift/latest/mgmt/amazon-redshift-limits.html) in the *Amazon Redshift Cluster Management Guide* .", "KmsKeyId": "The AWS Key Management Service (KMS) key ID of the encryption key that you want to use to encrypt data in the cluster.", "LoggingProperties": "Specifies logging information, such as queries and connection attempts, for the specified Amazon Redshift cluster.", "MaintenanceTrackName": "An optional parameter for the name of the maintenance track for the cluster. If you don't provide a maintenance track name, the cluster is assigned to the `current` track.", "ManualSnapshotRetentionPeriod": "The default number of days to retain a manual snapshot. If the value is -1, the snapshot is retained indefinitely. This setting doesn't change the retention period of existing snapshots.\n\nThe value must be either -1 or an integer between 1 and 3,653.", - "MasterUserPassword": "The password associated with the admin user account for the cluster that is being created.\n\nConstraints:\n\n- Must be between 8 and 64 characters in length.\n- Must contain at least one uppercase letter.\n- Must contain at least one lowercase letter.\n- Must contain one number.\n- Can be any printable ASCII character (ASCII code 33 to 126) except ' (single quote), \" (double quote), \\, /, @, or space.", + "MasterUserPassword": "The password associated with the admin user account for the cluster that is being created.\n\nConstraints:\n\n- Must be between 8 and 64 characters in length.\n- Must contain at least one uppercase letter.\n- Must contain at least one lowercase letter.\n- Must contain one number.\n- Can be any printable ASCII character (ASCII code 33-126) except ' (single quote), \" (double quote), \\, /, or @.", "MasterUsername": "The user name associated with the admin user account for the cluster that is being created.\n\nConstraints:\n\n- Must be 1 - 128 alphanumeric characters. The user name can't be `PUBLIC` .\n- First character must be a letter.\n- Cannot be a reserved word. A list of reserved words can be found in [Reserved Words](https://docs.aws.amazon.com/redshift/latest/dg/r_pg_keywords.html) in the Amazon Redshift Database Developer Guide.", "NodeType": "The node type to be provisioned for the cluster. For information about node types, go to [Working with Clusters](https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-clusters.html#how-many-nodes) in the *Amazon Redshift Cluster Management Guide* .\n\nValid Values: `ds2.xlarge` | `ds2.8xlarge` | `dc1.large` | `dc1.8xlarge` | `dc2.large` | `dc2.8xlarge` | `ra3.xlplus` | `ra3.4xlarge` | `ra3.16xlarge`", "NumberOfNodes": "The number of compute nodes in the cluster. This parameter is required when the *ClusterType* parameter is specified as `multi-node` .\n\nFor information about determining how many nodes you need, go to [Working with Clusters](https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-clusters.html#how-many-nodes) in the *Amazon Redshift Cluster Management Guide* .\n\nIf you don't specify this parameter, you get a single-node cluster. When requesting a multi-node cluster, you must specify the number of nodes that you want in the cluster.\n\nDefault: `1`\n\nConstraints: Value must be at least 1 and no more than 100.", @@ -32558,6 +33358,17 @@ "Url": "The URL to route traffic to. The URL must be an [rfc3986-formatted URL](https://docs.aws.amazon.com/https://datatracker.ietf.org/doc/html/rfc3986) . If the host is a domain name, the name must be resolvable over the public internet. If the scheme is `https` , the top level domain of the host must be listed in the [IANA root zone database](https://docs.aws.amazon.com/https://www.iana.org/domains/root/db) ." } }, + "AWS::Rekognition::Collection": { + "attributes": { + "Arn": "Returns the Amazon Resource Name of the collection.", + "Ref": "`Ref` returns the collection ID. For example:\n\n`{ \"Ref\": \"MyCollection\" }`" + }, + "description": "The `AWS::Rekognition::Collection` type creates a server-side container called a collection. You can use a collection to store information about detected faces and search for known faces in images, stored videos, and streaming videos.", + "properties": { + "CollectionId": "ID for the collection that you are creating.", + "Tags": "A set of tags (key-value pairs) that you want to attach to the collection." + } + }, "AWS::Rekognition::Project": { "attributes": { "Arn": "Returns the Amazon Resource Name of the project.", @@ -32695,7 +33506,7 @@ }, "AWS::RoboMaker::Robot": { "attributes": { - "Arn": "", + "Arn": "The Amazon Resource Name (ARN) of the robot.", "Ref": "When you pass the logical ID of an `AWS::RoboMaker::Robot` resource to the intrinsic `Ref` function, the function returns the Amazon Resource Name (ARN) of the robot application, such as `arn:aws:robomaker:us-west-2:123456789012:robot/MyRobot/1544035373264` ." }, "description": "The `AWS::RoboMaker::RobotApplication` resource creates an AWS RoboMaker robot.", @@ -32716,6 +33527,7 @@ "description": "The `AWS::RoboMaker::RobotApplication` resource creates an AWS RoboMaker robot application.", "properties": { "CurrentRevisionId": "The current revision id.", + "Environment": "The environment of the robot application.", "Name": "The name of the robot application.", "RobotSoftwareSuite": "The robot software suite (ROS distribuition) used by the robot application.", "Sources": "The sources of the robot application.", @@ -32741,8 +33553,8 @@ }, "AWS::RoboMaker::RobotApplicationVersion": { "attributes": { - "ApplicationVersion": "", - "Arn": "", + "ApplicationVersion": "The robot application version.", + "Arn": "The Amazon Resource Name (ARN) of the robot application version.", "Ref": "When you pass the logical ID of an `AWS::RoboMaker::RobotApplicationVersion` resource to the intrinsic `Ref` function, the function returns the Amazon Resource Name (ARN) of the robot application version, such as `arn:aws:robomaker:us-west-2:123456789012:robot-application/MyRobotApplication/1546541208251` ." }, "description": "The `AWS::RoboMaker::RobotApplicationVersion` resource creates an AWS RoboMaker robot version.", @@ -32760,7 +33572,7 @@ "description": "The `AWS::RoboMaker::SimulationApplication` resource creates an AWS RoboMaker simulation application.", "properties": { "CurrentRevisionId": "The current revision id.", - "Environment": "", + "Environment": "The environment of the simulation application.", "Name": "The name of the simulation application.", "RenderingEngine": "The rendering engine for the simulation application.", "RobotSoftwareSuite": "The robot software suite (ROS distribution) used by the simulation application.", @@ -32804,8 +33616,8 @@ }, "AWS::RoboMaker::SimulationApplicationVersion": { "attributes": { - "ApplicationVersion": "", - "Arn": "", + "ApplicationVersion": "The simulation application version.", + "Arn": "The Amazon Resource Name (ARN) of the simulation application version.", "Ref": "When you pass the logical ID of an `AWS::RoboMaker::SimulationApplicationVersion` resource to the intrinsic `Ref` function, the function returns the Amazon Resource Name (ARN) of the simulation application version, such as `arn:aws:robomaker:us-west-2:123456789012:simulation-application/MySimulationApplication/1546541201334` ." }, "description": "The `AWS::RoboMaker::SimulationApplicationVersion` resource creates a version of an AWS RoboMaker simulation application.", @@ -32848,12 +33660,12 @@ "NameServers": "Returns the set of name servers for the specific hosted zone. For example: `ns1.example.com` .\n\nThis attribute is not supported for private hosted zones.", "Ref": "`Ref` returns the hosted zone ID, such as `Z23ABC4XYZL05B` ." }, - "description": "Creates a new public or private hosted zone. You create records in a public hosted zone to define how you want to route traffic on the internet for a domain, such as example.com, and its subdomains (apex.example.com, acme.example.com). You create records in a private hosted zone to define how you want to route traffic for a domain and its subdomains within one or more Amazon Virtual Private Clouds (Amazon VPCs).\n\n> You can't convert a public hosted zone to a private hosted zone or vice versa. Instead, you must create a new hosted zone with the same name and create new resource record sets. \n\nFor more information about charges for hosted zones, see [Amazon Route 53 Pricing](https://docs.aws.amazon.com/route53/pricing/) .\n\nNote the following:\n\n- You can't create a hosted zone for a top-level domain (TLD) such as .com.\n- For public hosted zones, Route 53 automatically creates a default SOA record and four NS records for the zone. For more information about SOA and NS records, see [NS and SOA Records that Route 53 Creates for a Hosted Zone](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/SOA-NSrecords.html) in the *Amazon Route 53 Developer Guide* .\n\nIf you want to use the same name servers for multiple public hosted zones, you can optionally associate a reusable delegation set with the hosted zone. See the `DelegationSetId` element.\n- If your domain is registered with a registrar other than Route 53, you must update the name servers with your registrar to make Route 53 the DNS service for the domain. For more information, see [Migrating DNS Service for an Existing Domain to Amazon Route 53](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/MigratingDNS.html) in the *Amazon Route 53 Developer Guide* .\n\nWhen you submit a `CreateHostedZone` request, the initial status of the hosted zone is `PENDING` . For public hosted zones, this means that the NS and SOA records are not yet available on all Route 53 DNS servers. When the NS and SOA records are available, the status of the zone changes to `INSYNC` .\n\nThe `CreateHostedZone` request requires the caller to have an `ec2:DescribeVpcs` permission.", + "description": "Creates a new public or private hosted zone. You create records in a public hosted zone to define how you want to route traffic on the internet for a domain, such as example.com, and its subdomains (apex.example.com, acme.example.com). You create records in a private hosted zone to define how you want to route traffic for a domain and its subdomains within one or more Amazon Virtual Private Clouds (Amazon VPCs).\n\n> You can't convert a public hosted zone to a private hosted zone or vice versa. Instead, you must create a new hosted zone with the same name and create new resource record sets. \n\nFor more information about charges for hosted zones, see [Amazon Route 53 Pricing](https://docs.aws.amazon.com/route53/pricing/) .\n\nNote the following:\n\n- You can't create a hosted zone for a top-level domain (TLD) such as .com.\n- For public hosted zones, Route 53 automatically creates a default SOA record and four NS records for the zone. For more information about SOA and NS records, see [NS and SOA Records that Route 53 Creates for a Hosted Zone](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/SOA-NSrecords.html) in the *Amazon Route 53 Developer Guide* .\n\nIf you want to use the same name servers for multiple public hosted zones, you can optionally associate a reusable delegation set with the hosted zone. See the `DelegationSetId` element.\n- If your domain is registered with a registrar other than Route 53, you must update the name servers with your registrar to make Route 53 the DNS service for the domain. For more information, see [Migrating DNS Service for an Existing Domain to Amazon Route 53](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/MigratingDNS.html) in the *Amazon Route 53 Developer Guide* .\n\nWhen you submit a `CreateHostedZone` request, the initial status of the hosted zone is `PENDING` . For public hosted zones, this means that the NS and SOA records are not yet available on all Route 53 DNS servers. When the NS and SOA records are available, the status of the zone changes to `INSYNC` .\n\nThe `CreateHostedZone` request requires the caller to have an `ec2:DescribeVpcs` permission.\n\n> When creating private hosted zones, the Amazon VPC must belong to the same partition where the hosted zone is created. A partition is a group of AWS Regions . Each AWS account is scoped to one partition.\n> \n> The following are the supported partitions:\n> \n> - `aws` - AWS Regions\n> - `aws-cn` - China Regions\n> - `aws-us-gov` - AWS GovCloud (US) Region\n> \n> For more information, see [Access Management](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) in the *AWS General Reference* .", "properties": { "HostedZoneConfig": "A complex type that contains an optional comment.\n\nIf you don't want to specify a comment, omit the `HostedZoneConfig` and `Comment` elements.", "HostedZoneTags": "Adds, edits, or deletes tags for a health check or a hosted zone.\n\nFor information about using tags for cost allocation, see [Using Cost Allocation Tags](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/cost-alloc-tags.html) in the *AWS Billing and Cost Management User Guide* .", "Name": "The name of the domain. Specify a fully qualified domain name, for example, *www.example.com* . The trailing dot is optional; Amazon Route 53 assumes that the domain name is fully qualified. This means that Route 53 treats *www.example.com* (without a trailing dot) and *www.example.com.* (with a trailing dot) as identical.\n\nIf you're creating a public hosted zone, this is the name you have registered with your DNS registrar. If your domain name is registered with a registrar other than Route 53, change the name servers for your domain to the set of `NameServers` that are returned by the `Fn::GetAtt` intrinsic function.", - "QueryLoggingConfig": "Creates a configuration for DNS query logging. After you create a query logging configuration, Amazon Route 53 begins to publish log data to an Amazon CloudWatch Logs log group.\n\nDNS query logs contain information about the queries that Route 53 receives for a specified public hosted zone, such as the following:\n\n- Route 53 edge location that responded to the DNS query\n- Domain or subdomain that was requested\n- DNS record type, such as A or AAAA\n- DNS response code, such as `NoError` or `ServFail`\n\n- **Log Group and Resource Policy** - Before you create a query logging configuration, perform the following operations.\n\n> If you create a query logging configuration using the Route 53 console, Route 53 performs these operations automatically. \n\n- Create a CloudWatch Logs log group, and make note of the ARN, which you specify when you create a query logging configuration. Note the following:\n\n- You must create the log group in the us-east-1 region.\n- You must use the same AWS account to create the log group and the hosted zone that you want to configure query logging for.\n- When you create log groups for query logging, we recommend that you use a consistent prefix, for example:\n\n`/aws/route53/ *hosted zone name*`\n\nIn the next step, you'll create a resource policy, which controls access to one or more log groups and the associated AWS resources, such as Route 53 hosted zones. There's a limit on the number of resource policies that you can create, so we recommend that you use a consistent prefix so you can use the same resource policy for all the log groups that you create for query logging.\n- Create a CloudWatch Logs resource policy, and give it the permissions that Route 53 needs to create log streams and to send query logs to log streams. For the value of `Resource` , specify the ARN for the log group that you created in the previous step. To use the same resource policy for all the CloudWatch Logs log groups that you created for query logging configurations, replace the hosted zone name with `*` , for example:\n\n`arn:aws:logs:us-east-1:123412341234:log-group:/aws/route53/*`\n\n> You can't use the CloudWatch console to create or edit a resource policy. You must use the CloudWatch API, one of the AWS SDKs, or the AWS CLI .\n- **Log Streams and Edge Locations** - When Route 53 finishes creating the configuration for DNS query logging, it does the following:\n\n- Creates a log stream for an edge location the first time that the edge location responds to DNS queries for the specified hosted zone. That log stream is used to log all queries that Route 53 responds to for that edge location.\n- Begins to send query logs to the applicable log stream.\n\nThe name of each log stream is in the following format:\n\n`*hosted zone ID* / *edge location code*`\n\nThe edge location code is a three-letter code and an arbitrarily assigned number, for example, DFW3. The three-letter code typically corresponds with the International Air Transport Association airport code for an airport near the edge location. (These abbreviations might change in the future.) For a list of edge locations, see \"The Route 53 Global Network\" on the [Route 53 Product Details](https://docs.aws.amazon.com/route53/details/) page.\n- **Queries That Are Logged** - Query logs contain only the queries that DNS resolvers forward to Route 53. If a DNS resolver has already cached the response to a query (such as the IP address for a load balancer for example.com), the resolver will continue to return the cached response. It doesn't forward another query to Route 53 until the TTL for the corresponding resource record set expires. Depending on how many DNS queries are submitted for a resource record set, and depending on the TTL for that resource record set, query logs might contain information about only one query out of every several thousand queries that are submitted to DNS. For more information about how DNS works, see [Routing Internet Traffic to Your Website or Web Application](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/welcome-dns-service.html) in the *Amazon Route 53 Developer Guide* .\n- **Log File Format** - For a list of the values in each query log and the format of each value, see [Logging DNS Queries](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/query-logs.html) in the *Amazon Route 53 Developer Guide* .\n- **Pricing** - For information about charges for query logs, see [Amazon CloudWatch Pricing](https://docs.aws.amazon.com/cloudwatch/pricing/) .\n- **How to Stop Logging** - If you want Route 53 to stop sending query logs to CloudWatch Logs, delete the query logging configuration. For more information, see [DeleteQueryLoggingConfig](https://docs.aws.amazon.com/Route53/latest/APIReference/API_DeleteQueryLoggingConfig.html) .", + "QueryLoggingConfig": "Creates a configuration for DNS query logging. After you create a query logging configuration, Amazon Route 53 begins to publish log data to an Amazon CloudWatch Logs log group.\n\nDNS query logs contain information about the queries that Route 53 receives for a specified public hosted zone, such as the following:\n\n- Route 53 edge location that responded to the DNS query\n- Domain or subdomain that was requested\n- DNS record type, such as A or AAAA\n- DNS response code, such as `NoError` or `ServFail`\n\n- **Log Group and Resource Policy** - Before you create a query logging configuration, perform the following operations.\n\n> If you create a query logging configuration using the Route 53 console, Route 53 performs these operations automatically. \n\n- Create a CloudWatch Logs log group, and make note of the ARN, which you specify when you create a query logging configuration. Note the following:\n\n- You must create the log group in the us-east-1 region.\n- You must use the same AWS account to create the log group and the hosted zone that you want to configure query logging for.\n- When you create log groups for query logging, we recommend that you use a consistent prefix, for example:\n\n`/aws/route53/ *hosted zone name*`\n\nIn the next step, you'll create a resource policy, which controls access to one or more log groups and the associated AWS resources, such as Route 53 hosted zones. There's a limit on the number of resource policies that you can create, so we recommend that you use a consistent prefix so you can use the same resource policy for all the log groups that you create for query logging.\n- Create a CloudWatch Logs resource policy, and give it the permissions that Route 53 needs to create log streams and to send query logs to log streams. For the value of `Resource` , specify the ARN for the log group that you created in the previous step. To use the same resource policy for all the CloudWatch Logs log groups that you created for query logging configurations, replace the hosted zone name with `*` , for example:\n\n`arn:aws:logs:us-east-1:123412341234:log-group:/aws/route53/*`\n\nTo avoid the confused deputy problem, a security issue where an entity without a permission for an action can coerce a more-privileged entity to perform it, you can optionally limit the permissions that a service has to a resource in a resource-based policy by supplying the following values:\n\n- For `aws:SourceArn` , supply the hosted zone ARN used in creating the query logging configuration. For example, `aws:SourceArn: arn:aws:route53:::hostedzone/hosted zone ID` .\n- For `aws:SourceAccount` , supply the account ID for the account that creates the query logging configuration. For example, `aws:SourceAccount:111111111111` .\n\nFor more information, see [The confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html) in the *AWS IAM User Guide* .\n\n> You can't use the CloudWatch console to create or edit a resource policy. You must use the CloudWatch API, one of the AWS SDKs, or the AWS CLI .\n- **Log Streams and Edge Locations** - When Route 53 finishes creating the configuration for DNS query logging, it does the following:\n\n- Creates a log stream for an edge location the first time that the edge location responds to DNS queries for the specified hosted zone. That log stream is used to log all queries that Route 53 responds to for that edge location.\n- Begins to send query logs to the applicable log stream.\n\nThe name of each log stream is in the following format:\n\n`*hosted zone ID* / *edge location code*`\n\nThe edge location code is a three-letter code and an arbitrarily assigned number, for example, DFW3. The three-letter code typically corresponds with the International Air Transport Association airport code for an airport near the edge location. (These abbreviations might change in the future.) For a list of edge locations, see \"The Route 53 Global Network\" on the [Route 53 Product Details](https://docs.aws.amazon.com/route53/details/) page.\n- **Queries That Are Logged** - Query logs contain only the queries that DNS resolvers forward to Route 53. If a DNS resolver has already cached the response to a query (such as the IP address for a load balancer for example.com), the resolver will continue to return the cached response. It doesn't forward another query to Route 53 until the TTL for the corresponding resource record set expires. Depending on how many DNS queries are submitted for a resource record set, and depending on the TTL for that resource record set, query logs might contain information about only one query out of every several thousand queries that are submitted to DNS. For more information about how DNS works, see [Routing Internet Traffic to Your Website or Web Application](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/welcome-dns-service.html) in the *Amazon Route 53 Developer Guide* .\n- **Log File Format** - For a list of the values in each query log and the format of each value, see [Logging DNS Queries](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/query-logs.html) in the *Amazon Route 53 Developer Guide* .\n- **Pricing** - For information about charges for query logs, see [Amazon CloudWatch Pricing](https://docs.aws.amazon.com/cloudwatch/pricing/) .\n- **How to Stop Logging** - If you want Route 53 to stop sending query logs to CloudWatch Logs, delete the query logging configuration. For more information, see [DeleteQueryLoggingConfig](https://docs.aws.amazon.com/Route53/latest/APIReference/API_DeleteQueryLoggingConfig.html) .", "VPCs": "*Private hosted zones:* A complex type that contains information about the VPCs that are associated with the specified hosted zone.\n\n> For public hosted zones, omit `VPCs` , `VPCId` , and `VPCRegion` ." } }, @@ -32975,7 +33787,6 @@ "description": "Information about one record that you want to create.", "properties": { "AliasTarget": "*Alias resource record sets only:* Information about the AWS resource, such as a CloudFront distribution or an Amazon S3 bucket, that you want to route traffic to.\n\nIf you're creating resource records sets for a private hosted zone, note the following:\n\n- You can't create an alias resource record set in a private hosted zone to route traffic to a CloudFront distribution.\n- Creating geolocation alias resource record sets or latency alias resource record sets in a private hosted zone is unsupported.\n- For information about creating failover resource record sets in a private hosted zone, see [Configuring Failover in a Private Hosted Zone](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-failover-private-hosted-zones.html) in the *Amazon Route 53 Developer Guide* .", - "Comment": "*Optional:* Any comments you want to include about a change batch request.", "Failover": "*Failover resource record sets only:* To configure failover, you add the `Failover` element to two resource record sets. For one resource record set, you specify `PRIMARY` as the value for `Failover` ; for the other resource record set, you specify `SECONDARY` . In addition, you include the `HealthCheckId` element and specify the health check that you want Amazon Route 53 to perform for each resource record set.\n\nExcept where noted, the following failover behaviors assume that you have included the `HealthCheckId` element in both resource record sets:\n\n- When the primary resource record set is healthy, Route 53 responds to DNS queries with the applicable value from the primary resource record set regardless of the health of the secondary resource record set.\n- When the primary resource record set is unhealthy and the secondary resource record set is healthy, Route 53 responds to DNS queries with the applicable value from the secondary resource record set.\n- When the secondary resource record set is unhealthy, Route 53 responds to DNS queries with the applicable value from the primary resource record set regardless of the health of the primary resource record set.\n- If you omit the `HealthCheckId` element for the secondary resource record set, and if the primary resource record set is unhealthy, Route 53 always responds to DNS queries with the applicable value from the secondary resource record set. This is true regardless of the health of the associated endpoint.\n\nYou can't create non-failover resource record sets that have the same values for the `Name` and `Type` elements as failover resource record sets.\n\nFor failover alias resource record sets, you must also include the `EvaluateTargetHealth` element and set the value to true.\n\nFor more information about configuring failover for Route 53, see the following topics in the *Amazon Route 53 Developer Guide* :\n\n- [Route 53 Health Checks and DNS Failover](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-failover.html)\n- [Configuring Failover in a Private Hosted Zone](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-failover-private-hosted-zones.html)", "GeoLocation": "*Geolocation resource record sets only:* A complex type that lets you control how Amazon Route 53 responds to DNS queries based on the geographic origin of the query. For example, if you want all queries from Africa to be routed to a web server with an IP address of `192.0.2.111` , create a resource record set with a `Type` of `A` and a `ContinentCode` of `AF` .\n\n> Although creating geolocation and geolocation alias resource record sets in a private hosted zone is allowed, it's not supported. \n\nIf you create separate resource record sets for overlapping geographic regions (for example, one resource record set for a continent and one for a country on the same continent), priority goes to the smallest geographic region. This allows you to route most queries for a continent to one resource and to route queries for a country on that continent to a different resource.\n\nYou can't create two geolocation resource record sets that specify the same geographic location.\n\nThe value `*` in the `CountryCode` element matches all geographic locations that aren't specified in other geolocation resource record sets that have the same values for the `Name` and `Type` elements.\n\n> Geolocation works by mapping IP addresses to locations. However, some IP addresses aren't mapped to geographic locations, so even if you create geolocation resource record sets that cover all seven continents, Route 53 will receive some DNS queries from locations that it can't identify. We recommend that you create a resource record set for which the value of `CountryCode` is `*` . Two groups of queries are routed to the resource that you specify in this record: queries that come from locations for which you haven't created geolocation resource record sets and queries from IP addresses that aren't mapped to a location. If you don't create a `*` resource record set, Route 53 returns a \"no answer\" response for queries from those locations. \n\nYou can't create non-geolocation resource record sets that have the same values for the `Name` and `Type` elements as geolocation resource record sets.", "HealthCheckId": "If you want Amazon Route 53 to return this resource record set in response to a DNS query only when the status of a health check is healthy, include the `HealthCheckId` element and specify the ID of the applicable health check.\n\nRoute 53 determines whether a resource record set is healthy based on one of the following:\n\n- By periodically sending a request to the endpoint that is specified in the health check\n- By aggregating the status of a specified group of health checks (calculated health checks)\n- By determining the current state of a CloudWatch alarm (CloudWatch metric health checks)\n\n> Route 53 doesn't check the health of the endpoint that is specified in the resource record set, for example, the endpoint specified by the IP address in the `Value` element. When you add a `HealthCheckId` element to a resource record set, Route 53 checks the health of the endpoint that you specified in the health check. \n\nFor more information, see the following topics in the *Amazon Route 53 Developer Guide* :\n\n- [How Amazon Route 53 Determines Whether an Endpoint Is Healthy](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-failover-determining-health-of-endpoints.html)\n- [Route 53 Health Checks and DNS Failover](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-failover.html)\n- [Configuring Failover in a Private Hosted Zone](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-failover-private-hosted-zones.html)\n\n*When to Specify HealthCheckId*\n\nSpecifying a value for `HealthCheckId` is useful only when Route 53 is choosing between two or more resource record sets to respond to a DNS query, and you want Route 53 to base the choice in part on the status of a health check. Configuring health checks makes sense only in the following configurations:\n\n- *Non-alias resource record sets* : You're checking the health of a group of non-alias resource record sets that have the same routing policy, name, and type (such as multiple weighted records named www.example.com with a type of A) and you specify health check IDs for all the resource record sets.\n\nIf the health check status for a resource record set is healthy, Route 53 includes the record among the records that it responds to DNS queries with.\n\nIf the health check status for a resource record set is unhealthy, Route 53 stops responding to DNS queries using the value for that resource record set.\n\nIf the health check status for all resource record sets in the group is unhealthy, Route 53 considers all resource record sets in the group healthy and responds to DNS queries accordingly.\n- *Alias resource record sets* : You specify the following settings:\n\n- You set `EvaluateTargetHealth` to true for an alias resource record set in a group of resource record sets that have the same routing policy, name, and type (such as multiple weighted records named www.example.com with a type of A).\n- You configure the alias resource record set to route traffic to a non-alias resource record set in the same hosted zone.\n- You specify a health check ID for the non-alias resource record set.\n\nIf the health check status is healthy, Route 53 considers the alias resource record set to be healthy and includes the alias record among the records that it responds to DNS queries with.\n\nIf the health check status is unhealthy, Route 53 stops responding to DNS queries using the alias resource record set.\n\n> The alias resource record set can also route traffic to a *group* of non-alias resource record sets that have the same routing policy, name, and type. In that configuration, associate health checks with all of the resource record sets in the group of non-alias resource record sets.\n\n*Geolocation Routing*\n\nFor geolocation resource record sets, if an endpoint is unhealthy, Route 53 looks for a resource record set for the larger, associated geographic region. For example, suppose you have resource record sets for a state in the United States, for the entire United States, for North America, and a resource record set that has `*` for `CountryCode` is `*` , which applies to all locations. If the endpoint for the state resource record set is unhealthy, Route 53 checks for healthy resource record sets in the following order until it finds a resource record set for which the endpoint is healthy:\n\n- The United States\n- North America\n- The default resource record set\n\n*Specifying the Health Check Endpoint by Domain Name*\n\nIf your health checks specify the endpoint only by domain name, we recommend that you create a separate health check for each endpoint. For example, create a health check for each `HTTP` server that is serving content for `www.example.com` . For the value of `FullyQualifiedDomainName` , specify the domain name of the server (such as `us-east-2-www.example.com` ), not the name of the resource record sets ( `www.example.com` ).\n\n> Health check results will be unpredictable if you do the following:\n> \n> - Create a health check that has the same value for `FullyQualifiedDomainName` as the name of a resource record set.\n> - Associate that health check with the resource record set.", @@ -33044,7 +33855,7 @@ "attributes": { "Ref": "`Ref` returns the `SafetyRuleArn` object.", "SafetyRuleArn": "The Amazon Resource Name (ARN) of the safety rule.", - "Status": "" + "Status": "The deployment status of the safety rule. Status can be one of the following: PENDING, DEPLOYED, PENDING_DELETION." }, "description": "List the safety rules (the assertion rules and gating rules) that you've defined for the routing controls in a control panel.", "properties": { @@ -33164,7 +33975,7 @@ "properties": { "ComponentId": "The component identifier of the resource, generated when DNS target resource is used.", "DnsTargetResource": "A component for DNS/routing control readiness checks. This is a required setting when `ResourceSet` `ResourceSetType` is set to `AWS::Route53RecoveryReadiness::DNSTargetResource` . Do not set it for any other `ResourceSetType` setting.", - "ReadinessScopes": "A list of recovery group Amazon Resource Names (ARNs) and cell ARNs that this resource is contained within.", + "ReadinessScopes": "The recovery group Amazon Resource Name (ARN) or the cell ARN that the readiness checks for this resource set are scoped to.", "ResourceArn": "The Amazon Resource Name (ARN) of the AWS resource. This is a required setting for all `ResourceSet` `ResourceSetType` settings except `AWS::Route53RecoveryReadiness::DNSTargetResource` . Do not set this when `ResourceSetType` is set to `AWS::Route53RecoveryReadiness::DNSTargetResource` ." } }, @@ -33248,7 +34059,7 @@ "FirewallRuleGroupId": "The unique identifier of the firewall rule group.", "MutationProtection": "If enabled, this setting disallows modification or removal of the association, to help prevent against accidentally altering DNS firewall protections.", "Name": "The name of the association.", - "Priority": "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.\n\nYou must specify a unique priority for each rule group that you associate with a single VPC. To make it easier to insert rule groups later, leave space between the numbers, for example, use 101, 200, and so on. You can change the priority setting for a rule group association after you create it.\n\nThe allowed values for `Priority` are between 100 and 9900.", + "Priority": "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.\n\nYou must specify a unique priority for each rule group that you associate with a single VPC. To make it easier to insert rule groups later, leave space between the numbers, for example, use 101, 200, and so on. You can change the priority setting for a rule group association after you create it.\n\nThe allowed values for `Priority` are between 100 and 9900 (excluding 100 and 9900).", "Tags": "A list of the tag keys and values that you want to associate with the rule group.", "VpcId": "The unique identifier of the VPC that is associated with the rule group." } @@ -33549,7 +34360,7 @@ }, "AWS::S3::Bucket.EventBridgeConfiguration": { "attributes": {}, - "description": "Amazon S3 can send events to Amazon EventBridge whenever certain events happen in your bucket, see [Using EventBridge](https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventBridge.html) in the *Amazon S3 User Guide* .\n\nUnlike other destinations, delivery of events to EventBridge can be either enabled or disabled for a bucket. If enabled, all events will be sent to EventBridge and you can use EventBridge rules and filters to route events to additional targets. For more information, see [What Is Amazon EventBridge](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-what-is.html) in the *Amazon EventBridge User Guide*", + "description": "Amazon S3 can send events to Amazon EventBridge whenever certain events happen in your bucket, see [Using EventBridge](https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventBridge.html) in the *Amazon S3 User Guide* .\n\nUnlike other destinations, delivery of events to EventBridge can be either enabled or disabled for a bucket. If enabled, all events will be sent to EventBridge and you can use EventBridge rules to route events to additional targets. For more information, see [What Is Amazon EventBridge](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-what-is.html) in the *Amazon EventBridge User Guide*", "properties": { "EventBridgeEnabled": "Enables delivery of events to Amazon EventBridge." } @@ -33821,24 +34632,24 @@ }, "AWS::S3::Bucket.Rule": { "attributes": {}, - "description": "Specifies lifecycle rules for an Amazon S3 bucket. For more information, see [Put Bucket Lifecycle Configuration](https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTlifecycle.html) in the *Amazon S3 API Reference* .", + "description": "Specifies lifecycle rules for an Amazon S3 bucket. For more information, see [Put Bucket Lifecycle Configuration](https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTlifecycle.html) in the *Amazon S3 API Reference* .\n\nYou must specify at least one of the following properties: `AbortIncompleteMultipartUpload` , `ExpirationDate` , `ExpirationInDays` , `NoncurrentVersionExpirationInDays` , `NoncurrentVersionTransition` , `NoncurrentVersionTransitions` , `Transition` , or `Transitions` .", "properties": { - "AbortIncompleteMultipartUpload": "Specifies a lifecycle rule that stops incomplete multipart uploads to an Amazon S3 bucket.\n\nYou must specify at least one of the following properties: `AbortIncompleteMultipartUpload` , `ExpirationDate` , `ExpirationInDays` , `NoncurrentVersionExpirationInDays` , `NoncurrentVersionTransition` , `NoncurrentVersionTransitions` , `Transition` , or `Transitions` .", - "ExpirationDate": "Indicates when objects are deleted from Amazon S3 and Amazon S3 Glacier. The date value must be in ISO 8601 format. The time is always midnight UTC. If you specify an expiration and transition time, you must use the same time unit for both properties (either in days or by date). The expiration time must also be later than the transition time.\n\nYou must specify at least one of the following properties: `AbortIncompleteMultipartUpload` , `ExpirationDate` , `ExpirationInDays` , `NoncurrentVersionExpirationInDays` , `NoncurrentVersionTransition` , `NoncurrentVersionTransitions` , `Transition` , or `Transitions` .", - "ExpirationInDays": "Indicates the number of days after creation when objects are deleted from Amazon S3 and Amazon S3 Glacier. If you specify an expiration and transition time, you must use the same time unit for both properties (either in days or by date). The expiration time must also be later than the transition time.\n\nYou must specify at least one of the following properties: `AbortIncompleteMultipartUpload` , `ExpirationDate` , `ExpirationInDays` , `NoncurrentVersionExpirationInDays` , `NoncurrentVersionTransition` , `NoncurrentVersionTransitions` , `Transition` , or `Transitions` .", + "AbortIncompleteMultipartUpload": "Specifies a lifecycle rule that stops incomplete multipart uploads to an Amazon S3 bucket.", + "ExpirationDate": "Indicates when objects are deleted from Amazon S3 and Amazon S3 Glacier. The date value must be in ISO 8601 format. The time is always midnight UTC. If you specify an expiration and transition time, you must use the same time unit for both properties (either in days or by date). The expiration time must also be later than the transition time.", + "ExpirationInDays": "Indicates the number of days after creation when objects are deleted from Amazon S3 and Amazon S3 Glacier. If you specify an expiration and transition time, you must use the same time unit for both properties (either in days or by date). The expiration time must also be later than the transition time.", "ExpiredObjectDeleteMarker": "Indicates whether Amazon S3 will remove a delete marker without any noncurrent versions. If set to true, the delete marker will be removed if there are no noncurrent versions. This cannot be specified with `ExpirationInDays` , `ExpirationDate` , or `TagFilters` .", "Id": "Unique identifier for the rule. The value can't be longer than 255 characters.", "NoncurrentVersionExpiration": "Specifies when noncurrent object versions expire. Upon expiration, Amazon S3 permanently deletes the noncurrent object versions. You set this lifecycle configuration action on a bucket that has versioning enabled (or suspended) to request that Amazon S3 delete noncurrent object versions at a specific period in the object's lifetime.", - "NoncurrentVersionExpirationInDays": "(Deprecated.) For buckets with versioning enabled (or suspended), specifies the time, in days, between when a new version of the object is uploaded to the bucket and when old versions of the object expire. When object versions expire, Amazon S3 permanently deletes them. If you specify a transition and expiration time, the expiration time must be later than the transition time.\n\nYou must specify at least one of the following properties: `AbortIncompleteMultipartUpload` , `ExpirationDate` , `ExpirationInDays` , `NoncurrentVersionExpirationInDays` , `NoncurrentVersionTransition` , `NoncurrentVersionTransitions` , `Transition` , or `Transitions` .", - "NoncurrentVersionTransition": "(Deprecated.) For buckets with versioning enabled (or suspended), specifies when non-current objects transition to a specified storage class. If you specify a transition and expiration time, the expiration time must be later than the transition time. If you specify this property, don't specify the `NoncurrentVersionTransitions` property.\n\nYou must specify at least one of the following properties: `AbortIncompleteMultipartUpload` , `ExpirationDate` , `ExpirationInDays` , `NoncurrentVersionExpirationInDays` , `NoncurrentVersionTransition` , `NoncurrentVersionTransitions` , `Transition` , or `Transitions` .", - "NoncurrentVersionTransitions": "For buckets with versioning enabled (or suspended), one or more transition rules that specify when non-current objects transition to a specified storage class. If you specify a transition and expiration time, the expiration time must be later than the transition time. If you specify this property, don't specify the `NoncurrentVersionTransition` property.\n\nYou must specify at least one of the following properties: `AbortIncompleteMultipartUpload` , `ExpirationDate` , `ExpirationInDays` , `NoncurrentVersionExpirationInDays` , `NoncurrentVersionTransition` , `NoncurrentVersionTransitions` , `Transition` , or `Transitions` .", + "NoncurrentVersionExpirationInDays": "(Deprecated.) For buckets with versioning enabled (or suspended), specifies the time, in days, between when a new version of the object is uploaded to the bucket and when old versions of the object expire. When object versions expire, Amazon S3 permanently deletes them. If you specify a transition and expiration time, the expiration time must be later than the transition time.", + "NoncurrentVersionTransition": "(Deprecated.) For buckets with versioning enabled (or suspended), specifies when non-current objects transition to a specified storage class. If you specify a transition and expiration time, the expiration time must be later than the transition time. If you specify this property, don't specify the `NoncurrentVersionTransitions` property.", + "NoncurrentVersionTransitions": "For buckets with versioning enabled (or suspended), one or more transition rules that specify when non-current objects transition to a specified storage class. If you specify a transition and expiration time, the expiration time must be later than the transition time. If you specify this property, don't specify the `NoncurrentVersionTransition` property.", "ObjectSizeGreaterThan": "Specifies the minimum object size in bytes for this rule to apply to. For more information about size based rules, see [Lifecycle configuration using size-based rules](https://docs.aws.amazon.com/AmazonS3/latest/userguide/lifecycle-configuration-examples.html#lc-size-rules) in the *Amazon S3 User Guide* .", "ObjectSizeLessThan": "Specifies the maximum object size in bytes for this rule to apply to. For more information about sized based rules, see [Lifecycle configuration using size-based rules](https://docs.aws.amazon.com/AmazonS3/latest/userguide/lifecycle-configuration-examples.html#lc-size-rules) in the *Amazon S3 User Guide* .", "Prefix": "Object key prefix that identifies one or more objects to which this rule applies.\n\n> Replacement must be made for object keys containing special characters (such as carriage returns) when using XML requests. For more information, see [XML related object key constraints](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html#object-key-xml-related-constraints) .", "Status": "If `Enabled` , the rule is currently being applied. If `Disabled` , the rule is not currently being applied.", "TagFilters": "Tags to use to identify a subset of objects to which the lifecycle rule applies.", - "Transition": "(Deprecated.) Specifies when an object transitions to a specified storage class. If you specify an expiration and transition time, you must use the same time unit for both properties (either in days or by date). The expiration time must also be later than the transition time. If you specify this property, don't specify the `Transitions` property.\n\nYou must specify at least one of the following properties: `AbortIncompleteMultipartUpload` , `ExpirationDate` , `ExpirationInDays` , `NoncurrentVersionExpirationInDays` , `NoncurrentVersionTransition` , `NoncurrentVersionTransitions` , `Transition` , or `Transitions` .", - "Transitions": "One or more transition rules that specify when an object transitions to a specified storage class. If you specify an expiration and transition time, you must use the same time unit for both properties (either in days or by date). The expiration time must also be later than the transition time. If you specify this property, don't specify the `Transition` property.\n\nYou must specify at least one of the following properties: `AbortIncompleteMultipartUpload` , `ExpirationDate` , `ExpirationInDays` , `NoncurrentVersionExpirationInDays` , `NoncurrentVersionTransition` , `NoncurrentVersionTransitions` , `Transition` , or `Transitions` ." + "Transition": "(Deprecated.) Specifies when an object transitions to a specified storage class. If you specify an expiration and transition time, you must use the same time unit for both properties (either in days or by date). The expiration time must also be later than the transition time. If you specify this property, don't specify the `Transitions` property.", + "Transitions": "One or more transition rules that specify when an object transitions to a specified storage class. If you specify an expiration and transition time, you must use the same time unit for both properties (either in days or by date). The expiration time must also be later than the transition time. If you specify this property, don't specify the `Transition` property." } }, "AWS::S3::Bucket.S3KeyFilter": { @@ -34252,7 +35063,7 @@ "attributes": { "Ref": "`Ref` returns the resource name." }, - "description": "The name of the configuration set.\n\nConfiguration sets let you create groups of rules that you can apply to the emails you send using Amazon SES. For more information about using configuration sets, see [Using Amazon SES Configuration Sets](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/using-configuration-sets.html) in the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/) .", + "description": "The name of the configuration set.\n\nConfiguration sets let you create groups of rules that you can apply to the emails you send using Amazon SES. For more information about using configuration sets, see [Using Amazon SES Configuration Sets](https://docs.aws.amazon.com/ses/latest/dg/using-configuration-sets.html) in the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/) .", "properties": { "Name": "" } @@ -34267,14 +35078,14 @@ }, "AWS::SES::ConfigurationSetEventDestination.CloudWatchDestination": { "attributes": {}, - "description": "Contains information associated with an Amazon CloudWatch event destination to which email sending events are published.\n\nEvent destinations, such as Amazon CloudWatch, are associated with configuration sets, which enable you to publish email sending events. For information about using configuration sets, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/monitor-sending-activity.html) .", + "description": "Contains information associated with an Amazon CloudWatch event destination to which email sending events are published.\n\nEvent destinations, such as Amazon CloudWatch, are associated with configuration sets, which enable you to publish email sending events. For information about using configuration sets, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/monitor-sending-activity.html) .", "properties": { "DimensionConfigurations": "A list of dimensions upon which to categorize your emails when you publish email sending events to Amazon CloudWatch." } }, "AWS::SES::ConfigurationSetEventDestination.DimensionConfiguration": { "attributes": {}, - "description": "Contains the dimension configuration to use when you publish email sending events to Amazon CloudWatch.\n\nFor information about publishing email sending events to Amazon CloudWatch, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/monitor-sending-activity.html) .", + "description": "Contains the dimension configuration to use when you publish email sending events to Amazon CloudWatch.\n\nFor information about publishing email sending events to Amazon CloudWatch, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/monitor-sending-activity.html) .", "properties": { "DefaultDimensionValue": "The default value of the dimension that is published to Amazon CloudWatch if you do not provide the value of the dimension when you send an email. The default value must meet the following requirements:\n\n- Contain only ASCII letters (a-z, A-Z), numbers (0-9), underscores (_), dashes (-), at signs (@), or periods (.).\n- Contain 256 characters or fewer.", "DimensionName": "The name of an Amazon CloudWatch dimension associated with an email sending metric. The name must meet the following requirements:\n\n- Contain only ASCII letters (a-z, A-Z), numbers (0-9), underscores (_), or dashes (-).\n- Contain 256 characters or fewer.", @@ -34283,7 +35094,7 @@ }, "AWS::SES::ConfigurationSetEventDestination.EventDestination": { "attributes": {}, - "description": "Contains information about an event destination.\n\n> When you create or update an event destination, you must provide one, and only one, destination. The destination can be Amazon CloudWatch, Amazon Kinesis Firehose or Amazon Simple Notification Service (Amazon SNS). \n\nEvent destinations are associated with configuration sets, which enable you to publish email sending events to Amazon CloudWatch, Amazon Kinesis Firehose, or Amazon Simple Notification Service (Amazon SNS). For information about using configuration sets, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/monitor-sending-activity.html) .", + "description": "Contains information about an event destination.\n\n> When you create or update an event destination, you must provide one, and only one, destination. The destination can be Amazon CloudWatch, Amazon Kinesis Firehose or Amazon Simple Notification Service (Amazon SNS). \n\nEvent destinations are associated with configuration sets, which enable you to publish email sending events to Amazon CloudWatch, Amazon Kinesis Firehose, or Amazon Simple Notification Service (Amazon SNS). For information about using configuration sets, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/monitor-sending-activity.html) .", "properties": { "CloudWatchDestination": "An object that contains the names, default values, and sources of the dimensions associated with an Amazon CloudWatch event destination.", "Enabled": "Sets whether Amazon SES publishes events to this destination when you send an email with the associated configuration set. Set to `true` to enable publishing to this destination; set to `false` to prevent publishing to this destination. The default value is `false` .", @@ -34294,7 +35105,7 @@ }, "AWS::SES::ConfigurationSetEventDestination.KinesisFirehoseDestination": { "attributes": {}, - "description": "Contains the delivery stream ARN and the IAM role ARN associated with an Amazon Kinesis Firehose event destination.\n\nEvent destinations, such as Amazon Kinesis Firehose, are associated with configuration sets, which enable you to publish email sending events. For information about using configuration sets, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/monitor-sending-activity.html) .", + "description": "Contains the delivery stream ARN and the IAM role ARN associated with an Amazon Kinesis Firehose event destination.\n\nEvent destinations, such as Amazon Kinesis Firehose, are associated with configuration sets, which enable you to publish email sending events. For information about using configuration sets, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/monitor-sending-activity.html) .", "properties": { "DeliveryStreamARN": "The ARN of the Amazon Kinesis Firehose stream that email sending events should be published to.", "IAMRoleARN": "The ARN of the IAM role under which Amazon SES publishes email sending events to the Amazon Kinesis Firehose stream." @@ -34339,7 +35150,7 @@ }, "AWS::SES::ReceiptFilter.IpFilter": { "attributes": {}, - "description": "A receipt IP address filter enables you to specify whether to accept or reject mail originating from an IP address or range of IP addresses.\n\nFor information about setting up IP address filters, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-ip-filters.html) .", + "description": "A receipt IP address filter enables you to specify whether to accept or reject mail originating from an IP address or range of IP addresses.\n\nFor information about setting up IP address filters, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/receiving-email-ip-filtering-console-walkthrough.html) .", "properties": { "Cidr": "A single IP address or a range of IP addresses to block or allow, specified in Classless Inter-Domain Routing (CIDR) notation. An example of a single email address is 10.0.0.1. An example of a range of IP addresses is 10.0.0.1/24. For more information about CIDR notation, see [RFC 2317](https://docs.aws.amazon.com/https://tools.ietf.org/html/rfc2317) .", "Policy": "Indicates whether to block or allow incoming mail from the specified IP addresses." @@ -34358,7 +35169,7 @@ }, "AWS::SES::ReceiptRule.Action": { "attributes": {}, - "description": "An action that Amazon SES can take when it receives an email on behalf of one or more email addresses or domains that you own. An instance of this data type can represent only one action.\n\nFor information about setting up receipt rules, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-receipt-rules.html) .", + "description": "An action that Amazon SES can take when it receives an email on behalf of one or more email addresses or domains that you own. An instance of this data type can represent only one action.\n\nFor information about setting up receipt rules, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/receiving-email-receipt-rules-console-walkthrough.html) .", "properties": { "AddHeaderAction": "Adds a header to the received email.", "BounceAction": "Rejects the received email by returning a bounce response to the sender and, optionally, publishes a notification to Amazon Simple Notification Service (Amazon SNS).", @@ -34371,7 +35182,7 @@ }, "AWS::SES::ReceiptRule.AddHeaderAction": { "attributes": {}, - "description": "When included in a receipt rule, this action adds a header to the received email.\n\nFor information about adding a header using a receipt rule, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-add-header.html) .", + "description": "When included in a receipt rule, this action adds a header to the received email.\n\nFor information about adding a header using a receipt rule, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/receiving-email-action-add-header.html) .", "properties": { "HeaderName": "The name of the header to add to the incoming message. The name must contain at least one character, and can contain up to 50 characters. It consists of alphanumeric (a\u2013z, A\u2013Z, 0\u20139) characters and dashes.", "HeaderValue": "The content to include in the header. This value can contain up to 2048 characters. It can't contain newline ( `\\n` ) or carriage return ( `\\r` ) characters." @@ -34379,7 +35190,7 @@ }, "AWS::SES::ReceiptRule.BounceAction": { "attributes": {}, - "description": "When included in a receipt rule, this action rejects the received email by returning a bounce response to the sender and, optionally, publishes a notification to Amazon Simple Notification Service (Amazon SNS).\n\nFor information about sending a bounce message in response to a received email, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-bounce.html) .", + "description": "When included in a receipt rule, this action rejects the received email by returning a bounce response to the sender and, optionally, publishes a notification to Amazon Simple Notification Service (Amazon SNS).\n\nFor information about sending a bounce message in response to a received email, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/receiving-email-action-bounce.html) .", "properties": { "Message": "Human-readable text to include in the bounce message.", "Sender": "The email address of the sender of the bounced email. This is the address from which the bounce message is sent.", @@ -34390,7 +35201,7 @@ }, "AWS::SES::ReceiptRule.LambdaAction": { "attributes": {}, - "description": "When included in a receipt rule, this action calls an AWS Lambda function and, optionally, publishes a notification to Amazon Simple Notification Service (Amazon SNS).\n\nTo enable Amazon SES to call your AWS Lambda function or to publish to an Amazon SNS topic of another account, Amazon SES must have permission to access those resources. For information about giving permissions, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-permissions.html) .\n\nFor information about using AWS Lambda actions in receipt rules, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-lambda.html) .", + "description": "When included in a receipt rule, this action calls an AWS Lambda function and, optionally, publishes a notification to Amazon Simple Notification Service (Amazon SNS).\n\nTo enable Amazon SES to call your AWS Lambda function or to publish to an Amazon SNS topic of another account, Amazon SES must have permission to access those resources. For information about giving permissions, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/receiving-email-permissions.html) .\n\nFor information about using AWS Lambda actions in receipt rules, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/receiving-email-action-lambda.html) .", "properties": { "FunctionArn": "The Amazon Resource Name (ARN) of the AWS Lambda function. An example of an AWS Lambda function ARN is `arn:aws:lambda:us-west-2:account-id:function:MyFunction` . For more information about AWS Lambda, see the [AWS Lambda Developer Guide](https://docs.aws.amazon.com/lambda/latest/dg/welcome.html) .", "InvocationType": "The invocation type of the AWS Lambda function. An invocation type of `RequestResponse` means that the execution of the function immediately results in a response, and a value of `Event` means that the function is invoked asynchronously. The default value is `Event` . For information about AWS Lambda invocation types, see the [AWS Lambda Developer Guide](https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html) .\n\n> There is a 30-second timeout on `RequestResponse` invocations. You should use `Event` invocation in most cases. Use `RequestResponse` only to make a mail flow decision, such as whether to stop the receipt rule or the receipt rule set.", @@ -34399,7 +35210,7 @@ }, "AWS::SES::ReceiptRule.Rule": { "attributes": {}, - "description": "Receipt rules enable you to specify which actions Amazon SES should take when it receives mail on behalf of one or more email addresses or domains that you own.\n\nEach receipt rule defines a set of email addresses or domains that it applies to. If the email addresses or domains match at least one recipient address of the message, Amazon SES executes all of the receipt rule's actions on the message.\n\nFor information about setting up receipt rules, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-receipt-rules.html) .", + "description": "Receipt rules enable you to specify which actions Amazon SES should take when it receives mail on behalf of one or more email addresses or domains that you own.\n\nEach receipt rule defines a set of email addresses or domains that it applies to. If the email addresses or domains match at least one recipient address of the message, Amazon SES executes all of the receipt rule's actions on the message.\n\nFor information about setting up receipt rules, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/receiving-email-receipt-rules-console-walkthrough.html) .", "properties": { "Actions": "An ordered list of actions to perform on messages that match at least one of the recipient email addresses or domains specified in the receipt rule.", "Enabled": "If `true` , the receipt rule is active. The default value is `false` .", @@ -34411,17 +35222,17 @@ }, "AWS::SES::ReceiptRule.S3Action": { "attributes": {}, - "description": "When included in a receipt rule, this action saves the received message to an Amazon Simple Storage Service (Amazon S3) bucket and, optionally, publishes a notification to Amazon Simple Notification Service (Amazon SNS).\n\nTo enable Amazon SES to write emails to your Amazon S3 bucket, use an AWS KMS key to encrypt your emails, or publish to an Amazon SNS topic of another account, Amazon SES must have permission to access those resources. For information about granting permissions, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-permissions.html) .\n\n> When you save your emails to an Amazon S3 bucket, the maximum email size (including headers) is 30 MB. Emails larger than that bounces. \n\nFor information about specifying Amazon S3 actions in receipt rules, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-s3.html) .", + "description": "When included in a receipt rule, this action saves the received message to an Amazon Simple Storage Service (Amazon S3) bucket and, optionally, publishes a notification to Amazon Simple Notification Service (Amazon SNS).\n\nTo enable Amazon SES to write emails to your Amazon S3 bucket, use an AWS KMS key to encrypt your emails, or publish to an Amazon SNS topic of another account, Amazon SES must have permission to access those resources. For information about granting permissions, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/receiving-email-permissions.html) .\n\n> When you save your emails to an Amazon S3 bucket, the maximum email size (including headers) is 30 MB. Emails larger than that bounces. \n\nFor information about specifying Amazon S3 actions in receipt rules, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/receiving-email-action-s3.html) .", "properties": { "BucketName": "The name of the Amazon S3 bucket for incoming email.", - "KmsKeyArn": "The customer master key that Amazon SES should use to encrypt your emails before saving them to the Amazon S3 bucket. You can use the default master key or a custom master key that you created in AWS KMS as follows:\n\n- To use the default master key, provide an ARN in the form of `arn:aws:kms:REGION:ACCOUNT-ID-WITHOUT-HYPHENS:alias/aws/ses` . For example, if your AWS account ID is 123456789012 and you want to use the default master key in the US West (Oregon) Region, the ARN of the default master key would be `arn:aws:kms:us-west-2:123456789012:alias/aws/ses` . If you use the default master key, you don't need to perform any extra steps to give Amazon SES permission to use the key.\n- To use a custom master key that you created in AWS KMS, provide the ARN of the master key and ensure that you add a statement to your key's policy to give Amazon SES permission to use it. For more information about giving permissions, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-permissions.html) .\n\nFor more information about key policies, see the [AWS KMS Developer Guide](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html) . If you do not specify a master key, Amazon SES does not encrypt your emails.\n\n> Your mail is encrypted by Amazon SES using the Amazon S3 encryption client before the mail is submitted to Amazon S3 for storage. It is not encrypted using Amazon S3 server-side encryption. This means that you must use the Amazon S3 encryption client to decrypt the email after retrieving it from Amazon S3, as the service has no access to use your AWS KMS keys for decryption. This encryption client is currently available with the [AWS SDK for Java](https://docs.aws.amazon.com/sdk-for-java/) and [AWS SDK for Ruby](https://docs.aws.amazon.com/sdk-for-ruby/) only. For more information about client-side encryption using AWS KMS master keys, see the [Amazon S3 Developer Guide](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingClientSideEncryption.html) .", + "KmsKeyArn": "The customer master key that Amazon SES should use to encrypt your emails before saving them to the Amazon S3 bucket. You can use the default master key or a custom master key that you created in AWS KMS as follows:\n\n- To use the default master key, provide an ARN in the form of `arn:aws:kms:REGION:ACCOUNT-ID-WITHOUT-HYPHENS:alias/aws/ses` . For example, if your AWS account ID is 123456789012 and you want to use the default master key in the US West (Oregon) Region, the ARN of the default master key would be `arn:aws:kms:us-west-2:123456789012:alias/aws/ses` . If you use the default master key, you don't need to perform any extra steps to give Amazon SES permission to use the key.\n- To use a custom master key that you created in AWS KMS, provide the ARN of the master key and ensure that you add a statement to your key's policy to give Amazon SES permission to use it. For more information about giving permissions, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/receiving-email-permissions.html) .\n\nFor more information about key policies, see the [AWS KMS Developer Guide](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html) . If you do not specify a master key, Amazon SES does not encrypt your emails.\n\n> Your mail is encrypted by Amazon SES using the Amazon S3 encryption client before the mail is submitted to Amazon S3 for storage. It is not encrypted using Amazon S3 server-side encryption. This means that you must use the Amazon S3 encryption client to decrypt the email after retrieving it from Amazon S3, as the service has no access to use your AWS KMS keys for decryption. This encryption client is currently available with the [AWS SDK for Java](https://docs.aws.amazon.com/sdk-for-java/) and [AWS SDK for Ruby](https://docs.aws.amazon.com/sdk-for-ruby/) only. For more information about client-side encryption using AWS KMS master keys, see the [Amazon S3 Developer Guide](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingClientSideEncryption.html) .", "ObjectKeyPrefix": "The key prefix of the Amazon S3 bucket. The key prefix is similar to a directory name that enables you to store similar data under the same directory in a bucket.", "TopicArn": "The ARN of the Amazon SNS topic to notify when the message is saved to the Amazon S3 bucket. You can find the ARN of a topic by using the [ListTopics](https://docs.aws.amazon.com/sns/latest/api/API_ListTopics.html) operation in Amazon SNS.\n\nFor more information about Amazon SNS topics, see the [Amazon SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/CreateTopic.html) ." } }, "AWS::SES::ReceiptRule.SNSAction": { "attributes": {}, - "description": "When included in a receipt rule, this action publishes a notification to Amazon Simple Notification Service (Amazon SNS). This action includes a complete copy of the email content in the Amazon SNS notifications. Amazon SNS notifications for all other actions simply provide information about the email. They do not include the email content itself.\n\nIf you own the Amazon SNS topic, you don't need to do anything to give Amazon SES permission to publish emails to it. However, if you don't own the Amazon SNS topic, you need to attach a policy to the topic to give Amazon SES permissions to access it. For information about giving permissions, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-permissions.html) .\n\n> You can only publish emails that are 150 KB or less (including the header) to Amazon SNS. Larger emails bounce. If you anticipate emails larger than 150 KB, use the S3 action instead. \n\nFor information about using a receipt rule to publish an Amazon SNS notification, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-sns.html) .", + "description": "When included in a receipt rule, this action publishes a notification to Amazon Simple Notification Service (Amazon SNS). This action includes a complete copy of the email content in the Amazon SNS notifications. Amazon SNS notifications for all other actions simply provide information about the email. They do not include the email content itself.\n\nIf you own the Amazon SNS topic, you don't need to do anything to give Amazon SES permission to publish emails to it. However, if you don't own the Amazon SNS topic, you need to attach a policy to the topic to give Amazon SES permissions to access it. For information about giving permissions, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/receiving-email-permissions.html) .\n\n> You can only publish emails that are 150 KB or less (including the header) to Amazon SNS. Larger emails bounce. If you anticipate emails larger than 150 KB, use the S3 action instead. \n\nFor information about using a receipt rule to publish an Amazon SNS notification, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/receiving-email-action-sns.html) .", "properties": { "Encoding": "The encoding to use for the email within the Amazon SNS notification. UTF-8 is easier to use, but may not preserve all special characters when a message was encoded with a different encoding format. Base64 preserves all special characters. The default value is UTF-8.", "TopicArn": "The Amazon Resource Name (ARN) of the Amazon SNS topic to notify. You can find the ARN of a topic by using the [ListTopics](https://docs.aws.amazon.com/sns/latest/api/API_ListTopics.html) operation in Amazon SNS.\n\nFor more information about Amazon SNS topics, see the [Amazon SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/CreateTopic.html) ." @@ -34429,7 +35240,7 @@ }, "AWS::SES::ReceiptRule.StopAction": { "attributes": {}, - "description": "When included in a receipt rule, this action terminates the evaluation of the receipt rule set and, optionally, publishes a notification to Amazon Simple Notification Service (Amazon SNS).\n\nFor information about setting a stop action in a receipt rule, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-stop.html) .", + "description": "When included in a receipt rule, this action terminates the evaluation of the receipt rule set and, optionally, publishes a notification to Amazon Simple Notification Service (Amazon SNS).\n\nFor information about setting a stop action in a receipt rule, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/receiving-email-action-stop.html) .", "properties": { "Scope": "The scope of the StopAction. The only acceptable value is `RuleSet` .", "TopicArn": "The Amazon Resource Name (ARN) of the Amazon SNS topic to notify when the stop action is taken. You can find the ARN of a topic by using the [ListTopics](https://docs.aws.amazon.com/sns/latest/api/API_ListTopics.html) Amazon SNS operation.\n\nFor more information about Amazon SNS topics, see the [Amazon SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/CreateTopic.html) ." @@ -34437,7 +35248,7 @@ }, "AWS::SES::ReceiptRule.WorkmailAction": { "attributes": {}, - "description": "When included in a receipt rule, this action calls Amazon WorkMail and, optionally, publishes a notification to Amazon Simple Notification Service (Amazon SNS). It usually isn't necessary to set this up manually, because Amazon WorkMail adds the rule automatically during its setup procedure.\n\nFor information using a receipt rule to call Amazon WorkMail, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-workmail.html) .", + "description": "When included in a receipt rule, this action calls Amazon WorkMail and, optionally, publishes a notification to Amazon Simple Notification Service (Amazon SNS). It usually isn't necessary to set this up manually, because Amazon WorkMail adds the rule automatically during its setup procedure.\n\nFor information using a receipt rule to call Amazon WorkMail, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/receiving-email-action-workmail.html) .", "properties": { "OrganizationArn": "The Amazon Resource Name (ARN) of the Amazon WorkMail organization. Amazon WorkMail ARNs use the following format:\n\n`arn:aws:workmail:::organization/`\n\nYou can find the ID of your organization by using the [ListOrganizations](https://docs.aws.amazon.com/workmail/latest/APIReference/API_ListOrganizations.html) operation in Amazon WorkMail. Amazon WorkMail organization IDs begin with \" `m-` \", followed by a string of alphanumeric characters.\n\nFor information about Amazon WorkMail organizations, see the [Amazon WorkMail Administrator Guide](https://docs.aws.amazon.com/workmail/latest/adminguide/organizations_overview.html) .", "TopicArn": "The Amazon Resource Name (ARN) of the Amazon SNS topic to notify when the WorkMail action is called. You can find the ARN of a topic by using the [ListTopics](https://docs.aws.amazon.com/sns/latest/api/API_ListTopics.html) operation in Amazon SNS.\n\nFor more information about Amazon SNS topics, see the [Amazon SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/CreateTopic.html) ." @@ -34447,21 +35258,23 @@ "attributes": { "Ref": "`Ref` returns the resource name. For example:" }, - "description": "Creates an empty receipt rule set.\n\nFor information about setting up receipt rule sets, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-receipt-rule-set.html) .\n\nYou can execute this operation no more than once per second.", + "description": "Creates an empty receipt rule set.\n\nFor information about setting up receipt rule sets, see the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/receiving-email-concepts.html#receiving-email-concepts-rules) .\n\nYou can execute this operation no more than once per second.", "properties": { "RuleSetName": "The name of the receipt rule set to reorder." } }, "AWS::SES::Template": { - "attributes": {}, + "attributes": { + "Id": "" + }, "description": "Specifies an email template. Email templates enable you to send personalized email to one or more destinations in a single API operation.", "properties": { - "Template": "The content of the email, composed of a subject line, an HTML part, and a text-only part." + "Template": "The content of the email, composed of a subject line and either an HTML part or a text-only part." } }, "AWS::SES::Template.Template": { "attributes": {}, - "description": "The content of the email, composed of a subject line, an HTML part, and a text-only part.", + "description": "The content of the email, composed of a subject line and either an HTML part or a text-only part.", "properties": { "HtmlPart": "The HTML body of the email.", "SubjectPart": "The subject line of the email.", @@ -34520,6 +35333,7 @@ "attributes": { "Arn": "Returns the Amazon Resource Name (ARN) of the queue. For example: `arn:aws:sqs:us-east-2:123456789012:mystack-myqueue-15PG5C2FC1CW8` .", "QueueName": "Returns the queue name. For example: `mystack-myqueue-1VF9BKQH5BJVI` .", + "QueueUrl": "", "Ref": "`Ref` returns the queue URL. For example:\n\n`{ \"Ref\": \"https://sqs.us-east-2.amazonaws.com/123456789012/ab1-MyQueue-A2BCDEF3GHI4\" }`" }, "description": "The `AWS::SQS::Queue` resource creates an Amazon SQS standard or FIFO queue.\n\nKeep the following caveats in mind:\n\n- If you don't specify the `FifoQueue` property, Amazon SQS creates a standard queue.\n\n> You can't change the queue type after you create it and you can't convert an existing standard queue into a FIFO queue. You must either create a new FIFO queue for your application or delete your existing standard queue and recreate it as a FIFO queue. For more information, see [Moving from a standard queue to a FIFO queue](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues-moving.html) in the *Amazon SQS Developer Guide* .\n- If you don't provide a value for a property, the queue is created with the default value for the property.\n- If you delete a queue, you must wait at least 60 seconds before creating a queue with the same name.\n- To successfully create a new queue, you must provide a queue name that adheres to the [limits related to queues](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/limits-queues.html) and is unique within the scope of your queues.\n\nFor more information about creating FIFO (first-in-first-out) queues, see [Creating an Amazon SQS queue ( AWS CloudFormation )](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/screate-queue-cloudformation.html) in the *Amazon SQS Developer Guide* .", @@ -34553,7 +35367,7 @@ }, "AWS::SSM::Association": { "attributes": { - "AssociationId": "" + "AssociationId": "The association ID." }, "description": "The `AWS::SSM::Association` resource creates a State Manager association for your managed instances. A State Manager association defines the state that you want to maintain on your instances. For example, an association can specify that anti-virus software must be installed and running on your instances, or that certain ports must be closed. For static targets, the association specifies a schedule for when the configuration is reapplied. For dynamic targets, such as an AWS Resource Groups or an AWS Auto Scaling Group, State Manager applies the configuration when new instances are added to the group. The association also specifies actions to take when applying the configuration. For example, an association for anti-virus software might run once a day. If the software is not installed, then State Manager installs it. If the software is installed, but the service is not running, then the association might instruct State Manager to start the service.", "properties": { @@ -34562,7 +35376,7 @@ "AutomationTargetParameterName": "Choose the parameter that will define how your automation will branch out. This target is required for associations that use an Automation runbook and target resources by using rate controls. Automation is a capability of AWS Systems Manager .", "CalendarNames": "The names or Amazon Resource Names (ARNs) of the Change Calendar type documents your associations are gated under. The associations only run when that Change Calendar is open. For more information, see [AWS Systems Manager Change Calendar](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-change-calendar) .", "ComplianceSeverity": "The severity level that is assigned to the association.", - "DocumentVersion": "The version of the SSM document to associate with the target.", + "DocumentVersion": "The version of the SSM document to associate with the target.\n\n> Note the following important information.\n> \n> - State Manager doesn't support running associations that use a new version of a document if that document is shared from another account. State Manager always runs the `default` version of a document if shared from another account, even though the Systems Manager console shows that a new version was processed. If you want to run an association using a new version of a document shared form another account, you must set the document version to `default` .\n> - `DocumentVersion` is not valid for documents owned by AWS , such as `AWS-RunPatchBaseline` or `AWS-UpdateSSMAgent` . If you specify `DocumentVersion` for an AWS document, the system returns the following error: \"Error occurred during operation 'CreateAssociation'.\" (RequestToken: , HandlerErrorCode: GeneralServiceException).", "InstanceId": "The ID of the instance that the SSM document is associated with. You must specify the `InstanceId` or `Targets` property.\n\n> `InstanceId` has been deprecated. To specify an instance ID for an association, use the `Targets` parameter. If you use the parameter `InstanceId` , you cannot use the parameters `AssociationName` , `DocumentVersion` , `MaxErrors` , `MaxConcurrency` , `OutputLocation` , or `ScheduleExpression` . To use these parameters, you must use the `Targets` parameter.", "MaxConcurrency": "The maximum number of targets allowed to run the association at the same time. You can specify a number, for example 10, or a percentage of the target set, for example 10%. The default value is 100%, which means all targets run the association at the same time.\n\nIf a new managed node starts and attempts to run an association while Systems Manager is running `MaxConcurrency` associations, the association is allowed to run. During the next association interval, the new managed node will process its association within the limit specified for `MaxConcurrency` .", "MaxErrors": "The number of errors that are allowed before the system stops sending requests to run the association on additional targets. You can specify either an absolute number of errors, for example 10, or a percentage of the target set, for example 10%. If you specify 3, for example, the system stops sending requests when the fourth error is received. If you specify 0, then the system stops sending requests after the first error is returned. If you run an association on 50 managed nodes and set `MaxError` to 10%, then the system stops sending the request when the sixth error is received.\n\nExecutions that are already running an association when `MaxErrors` is reached are allowed to complete, but some of these executions may fail as well. If you need to ensure that there won't be more than max-errors failed executions, set `MaxConcurrency` to 1 so that executions proceed one at a time.", @@ -34607,7 +35421,7 @@ "properties": { "Attachments": "A list of key-value pairs that describe attachments to a version of a document.", "Content": "The content for the new SSM document in JSON or YAML.\n\n> This parameter also supports `String` data types.", - "DocumentFormat": "Specify the document format for the request. The document format can be JSON or YAML. JSON is the default format.\n\n> `TEXT` is not supported, even though it is listed in the `Allowed values` .", + "DocumentFormat": "Specify the document format for the request. JSON is the default format.", "DocumentType": "The type of document to create.\n\n*Allowed Values* : `ApplicationConfigurationSchema` | `Automation` | `Automation.ChangeTemplate` | `Command` | `DeploymentStrategy` | `Package` | `Policy` | `Session`", "Name": "A name for the SSM document.\n\n> You can't use the following strings as document name prefixes. These are reserved by AWS for use as document name prefixes:\n> \n> - `aws-`\n> - `amazon`\n> - `amzn`", "Requires": "A list of SSM documents required by a document. This parameter is used exclusively by AWS AppConfig . When a user creates an AWS AppConfig configuration in an SSM document, the user must also specify a required document for validation purposes. In this case, an `ApplicationConfiguration` document requires an `ApplicationConfigurationSchema` document for validation purposes. For more information, see [What is AWS AppConfig ?](https://docs.aws.amazon.com/appconfig/latest/userguide/what-is-appconfig.html) in the *AWS AppConfig User Guide* .", @@ -34683,8 +35497,8 @@ "CutoffBehavior": "The specification for whether tasks should continue to run after the cutoff time specified in the maintenance windows is reached.", "Description": "A description of the task.", "LoggingInfo": "Information about an Amazon S3 bucket to write task-level logs to.\n\n> `LoggingInfo` has been deprecated. To specify an Amazon S3 bucket to contain logs, instead use the `OutputS3BucketName` and `OutputS3KeyPrefix` options in the `TaskInvocationParameters` structure. For information about how Systems Manager handles these options for the supported maintenance window task types, see [AWS Systems Manager MaintenanceWindowTask TaskInvocationParameters](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-maintenancewindowtask-taskinvocationparameters.html) .", - "MaxConcurrency": "The maximum number of targets this task can be run for, in parallel.", - "MaxErrors": "The maximum number of errors allowed before this task stops being scheduled.", + "MaxConcurrency": "The maximum number of targets this task can be run for, in parallel.\n\n> Although this element is listed as \"Required: No\", a value can be omitted only when you are registering or updating a [targetless task](https://docs.aws.amazon.com/systems-manager/latest/userguide/maintenance-windows-targetless-tasks.html) You must provide a value in all other cases.\n> \n> For maintenance window tasks without a target specified, you can't supply a value for this option. Instead, the system inserts a placeholder value of `1` . This value doesn't affect the running of your task.", + "MaxErrors": "The maximum number of errors allowed before this task stops being scheduled.\n\n> Although this element is listed as \"Required: No\", a value can be omitted only when you are registering or updating a [targetless task](https://docs.aws.amazon.com/systems-manager/latest/userguide/maintenance-windows-targetless-tasks.html) You must provide a value in all other cases.\n> \n> For maintenance window tasks without a target specified, you can't supply a value for this option. Instead, the system inserts a placeholder value of `1` . This value doesn't affect the running of your task.", "Name": "The task name.", "Priority": "The priority of the task in the maintenance window. The lower the number, the higher the priority. Tasks that have the same priority are scheduled in parallel.", "ServiceRoleArn": "The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) service role to use to publish Amazon Simple Notification Service (Amazon SNS) notifications for maintenance window Run Command tasks.", @@ -34696,6 +35510,14 @@ "WindowId": "The ID of the maintenance window where the task is registered." } }, + "AWS::SSM::MaintenanceWindowTask.CloudWatchOutputConfig": { + "attributes": {}, + "description": "Configuration options for sending command output to Amazon CloudWatch Logs.", + "properties": { + "CloudWatchLogGroupName": "The name of the CloudWatch Logs log group where you want to send command output. If you don't specify a group name, AWS Systems Manager automatically creates a log group for you. The log group uses the following naming format:\n\n`aws/ssm/ *SystemsManagerDocumentName*`", + "CloudWatchOutputEnabled": "Enables Systems Manager to send command output to CloudWatch Logs." + } + }, "AWS::SSM::MaintenanceWindowTask.LoggingInfo": { "attributes": {}, "description": "The `LoggingInfo` property type specifies information about the Amazon S3 bucket to write instance-level logs to.\n\n`LoggingInfo` is a property of the [AWS::SSM::MaintenanceWindowTask](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-maintenancewindowtask.html) resource.\n\n> `LoggingInfo` has been deprecated. To specify an Amazon S3 bucket to contain logs, instead use the `OutputS3BucketName` and `OutputS3KeyPrefix` options in the `TaskInvocationParameters` structure. For information about how Systems Manager handles these options for the supported maintenance window task types, see [AWS Systems Manager MaintenanceWindowTask TaskInvocationParameters](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-maintenancewindowtask-taskinvocationparameters.html) .", @@ -34726,9 +35548,11 @@ "attributes": {}, "description": "The `MaintenanceWindowRunCommandParameters` property type specifies the parameters for a `RUN_COMMAND` task type for a maintenance window task in AWS Systems Manager . This means that these parameters are the same as those for the `SendCommand` API call. For more information about `SendCommand` parameters, see [SendCommand](https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_SendCommand.html) in the *AWS Systems Manager API Reference* .\n\nFor information about available parameters in SSM Command documents, you can view the content of the document itself in the Systems Manager console. For information, see [Viewing SSM command document content](https://docs.aws.amazon.com/systems-manager/latest/userguide/viewing-ssm-document-content.html) in the *AWS Systems Manager User Guide* .\n\n`MaintenanceWindowRunCommandParameters` is a property of the [TaskInvocationParameters](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-maintenancewindowtask-taskinvocationparameters.html) property type.", "properties": { + "CloudWatchOutputConfig": "Configuration options for sending command output to Amazon CloudWatch Logs.", "Comment": "Information about the command or commands to run.", "DocumentHash": "The SHA-256 or SHA-1 hash created by the system when the document was created. SHA-1 hashes have been deprecated.", "DocumentHashType": "The SHA-256 or SHA-1 hash type. SHA-1 hashes are deprecated.", + "DocumentVersion": "The AWS Systems Manager document (SSM document) version to use in the request. You can specify `$DEFAULT` , `$LATEST` , or a specific version number. If you run commands by using the AWS CLI, then you must escape the first two options by using a backslash. If you specify a version number, then you don't need to use the backslash. For example:\n\n`--document-version \"\\$DEFAULT\"`\n\n`--document-version \"\\$LATEST\"`\n\n`--document-version \"3\"`", "NotificationConfig": "Configurations for sending notifications about command status changes on a per-managed node basis.", "OutputS3BucketName": "The name of the Amazon Simple Storage Service (Amazon S3) bucket.", "OutputS3KeyPrefix": "The S3 bucket subfolder.", @@ -34783,12 +35607,12 @@ "AllowedPattern": "A regular expression used to validate the parameter value. For example, for String types with values restricted to numbers, you can specify the following: `AllowedPattern=^\\d+$`", "DataType": "The data type of the parameter, such as `text` or `aws:ec2:image` . The default is `text` .", "Description": "Information about the parameter.", - "Name": "The name of the parameter.", + "Name": "The name of the parameter.\n\n> The maximum length constraint listed below includes capacity for additional system attributes that aren't part of the name. The maximum length for a parameter name, including the full length of the parameter ARN, is 1011 characters. For example, the length of the following parameter name is 65 characters, not 20 characters: `arn:aws:ssm:us-east-2:111222333444:parameter/ExampleParameterName`", "Policies": "Information about the policies assigned to a parameter.\n\n[Assigning parameter policies](https://docs.aws.amazon.com/systems-manager/latest/userguide/parameter-store-policies.html) in the *AWS Systems Manager User Guide* .", "Tags": "Optional metadata that you assign to a resource in the form of an arbitrary set of tags (key-value pairs). Tags enable you to categorize a resource in different ways, such as by purpose, owner, or environment. For example, you might want to tag a Systems Manager parameter to identify the type of resource to which it applies, the environment, or the type of configuration data referenced by the parameter.", "Tier": "The parameter tier.", "Type": "The type of parameter.\n\n> AWS CloudFormation doesn't support creating a `SecureString` parameter type. \n\n*Allowed Values* : String | StringList", - "Value": "The parameter value." + "Value": "The parameter value.\n\n> If type is `StringList` , the system returns a comma-separated string with no spaces between commas in the `Value` field." } }, "AWS::SSM::PatchBaseline": { @@ -34862,7 +35686,7 @@ "AWS::SSM::ResourceDataSync": { "attributes": { "Ref": "`Ref` returns the name of the resource data sync, such as `TestResourceDataSync` .", - "SyncName": "" + "SyncName": "The name of the resource data sync." }, "description": "The `AWS::SSM::ResourceDataSync` resource creates, updates, or deletes a resource data sync for AWS Systems Manager . A resource data sync helps you view data from multiple sources in a single location. Systems Manager offers two types of resource data sync: `SyncToDestination` and `SyncFromSource` .\n\nYou can configure Systems Manager Inventory to use the `SyncToDestination` type to synchronize Inventory data from multiple AWS Regions to a single Amazon S3 bucket.\n\nYou can configure Systems Manager Explorer to use the `SyncFromSource` type to synchronize operational work items (OpsItems) and operational data (OpsData) from multiple AWS Regions . This type can synchronize OpsItems and OpsData from multiple AWS accounts and Regions or from an `EntireOrganization` by using AWS Organizations .\n\nA resource data sync is an asynchronous operation that returns immediately. After a successful initial sync is completed, the system continuously syncs data.\n\nBy default, data is not encrypted in Amazon S3 . We strongly recommend that you enable encryption in Amazon S3 to ensure secure data storage. We also recommend that you secure access to the Amazon S3 bucket by creating a restrictive bucket policy.\n\nFor more information, see [Configuring Inventory Collection](https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-inventory-configuring.html#sysman-inventory-datasync) and [Setting Up Systems Manager Explorer to Display Data from Multiple Accounts and Regions](https://docs.aws.amazon.com/systems-manager/latest/userguide/Explorer-resource-data-sync.html) in the *AWS Systems Manager User Guide* .\n\nImportant: The following *Syntax* section shows all fields that are supported for a resource data sync. The *Examples* section below shows the recommended way to specify configurations for each sync type. Please see the *Examples* section when you create your resource data sync.", "properties": { @@ -35127,7 +35951,7 @@ "attributes": {}, "description": "Specifies the ARN's of a SageMaker image and SageMaker image version, and the instance type that the version runs on.", "properties": { - "InstanceType": "The instance type that the image version runs on.", + "InstanceType": "The instance type that the image version runs on.\n\n> JupyterServer Apps only support the `system` value.", "SageMakerImageArn": "The ARN of the SageMaker image that the image version belongs to.", "SageMakerImageVersionArn": "The ARN of the image version created on the instance." } @@ -35174,7 +35998,7 @@ "CodeRepositoryName": "The name of the code repository, such as `myCodeRepo` .", "Ref": "`Ref` returns the Amazon Resource Name (ARN) of the code repository." }, - "description": "Creates a Git repository as a resource in your Amazon SageMaker account. You can associate the repository with notebook instances so that you can use Git source control for the notebooks you create. The Git repository is a resource in your Amazon SageMaker account, so it can be associated with more than one notebook instance, and it persists independently from the lifecycle of any notebook instances it is associated with.\n\nThe repository can be hosted either in [AWS CodeCommit](https://docs.aws.amazon.com/codecommit/latest/userguide/welcome.html) or in any other Git repository.", + "description": "Creates a Git repository as a resource in your SageMaker account. You can associate the repository with notebook instances so that you can use Git source control for the notebooks you create. The Git repository is a resource in your SageMaker account, so it can be associated with more than one notebook instance, and it persists independently from the lifecycle of any notebook instances it is associated with.\n\nThe repository can be hosted either in [AWS CodeCommit](https://docs.aws.amazon.com/codecommit/latest/userguide/welcome.html) or in any other Git repository.", "properties": { "CodeRepositoryName": "The name of the Git repository.", "GitConfig": "Configuration details for the Git repository, including the URL where it is located and the ARN of the AWS Secrets Manager secret that contains the credentials used to access the repository.", @@ -35314,9 +36138,9 @@ }, "AWS::SageMaker::DataQualityJobDefinition.StoppingCondition": { "attributes": {}, - "description": "Specifies a limit to how long a model training job or model compilation job can run. It also specifies how long a managed spot training job has to complete. When the job reaches the time limit, Amazon SageMaker ends the training or compilation job. Use this API to cap model training costs.\n\nTo stop a training job, Amazon SageMaker sends the algorithm the `SIGTERM` signal, which delays job termination for 120 seconds. Algorithms can use this 120-second window to save the model artifacts, so the results of training are not lost.\n\nThe training algorithms provided by Amazon SageMaker automatically save the intermediate results of a model training job when possible. This attempt to save artifacts is only a best effort case as model might not be in a state from which it can be saved. For example, if training has just started, the model might not be ready to save. When saved, this intermediate data is a valid model artifact. You can use it to create a model with `CreateModel` .\n\n> The Neural Topic Model (NTM) currently does not support saving intermediate model artifacts. When training NTMs, make sure that the maximum runtime is sufficient for the training job to complete.", + "description": "Specifies a limit to how long a model training job or model compilation job can run. It also specifies how long a managed spot training job has to complete. When the job reaches the time limit, SageMaker ends the training or compilation job. Use this API to cap model training costs.\n\nTo stop a training job, SageMaker sends the algorithm the `SIGTERM` signal, which delays job termination for 120 seconds. Algorithms can use this 120-second window to save the model artifacts, so the results of training are not lost.\n\nThe training algorithms provided by SageMaker automatically save the intermediate results of a model training job when possible. This attempt to save artifacts is only a best effort case as model might not be in a state from which it can be saved. For example, if training has just started, the model might not be ready to save. When saved, this intermediate data is a valid model artifact. You can use it to create a model with `CreateModel` .\n\n> The Neural Topic Model (NTM) currently does not support saving intermediate model artifacts. When training NTMs, make sure that the maximum runtime is sufficient for the training job to complete.", "properties": { - "MaxRuntimeInSeconds": "The maximum length of time, in seconds, that a training or compilation job can run.\n\nFor compilation jobs, if the job does not complete during this time, you will receive a `TimeOut` error. We recommend starting with 900 seconds and increase as necessary based on your model.\n\nFor all other jobs, if the job does not complete during this time, Amazon SageMaker ends the job. When `RetryStrategy` is specified in the job request, `MaxRuntimeInSeconds` specifies the maximum time for all of the attempts in total, not each individual attempt. The default value is 1 day. The maximum value is 28 days." + "MaxRuntimeInSeconds": "The maximum length of time, in seconds, that a training or compilation job can run.\n\nFor compilation jobs, if the job does not complete during this time, a `TimeOut` error is generated. We recommend starting with 900 seconds and increasing as necessary based on your model.\n\nFor all other jobs, if the job does not complete during this time, SageMaker ends the job. When `RetryStrategy` is specified in the job request, `MaxRuntimeInSeconds` specifies the maximum time for all of the attempts in total, not each individual attempt. The default value is 1 day. The maximum value is 28 days." } }, "AWS::SageMaker::DataQualityJobDefinition.VpcConfig": { @@ -35410,14 +36234,14 @@ "description": "The KernelGateway app settings.", "properties": { "CustomImages": "A list of custom SageMaker images that are configured to run as a KernelGateway app.", - "DefaultResourceSpec": "The default instance type and the Amazon Resource Name (ARN) of the default SageMaker image used by the KernelGateway app." + "DefaultResourceSpec": "The default instance type and the Amazon Resource Name (ARN) of the default SageMaker image used by the KernelGateway app.\n\n> The Amazon SageMaker Studio UI does not use the default instance type value set here. The default instance type set here is used when Apps are created using the AWS Command Line Interface or AWS CloudFormation and the instance type parameter value is not passed." } }, "AWS::SageMaker::Domain.ResourceSpec": { "attributes": {}, "description": "Specifies the ARN's of a SageMaker image and SageMaker image version, and the instance type that the version runs on.", "properties": { - "InstanceType": "The instance type that the image version runs on.", + "InstanceType": "The instance type that the image version runs on.\n\n> JupyterServer Apps only support the `system` value.", "SageMakerImageArn": "The ARN of the SageMaker image that the image version belongs to.", "SageMakerImageVersionArn": "The ARN of the image version created on the instance." } @@ -35639,7 +36463,7 @@ "ImageArn": "The Amazon Resource Name (ARN) of the image.\n\n*Type* : String\n\n*Length Constraints* : Maximum length of 256.\n\n*Pattern* : `^arn:aws(-[\\w]+)*:sagemaker:.+:[0-9]{12}:image/[a-z0-9]([-.]?[a-z0-9])*$`", "Ref": "`Ref` returns the ImageArn." }, - "description": "Creates a custom SageMaker image. A SageMaker image is a set of image versions. Each image version represents a container image stored in Amazon Container Registry (ECR). For more information, see [Bring your own SageMaker image](https://docs.aws.amazon.com/sagemaker/latest/dg/studio-byoi.html) .", + "description": "Creates a custom SageMaker image. A SageMaker image is a set of image versions. Each image version represents a container image stored in Amazon Elastic Container Registry (ECR). For more information, see [Bring your own SageMaker image](https://docs.aws.amazon.com/sagemaker/latest/dg/studio-byoi.html) .", "properties": { "ImageDescription": "The description of the image.\n\n*Length Constraints* : Minimum length of 1. Maximum length of 512.\n\n*Pattern* : `.*`", "ImageDisplayName": "The display name of the image.\n\n*Length Constraints* : Minimum length of 1. Maximum length of 128.\n\n*Pattern* : `^\\S(.*\\S)?$`", @@ -35671,7 +36495,7 @@ "properties": { "Containers": "Specifies the containers in the inference pipeline.", "EnableNetworkIsolation": "Isolates the model container. No inbound or outbound network calls can be made to or from the model container.", - "ExecutionRoleArn": "The Amazon Resource Name (ARN) of the IAM role that Amazon SageMaker can assume to access model artifacts and docker image for deployment on ML compute instances or for batch transform jobs. Deploying on ML compute instances is part of model hosting. For more information, see [Amazon SageMaker Roles](https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-roles.html) .\n\n> To be able to pass this role to Amazon SageMaker, the caller of this API must have the `iam:PassRole` permission.", + "ExecutionRoleArn": "The Amazon Resource Name (ARN) of the IAM role that SageMaker can assume to access model artifacts and docker image for deployment on ML compute instances or for batch transform jobs. Deploying on ML compute instances is part of model hosting. For more information, see [SageMaker Roles](https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-roles.html) .\n\n> To be able to pass this role to SageMaker, the caller of this API must have the `iam:PassRole` permission.", "InferenceExecutionConfig": "Specifies details of how containers in a multi-container endpoint are called.", "ModelName": "The name of the new model.", "PrimaryContainer": "The location of the primary docker image containing inference code, associated artifacts, and custom environment map that the inference code uses when the model is deployed for predictions.", @@ -35685,13 +36509,13 @@ "properties": { "ContainerHostname": "This parameter is ignored for models that contain only a `PrimaryContainer` .\n\nWhen a `ContainerDefinition` is part of an inference pipeline, the value of the parameter uniquely identifies the container for the purposes of logging and metrics. For information, see [Use Logs and Metrics to Monitor an Inference Pipeline](https://docs.aws.amazon.com/sagemaker/latest/dg/inference-pipeline-logs-metrics.html) . If you don't specify a value for this parameter for a `ContainerDefinition` that is part of an inference pipeline, a unique name is automatically assigned based on the position of the `ContainerDefinition` in the pipeline. If you specify a value for the `ContainerHostName` for any `ContainerDefinition` that is part of an inference pipeline, you must specify a value for the `ContainerHostName` parameter of every `ContainerDefinition` in that pipeline.", "Environment": "The environment variables to set in the Docker container. Each key and value in the `Environment` string to string map can have length of up to 1024. We support up to 16 entries in the map.", - "Image": "The path where inference code is stored. This can be either in Amazon EC2 Container Registry or in a Docker registry that is accessible from the same VPC that you configure for your endpoint. If you are using your own custom algorithm instead of an algorithm provided by Amazon SageMaker, the inference code must meet Amazon SageMaker requirements. Amazon SageMaker supports both `registry/repository[:tag]` and `registry/repository[@digest]` image path formats. For more information, see [Using Your Own Algorithms with Amazon SageMaker](https://docs.aws.amazon.com/sagemaker/latest/dg/your-algorithms.html)", + "Image": "The path where inference code is stored. This can be either in Amazon EC2 Container Registry or in a Docker registry that is accessible from the same VPC that you configure for your endpoint. If you are using your own custom algorithm instead of an algorithm provided by SageMaker, the inference code must meet SageMaker requirements. SageMaker supports both `registry/repository[:tag]` and `registry/repository[@digest]` image path formats. For more information, see [Using Your Own Algorithms with Amazon SageMaker](https://docs.aws.amazon.com/sagemaker/latest/dg/your-algorithms.html)", "ImageConfig": "Specifies whether the model container is in Amazon ECR or a private Docker registry accessible from your Amazon Virtual Private Cloud (VPC). For information about storing containers in a private Docker registry, see [Use a Private Docker Registry for Real-Time Inference Containers](https://docs.aws.amazon.com/sagemaker/latest/dg/your-algorithms-containers-inference-private.html)", "InferenceSpecificationName": "The inference specification name in the model package version.", "Mode": "Whether the container hosts a single model or multiple models.", - "ModelDataUrl": "The S3 path where the model artifacts, which result from model training, are stored. This path must point to a single gzip compressed tar archive (.tar.gz suffix). The S3 path is required for Amazon SageMaker built-in algorithms, but not if you use your own algorithms. For more information on built-in algorithms, see [Common Parameters](https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-algo-docker-registry-paths.html) .\n\n> The model artifacts must be in an S3 bucket that is in the same region as the model or endpoint you are creating. \n\nIf you provide a value for this parameter, Amazon SageMaker uses AWS Security Token Service to download model artifacts from the S3 path you provide. AWS STS is activated in your IAM user account by default. If you previously deactivated AWS STS for a region, you need to reactivate AWS STS for that region. For more information, see [Activating and Deactivating AWS STS in an AWS Region](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_enable-regions.html) in the *AWS Identity and Access Management User Guide* .\n\n> If you use a built-in algorithm to create a model, Amazon SageMaker requires that you provide a S3 path to the model artifacts in `ModelDataUrl` .", + "ModelDataUrl": "The S3 path where the model artifacts, which result from model training, are stored. This path must point to a single gzip compressed tar archive (.tar.gz suffix). The S3 path is required for SageMaker built-in algorithms, but not if you use your own algorithms. For more information on built-in algorithms, see [Common Parameters](https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-algo-docker-registry-paths.html) .\n\n> The model artifacts must be in an S3 bucket that is in the same region as the model or endpoint you are creating. \n\nIf you provide a value for this parameter, SageMaker uses AWS Security Token Service to download model artifacts from the S3 path you provide. AWS STS is activated in your IAM user account by default. If you previously deactivated AWS STS for a region, you need to reactivate AWS STS for that region. For more information, see [Activating and Deactivating AWS STS in an AWS Region](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_enable-regions.html) in the *AWS Identity and Access Management User Guide* .\n\n> If you use a built-in algorithm to create a model, SageMaker requires that you provide a S3 path to the model artifacts in `ModelDataUrl` .", "ModelPackageName": "The name or Amazon Resource Name (ARN) of the model package to use to create the model.", - "MultiModelConfig": "" + "MultiModelConfig": "Specifies additional configuration for multi-model endpoints." } }, "AWS::SageMaker::Model.ImageConfig": { @@ -35704,7 +36528,7 @@ }, "AWS::SageMaker::Model.InferenceExecutionConfig": { "attributes": {}, - "description": "", + "description": "Specifies details about how containers in a multi-container endpoint are run.", "properties": { "Mode": "How containers in a multi-container are run. The following values are valid.\n\n- `Serial` - Containers run as a serial pipeline.\n- `Direct` - Only the individual container that you specify is run." } @@ -35858,9 +36682,9 @@ }, "AWS::SageMaker::ModelBiasJobDefinition.StoppingCondition": { "attributes": {}, - "description": "Specifies a limit to how long a model training job or model compilation job can run. It also specifies how long a managed spot training job has to complete. When the job reaches the time limit, Amazon SageMaker ends the training or compilation job. Use this API to cap model training costs.\n\nTo stop a training job, Amazon SageMaker sends the algorithm the `SIGTERM` signal, which delays job termination for 120 seconds. Algorithms can use this 120-second window to save the model artifacts, so the results of training are not lost.\n\nThe training algorithms provided by Amazon SageMaker automatically save the intermediate results of a model training job when possible. This attempt to save artifacts is only a best effort case as model might not be in a state from which it can be saved. For example, if training has just started, the model might not be ready to save. When saved, this intermediate data is a valid model artifact. You can use it to create a model with `CreateModel` .\n\n> The Neural Topic Model (NTM) currently does not support saving intermediate model artifacts. When training NTMs, make sure that the maximum runtime is sufficient for the training job to complete.", + "description": "Specifies a limit to how long a model training job or model compilation job can run. It also specifies how long a managed spot training job has to complete. When the job reaches the time limit, SageMaker ends the training or compilation job. Use this API to cap model training costs.\n\nTo stop a training job, SageMaker sends the algorithm the `SIGTERM` signal, which delays job termination for 120 seconds. Algorithms can use this 120-second window to save the model artifacts, so the results of training are not lost.\n\nThe training algorithms provided by SageMaker automatically save the intermediate results of a model training job when possible. This attempt to save artifacts is only a best effort case as model might not be in a state from which it can be saved. For example, if training has just started, the model might not be ready to save. When saved, this intermediate data is a valid model artifact. You can use it to create a model with `CreateModel` .\n\n> The Neural Topic Model (NTM) currently does not support saving intermediate model artifacts. When training NTMs, make sure that the maximum runtime is sufficient for the training job to complete.", "properties": { - "MaxRuntimeInSeconds": "The maximum length of time, in seconds, that a training or compilation job can run.\n\nFor compilation jobs, if the job does not complete during this time, you will receive a `TimeOut` error. We recommend starting with 900 seconds and increase as necessary based on your model.\n\nFor all other jobs, if the job does not complete during this time, Amazon SageMaker ends the job. When `RetryStrategy` is specified in the job request, `MaxRuntimeInSeconds` specifies the maximum time for all of the attempts in total, not each individual attempt. The default value is 1 day. The maximum value is 28 days." + "MaxRuntimeInSeconds": "The maximum length of time, in seconds, that a training or compilation job can run.\n\nFor compilation jobs, if the job does not complete during this time, a `TimeOut` error is generated. We recommend starting with 900 seconds and increasing as necessary based on your model.\n\nFor all other jobs, if the job does not complete during this time, SageMaker ends the job. When `RetryStrategy` is specified in the job request, `MaxRuntimeInSeconds` specifies the maximum time for all of the attempts in total, not each individual attempt. The default value is 1 day. The maximum value is 28 days." } }, "AWS::SageMaker::ModelBiasJobDefinition.VpcConfig": { @@ -35935,7 +36759,7 @@ "description": "The configuration for a baseline model explainability job.", "properties": { "BaseliningJobName": "The name of the baseline model explainability job.", - "ConstraintsResource": "" + "ConstraintsResource": "The constraints resource for a model explainability job." } }, "AWS::SageMaker::ModelExplainabilityJobDefinition.ModelExplainabilityJobInput": { @@ -35987,9 +36811,9 @@ }, "AWS::SageMaker::ModelExplainabilityJobDefinition.StoppingCondition": { "attributes": {}, - "description": "Specifies a limit to how long a model training job or model compilation job can run. It also specifies how long a managed spot training job has to complete. When the job reaches the time limit, Amazon SageMaker ends the training or compilation job. Use this API to cap model training costs.\n\nTo stop a training job, Amazon SageMaker sends the algorithm the `SIGTERM` signal, which delays job termination for 120 seconds. Algorithms can use this 120-second window to save the model artifacts, so the results of training are not lost.\n\nThe training algorithms provided by Amazon SageMaker automatically save the intermediate results of a model training job when possible. This attempt to save artifacts is only a best effort case as model might not be in a state from which it can be saved. For example, if training has just started, the model might not be ready to save. When saved, this intermediate data is a valid model artifact. You can use it to create a model with `CreateModel` .\n\n> The Neural Topic Model (NTM) currently does not support saving intermediate model artifacts. When training NTMs, make sure that the maximum runtime is sufficient for the training job to complete.", + "description": "Specifies a limit to how long a model training job or model compilation job can run. It also specifies how long a managed spot training job has to complete. When the job reaches the time limit, SageMaker ends the training or compilation job. Use this API to cap model training costs.\n\nTo stop a training job, SageMaker sends the algorithm the `SIGTERM` signal, which delays job termination for 120 seconds. Algorithms can use this 120-second window to save the model artifacts, so the results of training are not lost.\n\nThe training algorithms provided by SageMaker automatically save the intermediate results of a model training job when possible. This attempt to save artifacts is only a best effort case as model might not be in a state from which it can be saved. For example, if training has just started, the model might not be ready to save. When saved, this intermediate data is a valid model artifact. You can use it to create a model with `CreateModel` .\n\n> The Neural Topic Model (NTM) currently does not support saving intermediate model artifacts. When training NTMs, make sure that the maximum runtime is sufficient for the training job to complete.", "properties": { - "MaxRuntimeInSeconds": "The maximum length of time, in seconds, that a training or compilation job can run.\n\nFor compilation jobs, if the job does not complete during this time, you will receive a `TimeOut` error. We recommend starting with 900 seconds and increase as necessary based on your model.\n\nFor all other jobs, if the job does not complete during this time, Amazon SageMaker ends the job. When `RetryStrategy` is specified in the job request, `MaxRuntimeInSeconds` specifies the maximum time for all of the attempts in total, not each individual attempt. The default value is 1 day. The maximum value is 28 days." + "MaxRuntimeInSeconds": "The maximum length of time, in seconds, that a training or compilation job can run.\n\nFor compilation jobs, if the job does not complete during this time, a `TimeOut` error is generated. We recommend starting with 900 seconds and increasing as necessary based on your model.\n\nFor all other jobs, if the job does not complete during this time, SageMaker ends the job. When `RetryStrategy` is specified in the job request, `MaxRuntimeInSeconds` specifies the maximum time for all of the attempts in total, not each individual attempt. The default value is 1 day. The maximum value is 28 days." } }, "AWS::SageMaker::ModelExplainabilityJobDefinition.VpcConfig": { @@ -36021,11 +36845,11 @@ "JobDefinitionArn": "The Amazon Resource Name (ARN) of the job definition.", "Ref": "" }, - "description": "", + "description": "Creates a definition for a job that monitors model quality and drift. For information about model monitor, see [Amazon SageMaker Model Monitor](https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor.html) .", "properties": { "JobDefinitionName": "The name of the monitoring job definition.", "JobResources": "Identifies the resources to deploy for a monitoring job.", - "ModelQualityAppSpecification": "", + "ModelQualityAppSpecification": "Container image configuration object for the monitoring job.", "ModelQualityBaselineConfig": "Specifies the constraints and baselines for the monitoring job.", "ModelQualityJobInput": "A list of the inputs that are monitored. Currently endpoints are supported.", "ModelQualityJobOutputConfig": "The output configuration for monitoring jobs.", @@ -36085,7 +36909,7 @@ "description": "Configuration for monitoring constraints and monitoring statistics. These baseline resources are compared against the results of the current job from the series of jobs scheduled to collect data periodically.", "properties": { "BaseliningJobName": "The name of the job that performs baselining for the monitoring job.", - "ConstraintsResource": "" + "ConstraintsResource": "The constraints resource for a monitoring job." } }, "AWS::SageMaker::ModelQualityJobDefinition.ModelQualityJobInput": { @@ -36145,9 +36969,9 @@ }, "AWS::SageMaker::ModelQualityJobDefinition.StoppingCondition": { "attributes": {}, - "description": "Specifies a limit to how long a model training job or model compilation job can run. It also specifies how long a managed spot training job has to complete. When the job reaches the time limit, Amazon SageMaker ends the training or compilation job. Use this API to cap model training costs.\n\nTo stop a training job, Amazon SageMaker sends the algorithm the `SIGTERM` signal, which delays job termination for 120 seconds. Algorithms can use this 120-second window to save the model artifacts, so the results of training are not lost.\n\nThe training algorithms provided by Amazon SageMaker automatically save the intermediate results of a model training job when possible. This attempt to save artifacts is only a best effort case as model might not be in a state from which it can be saved. For example, if training has just started, the model might not be ready to save. When saved, this intermediate data is a valid model artifact. You can use it to create a model with `CreateModel` .\n\n> The Neural Topic Model (NTM) currently does not support saving intermediate model artifacts. When training NTMs, make sure that the maximum runtime is sufficient for the training job to complete.", + "description": "Specifies a limit to how long a model training job or model compilation job can run. It also specifies how long a managed spot training job has to complete. When the job reaches the time limit, SageMaker ends the training or compilation job. Use this API to cap model training costs.\n\nTo stop a training job, SageMaker sends the algorithm the `SIGTERM` signal, which delays job termination for 120 seconds. Algorithms can use this 120-second window to save the model artifacts, so the results of training are not lost.\n\nThe training algorithms provided by SageMaker automatically save the intermediate results of a model training job when possible. This attempt to save artifacts is only a best effort case as model might not be in a state from which it can be saved. For example, if training has just started, the model might not be ready to save. When saved, this intermediate data is a valid model artifact. You can use it to create a model with `CreateModel` .\n\n> The Neural Topic Model (NTM) currently does not support saving intermediate model artifacts. When training NTMs, make sure that the maximum runtime is sufficient for the training job to complete.", "properties": { - "MaxRuntimeInSeconds": "The maximum length of time, in seconds, that a training or compilation job can run.\n\nFor compilation jobs, if the job does not complete during this time, you will receive a `TimeOut` error. We recommend starting with 900 seconds and increase as necessary based on your model.\n\nFor all other jobs, if the job does not complete during this time, Amazon SageMaker ends the job. When `RetryStrategy` is specified in the job request, `MaxRuntimeInSeconds` specifies the maximum time for all of the attempts in total, not each individual attempt. The default value is 1 day. The maximum value is 28 days." + "MaxRuntimeInSeconds": "The maximum length of time, in seconds, that a training or compilation job can run.\n\nFor compilation jobs, if the job does not complete during this time, a `TimeOut` error is generated. We recommend starting with 900 seconds and increasing as necessary based on your model.\n\nFor all other jobs, if the job does not complete during this time, SageMaker ends the job. When `RetryStrategy` is specified in the job request, `MaxRuntimeInSeconds` specifies the maximum time for all of the attempts in total, not each individual attempt. The default value is 1 day. The maximum value is 28 days." } }, "AWS::SageMaker::ModelQualityJobDefinition.VpcConfig": { @@ -36162,7 +36986,7 @@ "attributes": { "CreationTime": "The time when the monitoring schedule was created.", "LastModifiedTime": "The last time that the monitoring schedule was modified.", - "MonitoringScheduleArn": "", + "MonitoringScheduleArn": "The Amazon Resource Name (ARN) of the monitoring schedule.", "Ref": "`Ref` returns the Amazon Resource Name (ARN) of the monitoring schedule." }, "description": "The `AWS::SageMaker::MonitoringSchedule` resource is an Amazon SageMaker resource type that regularly starts SageMaker processing Jobs to monitor the data captured for a SageMaker endpoint.", @@ -36324,9 +37148,9 @@ }, "AWS::SageMaker::MonitoringSchedule.StoppingCondition": { "attributes": {}, - "description": "Specifies a limit to how long a model training job or model compilation job can run. It also specifies how long a managed spot training job has to complete. When the job reaches the time limit, Amazon SageMaker ends the training or compilation job. Use this API to cap model training costs.\n\nTo stop a training job, Amazon SageMaker sends the algorithm the `SIGTERM` signal, which delays job termination for 120 seconds. Algorithms can use this 120-second window to save the model artifacts, so the results of training are not lost.\n\nThe training algorithms provided by Amazon SageMaker automatically save the intermediate results of a model training job when possible. This attempt to save artifacts is only a best effort case as model might not be in a state from which it can be saved. For example, if training has just started, the model might not be ready to save. When saved, this intermediate data is a valid model artifact. You can use it to create a model with `CreateModel` .\n\n> The Neural Topic Model (NTM) currently does not support saving intermediate model artifacts. When training NTMs, make sure that the maximum runtime is sufficient for the training job to complete.", + "description": "Specifies a limit to how long a model training job or model compilation job can run. It also specifies how long a managed spot training job has to complete. When the job reaches the time limit, SageMaker ends the training or compilation job. Use this API to cap model training costs.\n\nTo stop a training job, SageMaker sends the algorithm the `SIGTERM` signal, which delays job termination for 120 seconds. Algorithms can use this 120-second window to save the model artifacts, so the results of training are not lost.\n\nThe training algorithms provided by SageMaker automatically save the intermediate results of a model training job when possible. This attempt to save artifacts is only a best effort case as model might not be in a state from which it can be saved. For example, if training has just started, the model might not be ready to save. When saved, this intermediate data is a valid model artifact. You can use it to create a model with `CreateModel` .\n\n> The Neural Topic Model (NTM) currently does not support saving intermediate model artifacts. When training NTMs, make sure that the maximum runtime is sufficient for the training job to complete.", "properties": { - "MaxRuntimeInSeconds": "The maximum length of time, in seconds, that a training or compilation job can run.\n\nFor compilation jobs, if the job does not complete during this time, you will receive a `TimeOut` error. We recommend starting with 900 seconds and increase as necessary based on your model.\n\nFor all other jobs, if the job does not complete during this time, Amazon SageMaker ends the job. When `RetryStrategy` is specified in the job request, `MaxRuntimeInSeconds` specifies the maximum time for all of the attempts in total, not each individual attempt. The default value is 1 day. The maximum value is 28 days." + "MaxRuntimeInSeconds": "The maximum length of time, in seconds, that a training or compilation job can run.\n\nFor compilation jobs, if the job does not complete during this time, a `TimeOut` error is generated. We recommend starting with 900 seconds and increasing as necessary based on your model.\n\nFor all other jobs, if the job does not complete during this time, SageMaker ends the job. When `RetryStrategy` is specified in the job request, `MaxRuntimeInSeconds` specifies the maximum time for all of the attempts in total, not each individual attempt. The default value is 1 day. The maximum value is 28 days." } }, "AWS::SageMaker::MonitoringSchedule.VpcConfig": { @@ -36345,15 +37169,15 @@ "description": "The `AWS::SageMaker::NotebookInstance` resource creates an Amazon SageMaker notebook instance. A notebook instance is a machine learning (ML) compute instance running on a Jupyter notebook. For more information, see [Use Notebook Instances](https://docs.aws.amazon.com/sagemaker/latest/dg/nbi.html) .", "properties": { "AcceleratorTypes": "A list of Amazon Elastic Inference (EI) instance types to associate with the notebook instance. Currently, only one instance type can be associated with a notebook instance. For more information, see [Using Elastic Inference in Amazon SageMaker](https://docs.aws.amazon.com/sagemaker/latest/dg/ei.html) .\n\n*Valid Values:* `ml.eia1.medium | ml.eia1.large | ml.eia1.xlarge | ml.eia2.medium | ml.eia2.large | ml.eia2.xlarge` .", - "AdditionalCodeRepositories": "An array of up to three Git repositories associated with the notebook instance. These can be either the names of Git repositories stored as resources in your account, or the URL of Git repositories in [AWS CodeCommit](https://docs.aws.amazon.com/codecommit/latest/userguide/welcome.html) or in any other Git repository. These repositories are cloned at the same level as the default repository of your notebook instance. For more information, see [Associating Git Repositories with Amazon SageMaker Notebook Instances](https://docs.aws.amazon.com/sagemaker/latest/dg/nbi-git-repo.html) .", - "DefaultCodeRepository": "The Git repository associated with the notebook instance as its default code repository. This can be either the name of a Git repository stored as a resource in your account, or the URL of a Git repository in [AWS CodeCommit](https://docs.aws.amazon.com/codecommit/latest/userguide/welcome.html) or in any other Git repository. When you open a notebook instance, it opens in the directory that contains this repository. For more information, see [Associating Git Repositories with Amazon SageMaker Notebook Instances](https://docs.aws.amazon.com/sagemaker/latest/dg/nbi-git-repo.html) .", - "DirectInternetAccess": "Sets whether Amazon SageMaker provides internet access to the notebook instance. If you set this to `Disabled` this notebook instance is able to access resources only in your VPC, and is not be able to connect to Amazon SageMaker training and endpoint services unless you configure a NAT Gateway in your VPC.\n\nFor more information, see [Notebook Instances Are Internet-Enabled by Default](https://docs.aws.amazon.com/sagemaker/latest/dg/appendix-additional-considerations.html#appendix-notebook-and-internet-access) . You can set the value of this parameter to `Disabled` only if you set a value for the `SubnetId` parameter.", + "AdditionalCodeRepositories": "An array of up to three Git repositories associated with the notebook instance. These can be either the names of Git repositories stored as resources in your account, or the URL of Git repositories in [AWS CodeCommit](https://docs.aws.amazon.com/codecommit/latest/userguide/welcome.html) or in any other Git repository. These repositories are cloned at the same level as the default repository of your notebook instance. For more information, see [Associating Git Repositories with SageMaker Notebook Instances](https://docs.aws.amazon.com/sagemaker/latest/dg/nbi-git-repo.html) .", + "DefaultCodeRepository": "The Git repository associated with the notebook instance as its default code repository. This can be either the name of a Git repository stored as a resource in your account, or the URL of a Git repository in [AWS CodeCommit](https://docs.aws.amazon.com/codecommit/latest/userguide/welcome.html) or in any other Git repository. When you open a notebook instance, it opens in the directory that contains this repository. For more information, see [Associating Git Repositories with SageMaker Notebook Instances](https://docs.aws.amazon.com/sagemaker/latest/dg/nbi-git-repo.html) .", + "DirectInternetAccess": "Sets whether SageMaker provides internet access to the notebook instance. If you set this to `Disabled` this notebook instance is able to access resources only in your VPC, and is not be able to connect to SageMaker training and endpoint services unless you configure a NAT Gateway in your VPC.\n\nFor more information, see [Notebook Instances Are Internet-Enabled by Default](https://docs.aws.amazon.com/sagemaker/latest/dg/appendix-additional-considerations.html#appendix-notebook-and-internet-access) . You can set the value of this parameter to `Disabled` only if you set a value for the `SubnetId` parameter.", "InstanceType": "The type of ML compute instance to launch for the notebook instance.\n\n> Expect some interruption of service if this parameter is changed as CloudFormation stops a notebook instance and starts it up again to update it.", - "KmsKeyId": "The Amazon Resource Name (ARN) of a AWS Key Management Service key that Amazon SageMaker uses to encrypt data on the storage volume attached to your notebook instance. The KMS key you provide must be enabled. For information, see [Enabling and Disabling Keys](https://docs.aws.amazon.com/kms/latest/developerguide/enabling-keys.html) in the *AWS Key Management Service Developer Guide* .", + "KmsKeyId": "The Amazon Resource Name (ARN) of a AWS Key Management Service key that SageMaker uses to encrypt data on the storage volume attached to your notebook instance. The KMS key you provide must be enabled. For information, see [Enabling and Disabling Keys](https://docs.aws.amazon.com/kms/latest/developerguide/enabling-keys.html) in the *AWS Key Management Service Developer Guide* .", "LifecycleConfigName": "The name of a lifecycle configuration to associate with the notebook instance. For information about lifecycle configurations, see [Customize a Notebook Instance](https://docs.aws.amazon.com/sagemaker/latest/dg/notebook-lifecycle-config.html) in the *Amazon SageMaker Developer Guide* .", "NotebookInstanceName": "The name of the new notebook instance.", "PlatformIdentifier": "The platform identifier of the notebook instance runtime environment.", - "RoleArn": "When you send any requests to AWS resources from the notebook instance, Amazon SageMaker assumes this role to perform tasks on your behalf. You must grant this role necessary permissions so Amazon SageMaker can perform these tasks. The policy must allow the Amazon SageMaker service principal (sagemaker.amazonaws.com) permissions to assume this role. For more information, see [Amazon SageMaker Roles](https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-roles.html) .\n\n> To be able to pass this role to Amazon SageMaker, the caller of this API must have the `iam:PassRole` permission.", + "RoleArn": "When you send any requests to AWS resources from the notebook instance, SageMaker assumes this role to perform tasks on your behalf. You must grant this role necessary permissions so SageMaker can perform these tasks. The policy must allow the SageMaker service principal (sagemaker.amazonaws.com) permissions to assume this role. For more information, see [SageMaker Roles](https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-roles.html) .\n\n> To be able to pass this role to SageMaker, the caller of this API must have the `iam:PassRole` permission.", "RootAccess": "Whether root access is enabled or disabled for users of the notebook instance. The default value is `Enabled` .\n\n> Lifecycle configurations need root access to be able to set up a notebook instance. Because of this, lifecycle configurations associated with a notebook instance always run with root access even if you disable root access for users.", "SecurityGroupIds": "The VPC security group IDs, in the form sg-xxxxxxxx. The security groups must be for the same VPC as specified in the subnet.", "SubnetId": "The ID of the subnet in a VPC to which you would like to have a connectivity from your ML compute instance.", @@ -36386,6 +37210,7 @@ }, "description": "The `AWS::SageMaker::Pipeline` resource creates shell scripts that run when you create and/or start a SageMaker Pipeline. For information about SageMaker Pipelines, see [SageMaker Pipelines](https://docs.aws.amazon.com/sagemaker/latest/dg/pipelines.html) in the *Amazon SageMaker Developer Guide* .", "properties": { + "ParallelismConfiguration": "The parallelism configuration applied to the pipeline.", "PipelineDefinition": "The definition of the pipeline. This can be either a JSON string or an Amazon S3 location.", "PipelineDescription": "The description of the pipeline.", "PipelineDisplayName": "The display name of the pipeline.", @@ -36446,14 +37271,14 @@ "description": "The KernelGateway app settings.", "properties": { "CustomImages": "A list of custom SageMaker images that are configured to run as a KernelGateway app.", - "DefaultResourceSpec": "The default instance type and the Amazon Resource Name (ARN) of the default SageMaker image used by the KernelGateway app." + "DefaultResourceSpec": "The default instance type and the Amazon Resource Name (ARN) of the default SageMaker image used by the KernelGateway app.\n\n> The Amazon SageMaker Studio UI does not use the default instance type value set here. The default instance type set here is used when Apps are created using the AWS Command Line Interface or AWS CloudFormation and the instance type parameter value is not passed." } }, "AWS::SageMaker::UserProfile.ResourceSpec": { "attributes": {}, "description": "Specifies the ARN's of a SageMaker image and SageMaker image version, and the instance type that the version runs on.", "properties": { - "InstanceType": "The instance type that the image version runs on.", + "InstanceType": "The instance type that the image version runs on.\n\n> JupyterServer Apps only support the `system` value.", "SageMakerImageArn": "The ARN of the SageMaker image that the image version belongs to.", "SageMakerImageVersionArn": "The ARN of the image version created on the instance." } @@ -36481,14 +37306,14 @@ "AWS::SageMaker::Workteam": { "attributes": { "Ref": "", - "WorkteamName": "" + "WorkteamName": "The name of the work team." }, "description": "Creates a new work team for labeling your data. A work team is defined by one or more Amazon Cognito user pools. You must first create the user pools before you can create a work team.\n\nYou cannot create more than 25 work teams in an account and region.", "properties": { "Description": "A description of the work team.", "MemberDefinitions": "A list of `MemberDefinition` objects that contains objects that identify the workers that make up the work team.\n\nWorkforces can be created using Amazon Cognito or your own OIDC Identity Provider (IdP). For private workforces created using Amazon Cognito use `CognitoMemberDefinition` . For workforces created using your own OIDC identity provider (IdP) use `OidcMemberDefinition` .", "NotificationConfiguration": "Configures SNS notifications of available or expiring work items for work teams.", - "Tags": "", + "Tags": "An array of key-value pairs.", "WorkteamName": "The name of the work team." } }, @@ -36496,9 +37321,9 @@ "attributes": {}, "description": "Identifies a Amazon Cognito user group. A user group can be used in on or more work teams.", "properties": { - "CognitoClientId": "", - "CognitoUserGroup": "", - "CognitoUserPool": "" + "CognitoClientId": "An identifier for an application client. You must create the app client ID using Amazon Cognito.", + "CognitoUserGroup": "An identifier for a user group.", + "CognitoUserPool": "An identifier for a user pool. The user pool must be in the same region as the service that you are calling." } }, "AWS::SageMaker::Workteam.MemberDefinition": { @@ -36533,6 +37358,7 @@ "description": "Configures rotation for a secret. You must already configure the secret with the details of the database or service. If you define both the secret and the database or service in an AWS CloudFormation template, then define the [AWS::SecretsManager::SecretTargetAttachment](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secrettargetattachment.html) resource to populate the secret with the connection details of the database or service before you attempt to configure rotation.\n\n> When you configure rotation for a secret, AWS CloudFormation automatically rotates the secret one time.", "properties": { "HostedRotationLambda": "To use these values, you must specify `Transform: AWS::SecretsManager-2020-07-23` at the beginning of the CloudFormation template.\n\nWhen you enter valid values for `RotationSchedule.HostedRotationLambda` , Secrets Manager launches a Lambda that performs rotation on the secret specified in the `secret-id` property. The template creates a Lambda as part of a nested stack within the current stack.", + "RotateImmediatelyOnUpdate": "", "RotationLambdaARN": "The ARN of the Lambda function that can rotate the secret. If you don't specify this parameter, then the secret must already have the ARN of a Lambda function configured.\n\nTo reference a Lambda function also created in this template, use the [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) function with the function's logical ID.", "RotationRules": "A structure that defines the rotation configuration for this secret.", "SecretId": "The ARN or name of the secret to rotate.\n\nTo reference a secret also created in this template, use the [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) function with the secret's logical ID." @@ -36547,8 +37373,8 @@ "MasterSecretKmsKeyArn": "The ARN of the KMS key that Secrets Manager uses to encrypt the elevated secret if you use the [alternating users strategy](https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotating-secrets_strategies.html#rotating-secrets-two-users) . If you don't specify this value and you use the alternating users strategy, then Secrets Manager uses the key `aws/secretsmanager` . If `aws/secretsmanager` doesn't yet exist, then Secrets Manager creates it for you automatically the first time it encrypts the secret value.", "RotationLambdaName": "The name of the Lambda rotation function.", "RotationType": "The type of rotation template to use. For more information, see [Secrets Manager rotation function templates](https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_available-rotation-templates.html) .\n\nYou can specify one of the following `RotationTypes` :\n\n- MySQLSingleUser\n- MySQLMultiUser\n- PostgreSQLSingleUser\n- PostgreSQLMultiUser\n- OracleSingleUser\n- OracleMultiUser\n- MariaDBSingleUser\n- MariaDBMultiUser\n- SQLServerSingleUser\n- SQLServerMultiUser\n- RedshiftSingleUser\n- RedshiftMultiUser\n- MongoDBSingleUser\n- MongoDBMultiUser", - "SuperuserSecretArn": "", - "SuperuserSecretKmsKeyArn": "", + "SuperuserSecretArn": "The ARN of the secret that contains elevated credentials. The Lambda rotation function uses this secret for the [Alternating users rotation strategy](https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotating-secrets_strategies.html#rotating-secrets-two-users) .", + "SuperuserSecretKmsKeyArn": "The ARN of the KMS key that Secrets Manager uses to encrypt the elevated secret if you use the [alternating users strategy](https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotating-secrets_strategies.html#rotating-secrets-two-users) . If you don't specify this value and you use the alternating users strategy, then Secrets Manager uses the key `aws/secretsmanager` . If `aws/secretsmanager` doesn't yet exist, then Secrets Manager creates it for you automatically the first time it encrypts the secret value.", "VpcSecurityGroupIds": "A comma-separated list of security group IDs applied to the target database.\n\nThe templates applies the same security groups as on the Lambda rotation function that is created as part of this stack.", "VpcSubnetIds": "A comma separated list of VPC subnet IDs of the target database network. The Lambda rotation function is in the same subnet group." } @@ -36557,7 +37383,9 @@ "attributes": {}, "description": "A structure that defines the rotation configuration for the secret.", "properties": { - "AutomaticallyAfterDays": "Specifies the number of days between automatic scheduled rotations of the secret.\n\nSecrets Manager schedules the next rotation when the previous one is complete. Secrets Manager schedules the date by adding the rotation interval (number of days) to the actual date of the last rotation. The service chooses the hour within that 24-hour date window randomly. The minute is also chosen somewhat randomly, but weighted towards the top of the hour and influenced by a variety of factors that help distribute load." + "AutomaticallyAfterDays": "The number of days between automatic scheduled rotations of the secret. You can use this value to check that your secret meets your compliance guidelines for how often secrets must be rotated.\n\nIn `DescribeSecret` and `ListSecrets` , this value is calculated from the rotation schedule after every successful rotation. In `RotateSecret` , you can set the rotation schedule in `RotationRules` with `AutomaticallyAfterDays` or `ScheduleExpression` , but not both.", + "Duration": "", + "ScheduleExpression": "" } }, "AWS::SecretsManager::Secret": { @@ -36606,7 +37434,7 @@ "description": "The `AWS::SecretsManager::SecretTargetAttachment` resource completes the final link between a Secrets Manager secret and the associated database. This is required because each has a dependency on the other. No matter which one you create first, the other doesn't exist yet. To resolve this, you must create the resources in the following order:\n\n- Define the secret without referencing the service or database. You can't reference the service or database because it doesn't exist yet. The secret must contain a user name and password.\n- Next, define the service or database. Include the reference to the secret to use stored credentials to define the database admin user and password.\n- Finally, define a `SecretTargetAttachment` resource type to finish configuring the secret with the required database engine type and the connection details of the service or database. The rotation function requires the details, if you attach one later by defining a [AWS::SecretsManager::RotationSchedule](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-rotationschedule.html) resource type.", "properties": { "SecretId": "The ARN or name of the secret. To reference a secret also created in this template, use the see [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) function with the secret's logical ID.", - "TargetId": "The ARN of the database or cluster.", + "TargetId": "The ID of the database or cluster.", "TargetType": "A string that defines the type of service or database associated with the secret. This value instructs Secrets Manager how to update the secret with the details of the service or database. This value must be one of the following:\n\n- AWS::RDS::DBInstance\n- AWS::RDS::DBCluster\n- AWS::Redshift::Cluster\n- AWS::DocDB::DBInstance\n- AWS::DocDB::DBCluster" } }, @@ -36960,6 +37788,7 @@ "AWS::ServiceDiscovery::PrivateDnsNamespace": { "attributes": { "Arn": "The Amazon Resource Name (ARN) of the private namespace.", + "HostedZoneId": "", "Id": "The ID of the private namespace.", "Ref": "`Ref` returns the value of `Id` for the namespace, such as `ns-e4anhexample0004` ." }, @@ -36996,6 +37825,7 @@ "AWS::ServiceDiscovery::PublicDnsNamespace": { "attributes": { "Arn": "The Amazon Resource Name (ARN) of the public namespace.", + "HostedZoneId": "", "Id": "The ID of the public namespace.", "Ref": "`Ref` returns the value of `Id` for the namespace, such as `ns-e4anhexample0004` ." }, @@ -37434,6 +38264,7 @@ "description": "The CreateTable operation adds a new table to an existing database in your account. In an AWS account, table names must be at least unique within each Region if they are in the same database. You may have identical table names in the same Region if the tables are in separate databases. While creating the table, you must specify the table name, database name, and the retention properties. [Service quotas apply](https://docs.aws.amazon.com/timestream/latest/developerguide/ts-limits.html) . See [code sample](https://docs.aws.amazon.com/timestream/latest/developerguide/code-samples.create-table.html) for details.", "properties": { "DatabaseName": "The name of the Timestream database that contains this table.\n\n*Length Constraints* : Minimum length of 3 bytes. Maximum length of 256 bytes.", + "MagneticStoreWriteProperties": "Contains properties to set on the table when enabling magnetic store writes.\n\nThis object has the following attributes:\n\n- *EnableMagneticStoreWrites* : A `boolean` flag to enable magnetic store writes.\n- *MagneticStoreRejectedDataLocation* : The location to write error reports for records rejected, asynchronously, during magnetic store writes. Only `S3Configuration` objects are allowed. The `S3Configuration` object has the following attributes:\n\n- *BucketName* : The name of the S3 bucket.\n- *EncryptionOption* : The encryption option for the S3 location. Valid values are S3 server-side encryption with an S3 managed key ( `SSE_S3` ) or AWS managed key ( `SSE_KMS` ).\n- *KmsKeyId* : The AWS KMS key ID to use when encrypting with an AWS managed key.\n- *ObjectKeyPrefix* : The prefix to use option for the objects stored in S3.\n\nBoth `BucketName` and `EncryptionOption` are *required* when `S3Configuration` is specified. If you specify `SSE_KMS` as your `EncryptionOption` then `KmsKeyId` is *required* .\n\n`EnableMagneticStoreWrites` attribute is *required* when `MagneticStoreWriteProperties` is specified. `MagneticStoreRejectedDataLocation` attribute is *required* when `EnableMagneticStoreWrites` is set to `true` .\n\nSee the following examples:\n\n*JSON*\n\n```json\n{ \"Type\" : AWS::Timestream::Table\", \"Properties\":{ \"DatabaseName\":\"TestDatabase\", \"TableName\":\"TestTable\", \"MagneticStoreWriteProperties\":{ \"EnableMagneticStoreWrites\":true, \"MagneticStoreRejectedDataLocation\":{ \"S3Configuration\":{ \"BucketName\":\"testbucket\", \"EncryptionOption\":\"SSE_KMS\", \"KmsKeyId\":\"1234abcd-12ab-34cd-56ef-1234567890ab\", \"ObjectKeyPrefix\":\"prefix\" } } } }\n}\n```\n\n*YAML*\n\n```\nType: AWS::Timestream::Table\nDependsOn: TestDatabase\nProperties: TableName: \"TestTable\" DatabaseName: \"TestDatabase\" MagneticStoreWriteProperties: EnableMagneticStoreWrites: true MagneticStoreRejectedDataLocation: S3Configuration: BucketName: \"testbucket\" EncryptionOption: \"SSE_KMS\" BucketName: \"1234abcd-12ab-34cd-56ef-1234567890ab\" EncryptionOption: \"prefix\"\n```", "RetentionProperties": "The retention duration for the memory store and magnetic store. This object has the following attributes:\n\n- *MemoryStoreRetentionPeriodInHours* : Retention duration for memory store, in hours.\n- *MagneticStoreRetentionPeriodInDays* : Retention duration for magnetic store, in days.\n\nBoth attributes are of type `string` . Both attributes are *required* when `RetentionProperties` is specified.\n\nSee the following examples:\n\n*JSON*\n\n`{ \"Type\" : AWS::Timestream::Table\", \"Properties\" : { \"DatabaseName\" : \"TestDatabase\", \"TableName\" : \"TestTable\", \"RetentionProperties\" : { \"MemoryStoreRetentionPeriodInHours\": \"24\", \"MagneticStoreRetentionPeriodInDays\": \"7\" } } }` \n\n*YAML*\n\n```\nType: AWS::Timestream::Table\nDependsOn: TestDatabase\nProperties: TableName: \"TestTable\" DatabaseName: \"TestDatabase\" RetentionProperties: MemoryStoreRetentionPeriodInHours: \"24\" MagneticStoreRetentionPeriodInDays: \"7\"\n```", "TableName": "The name of the Timestream table.\n\n*Length Constraints* : Minimum length of 3 bytes. Maximum length of 256 bytes.", "Tags": "The tags to add to the table" @@ -37491,7 +38322,8 @@ "attributes": {}, "description": "Protocol settings that are configured for your server.", "properties": { - "PassiveIp": "Indicates passive mode, for FTP and FTPS protocols. Enter a single dotted-quad IPv4 address, such as the external IP address of a firewall, router, or load balancer." + "PassiveIp": "Indicates passive mode, for FTP and FTPS protocols. Enter a single dotted-quad IPv4 address, such as the external IP address of a firewall, router, or load balancer.", + "TlsSessionResumptionMode": "A property used with Transfer servers that use the FTPS protocol. TLS Session Resumption provides a mechanism to resume or share a negotiated secret key between the control and data connection for an FTPS session. `TlsSessionResumptionMode` determines whether or not the server resumes recent, negotiated sessions through a unique session ID. This property is available during `CreateServer` and `UpdateServer` calls. If a `TlsSessionResumptionMode` value is not specified during CreateServer, it is set to `ENFORCED` by default.\n\n- `DISABLED` : the server does not process TLS session resumption client requests and creates a new TLS session for each request.\n- `ENABLED` : the server processes and accepts clients that are performing TLS session resumption. The server doesn't reject client data connections that do not perform the TLS session resumption client processing.\n- `ENFORCED` : the server processes and accepts clients that are performing TLS session resumption. The server rejects client data connections that do not perform the TLS session resumption client processing. Before you set the value to `ENFORCED` , test your clients.\n\n> Not all FTPS clients perform TLS session resumption. So, if you choose to enforce TLS session resumption, you prevent any connections from FTPS clients that don't perform the protocol negotiation. To determine whether or not you can use the `ENFORCED` value, you need to test your clients." } }, "AWS::Transfer::Server.WorkflowDetail": { @@ -38005,7 +38837,7 @@ "ManagedByFirewallManager": "Indicates whether the logging configuration was created by AWS Firewall Manager , as part of an AWS WAF policy configuration. If true, only Firewall Manager can modify or delete the configuration.", "Ref": "`Ref` returns the Amazon Resource Name (ARN) of the web ACL." }, - "description": "Defines an association between logging destinations and a web ACL resource, for logging from AWS WAF . As part of the association, you can specify parts of the standard logging fields to keep out of the logs and you can specify filters so that you log only a subset of the logging records.\n\nFor information about configuring web ACL logging destinations, see [Logging web ACL traffic information](https://docs.aws.amazon.com/waf/latest/developerguide/logging.html) in the *AWS WAF Developer Guide* .", + "description": "Defines an association between logging destinations and a web ACL resource, for logging from AWS WAF . As part of the association, you can specify parts of the standard logging fields to keep out of the logs and you can specify filters so that you log only a subset of the logging records.\n\n> You can define one logging destination per web ACL. \n\nYou can access information about the traffic that AWS WAF inspects using the following steps:\n\n- Create your logging destination. You can use an Amazon CloudWatch Logs log group, an Amazon Simple Storage Service (Amazon S3) bucket, or an Amazon Kinesis Data Firehose. For information about configuring logging destinations and the permissions that are required for each, see [Logging web ACL traffic information](https://docs.aws.amazon.com/waf/latest/developerguide/logging.html) in the *AWS WAF Developer Guide* .\n- Associate your logging destination to your web ACL using a `PutLoggingConfiguration` request.\n\nWhen you successfully enable logging using a `PutLoggingConfiguration` request, AWS WAF creates an additional role or policy that is required to write logs to the logging destination. For an Amazon CloudWatch Logs log group, AWS WAF creates a resource policy on the log group. For an Amazon S3 bucket, AWS WAF creates a bucket policy. For an Amazon Kinesis Data Firehose, AWS WAF creates a service-linked role.\n\nFor additional information about web ACL logging, see [Logging web ACL traffic information](https://docs.aws.amazon.com/waf/latest/developerguide/logging.html) in the *AWS WAF Developer Guide* .", "properties": { "LogDestinationConfigs": "The Amazon Resource Names (ARNs) of the logging destinations that you want to associate with the web ACL.", "LoggingFilter": "Filtering that specifies which web requests are kept in the logs and which are dropped. You can filter on the rule action and on the web request labels that were applied by matching rules during web ACL evaluation.", @@ -38434,6 +39266,13 @@ "Name": "The name of the rule to exclude." } }, + "AWS::WAFv2::WebACL.FieldIdentifier": { + "attributes": {}, + "description": "The identifier of the username or password field, used in the `ManagedRuleGroupConfig` settings.", + "properties": { + "Identifier": "The name of the username or password field, used in the `ManagedRuleGroupConfig` settings.\n\nWhen the `PayloadType` is `JSON` , the identifier must be in JSON pointer syntax. For example `/form/username` . For information about the JSON Pointer syntax, see the Internet Engineering Task Force (IETF) documentation [JavaScript Object Notation (JSON) Pointer](https://docs.aws.amazon.com/https://tools.ietf.org/html/rfc6901) .\n\nWhen the `PayloadType` is `FORM_ENCODED` , use the HTML form names. For example, `username` ." + } + }, "AWS::WAFv2::WebACL.FieldToMatch": { "attributes": {}, "description": "The part of a web request that you want AWS WAF to inspect. Include the single `FieldToMatch` type that you want to inspect, with additional specifications as needed, according to the type. You specify a single request component in `FieldToMatch` for each rule statement that requires it. To inspect more than one component of a web request, create a separate rule statement for each component.", @@ -38520,11 +39359,22 @@ "Scope": "Specify whether you want to match using the label name or just the namespace." } }, + "AWS::WAFv2::WebACL.ManagedRuleGroupConfig": { + "attributes": {}, + "description": "Additional information that's used by a managed rule group. Most managed rule groups don't require this.\n\nUse this for the account takeover prevention managed rule group `AWSManagedRulesATPRuleSet` , to provide information about the sign-in page of your application.", + "properties": { + "LoginPath": "The path of the login endpoint for your application. For example, for the URL `https://example.com/web/login` , you would provide the path `/web/login` .", + "PasswordField": "Details about your login page password field.", + "PayloadType": "The payload type for your login endpoint, either JSON or form encoded.", + "UsernameField": "Details about your login page username field." + } + }, "AWS::WAFv2::WebACL.ManagedRuleGroupStatement": { "attributes": {}, "description": "A rule statement used to run the rules that are defined in a managed rule group. To use this, provide the vendor name and the name of the rule group in this statement.\n\nYou can't nest a `ManagedRuleGroupStatement` , for example for use inside a `NotStatement` or `OrStatement` . It can only be referenced as a top-level statement within a rule.", "properties": { "ExcludedRules": "The rules whose actions are set to `COUNT` by the web ACL, regardless of the action that is configured in the rule. This effectively excludes the rule from acting on web requests.", + "ManagedRuleGroupConfigs": "Additional information that's used by a managed rule group. Most managed rule groups don't require this.\n\nUse this for the account takeover prevention managed rule group `AWSManagedRulesATPRuleSet` , to provide information about the sign-in page of your application.", "Name": "The name of the managed rule group. You use this, along with the vendor name, to identify the rule group.", "ScopeDownStatement": "Statement nested inside a managed rule group statement to narrow the scope of the requests that AWS WAF evaluates using the rule group. Requests that match the scope-down statement are evaluated using the rule group. Requests that don't match the scope-down statement are not a match for the managed rule group statement, without any further evaluation.", "VendorName": "The name of the managed rule group vendor. You use this, along with the rule group name, to identify the rule group.", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ACMPCA.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ACMPCA.json index b04c9b991d117..537323fa5310e 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ACMPCA.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ACMPCA.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::ACMPCA::Certificate.ApiPassthrough": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-apipassthrough.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_APS.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_APS.json index 066fa41a16091..188318c027ea0 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_APS.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_APS.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": {}, "ResourceTypes": { "AWS::APS::RuleGroupsNamespace": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AccessAnalyzer.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AccessAnalyzer.json index 1c52130ca7172..a467a33442194 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AccessAnalyzer.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AccessAnalyzer.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::AccessAnalyzer::Analyzer.ArchiveRule": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-accessanalyzer-analyzer-archiverule.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AmazonMQ.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AmazonMQ.json index e410da44a289c..b3a9f17020ff4 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AmazonMQ.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AmazonMQ.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::AmazonMQ::Broker.ConfigurationId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-amazonmq-broker-configurationid.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Amplify.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Amplify.json index 6cba09d4006af..c09418d00b487 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Amplify.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Amplify.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Amplify::App.AutoBranchCreationConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-amplify-app-autobranchcreationconfig.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AmplifyUIBuilder.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AmplifyUIBuilder.json index e3459e1eab77b..00e82f58b0a24 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AmplifyUIBuilder.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AmplifyUIBuilder.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::AmplifyUIBuilder::Component.ComponentBindingPropertiesValue": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-amplifyuibuilder-component-componentbindingpropertiesvalue.html", @@ -175,13 +175,22 @@ } }, "AWS::AmplifyUIBuilder::Component.ComponentOverrides": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-amplifyuibuilder-component-componentoverrides.html" + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-amplifyuibuilder-component-componentoverrides.html", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" }, "AWS::AmplifyUIBuilder::Component.ComponentOverridesValue": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-amplifyuibuilder-component-componentoverridesvalue.html" + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-amplifyuibuilder-component-componentoverridesvalue.html", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" }, "AWS::AmplifyUIBuilder::Component.ComponentProperties": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-amplifyuibuilder-component-componentproperties.html" + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-amplifyuibuilder-component-componentproperties.html", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" }, "AWS::AmplifyUIBuilder::Component.ComponentProperty": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-amplifyuibuilder-component-componentproperty.html", @@ -302,10 +311,16 @@ } }, "AWS::AmplifyUIBuilder::Component.ComponentVariantValues": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-amplifyuibuilder-component-componentvariantvalues.html" + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-amplifyuibuilder-component-componentvariantvalues.html", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" }, "AWS::AmplifyUIBuilder::Component.FormBindings": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-amplifyuibuilder-component-formbindings.html" + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-amplifyuibuilder-component-formbindings.html", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" }, "AWS::AmplifyUIBuilder::Component.Predicate": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-amplifyuibuilder-component-predicate.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ApiGateway.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ApiGateway.json index f9d78d21cc5ed..7df30332a9b57 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ApiGateway.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ApiGateway.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::ApiGateway::ApiKey.StageKey": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-apikey-stagekey.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ApiGatewayV2.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ApiGatewayV2.json index 755ff079c3395..50cfce7f1aea9 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ApiGatewayV2.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ApiGatewayV2.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::ApiGatewayV2::Api.BodyS3Location": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigatewayv2-api-bodys3location.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppConfig.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppConfig.json index c1e2d83e71676..dd16afa936fbd 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppConfig.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppConfig.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::AppConfig::Application.Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appconfig-application-tags.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppFlow.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppFlow.json index 470bc338af602..d07a37b694e36 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppFlow.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppFlow.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::AppFlow::ConnectorProfile.AmplitudeConnectorProfileCredentials": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appflow-connectorprofile-amplitudeconnectorprofilecredentials.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppIntegrations.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppIntegrations.json index 12a5f564eb043..fd09492304583 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppIntegrations.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppIntegrations.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "53.1.0", "PropertyTypes": { "AWS::AppIntegrations::EventIntegration.EventFilter": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appintegrations-eventintegration-eventfilter.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppMesh.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppMesh.json index 213aea908c107..9b11ecbbb192b 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppMesh.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppMesh.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::AppMesh::GatewayRoute.GatewayRouteHostnameMatch": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appmesh-gatewayroute-gatewayroutehostnamematch.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppRunner.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppRunner.json index b1ff46dfc03fc..936be3fc26632 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppRunner.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppRunner.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::AppRunner::Service.AuthenticationConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-authenticationconfiguration.html", @@ -94,6 +94,23 @@ } } }, + "AWS::AppRunner::Service.EgressConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-egressconfiguration.html", + "Properties": { + "EgressType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-egressconfiguration.html#cfn-apprunner-service-egressconfiguration-egresstype", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "VpcConnectorArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-egressconfiguration.html#cfn-apprunner-service-egressconfiguration-vpcconnectorarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::AppRunner::Service.EncryptionConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-encryptionconfiguration.html", "Properties": { @@ -233,6 +250,17 @@ } } }, + "AWS::AppRunner::Service.NetworkConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-networkconfiguration.html", + "Properties": { + "EgressConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-networkconfiguration.html#cfn-apprunner-service-networkconfiguration-egressconfiguration", + "Required": true, + "Type": "EgressConfiguration", + "UpdateType": "Mutable" + } + } + }, "AWS::AppRunner::Service.SourceCodeVersion": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-sourcecodeversion.html", "Properties": { @@ -322,6 +350,12 @@ "Type": "InstanceConfiguration", "UpdateType": "Mutable" }, + "NetworkConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apprunner-service.html#cfn-apprunner-service-networkconfiguration", + "Required": false, + "Type": "NetworkConfiguration", + "UpdateType": "Mutable" + }, "ServiceName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apprunner-service.html#cfn-apprunner-service-servicename", "PrimitiveType": "String", @@ -342,6 +376,48 @@ "UpdateType": "Immutable" } } + }, + "AWS::AppRunner::VpcConnector": { + "Attributes": { + "VpcConnectorArn": { + "PrimitiveType": "String" + }, + "VpcConnectorRevision": { + "PrimitiveType": "Integer" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apprunner-vpcconnector.html", + "Properties": { + "SecurityGroups": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apprunner-vpcconnector.html#cfn-apprunner-vpcconnector-securitygroups", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "Subnets": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apprunner-vpcconnector.html#cfn-apprunner-vpcconnector-subnets", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": true, + "Type": "List", + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apprunner-vpcconnector.html#cfn-apprunner-vpcconnector-tags", + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "VpcConnectorName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apprunner-vpcconnector.html#cfn-apprunner-vpcconnector-vpcconnectorname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } } } } diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppStream.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppStream.json index 65267bb407da0..1ccf1eec68e48 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppStream.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppStream.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::AppStream::AppBlock.S3Location": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appstream-appblock-s3location.html", @@ -81,6 +81,23 @@ } } }, + "AWS::AppStream::Entitlement.Attribute": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appstream-entitlement-attribute.html", + "Properties": { + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appstream-entitlement-attribute.html#cfn-appstream-entitlement-attribute-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appstream-entitlement-attribute.html#cfn-appstream-entitlement-attribute-value", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::AppStream::Fleet.ComputeCapacity": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appstream-fleet-computecapacity.html", "Properties": { @@ -402,6 +419,29 @@ } } }, + "AWS::AppStream::ApplicationEntitlementAssociation": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appstream-applicationentitlementassociation.html", + "Properties": { + "ApplicationIdentifier": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appstream-applicationentitlementassociation.html#cfn-appstream-applicationentitlementassociation-applicationidentifier", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "EntitlementName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appstream-applicationentitlementassociation.html#cfn-appstream-applicationentitlementassociation-entitlementname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "StackName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appstream-applicationentitlementassociation.html#cfn-appstream-applicationentitlementassociation-stackname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, "AWS::AppStream::ApplicationFleetAssociation": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appstream-applicationfleetassociation.html", "Properties": { @@ -443,6 +483,51 @@ } } }, + "AWS::AppStream::Entitlement": { + "Attributes": { + "CreatedTime": { + "PrimitiveType": "String" + }, + "LastModifiedTime": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appstream-entitlement.html", + "Properties": { + "AppVisibility": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appstream-entitlement.html#cfn-appstream-entitlement-appvisibility", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Attributes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appstream-entitlement.html#cfn-appstream-entitlement-attributes", + "DuplicatesAllowed": false, + "ItemType": "Attribute", + "Required": true, + "Type": "List", + "UpdateType": "Mutable" + }, + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appstream-entitlement.html#cfn-appstream-entitlement-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appstream-entitlement.html#cfn-appstream-entitlement-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "StackName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appstream-entitlement.html#cfn-appstream-entitlement-stackname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, "AWS::AppStream::Fleet": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appstream-fleet.html", "Properties": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppSync.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppSync.json index 69766ae997e64..3b086620021dd 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppSync.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppSync.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::AppSync::DataSource.AuthorizationConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-datasource-authorizationconfig.html", @@ -424,7 +424,7 @@ "Ttl": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-resolver-cachingconfig.html#cfn-appsync-resolver-cachingconfig-ttl", "PrimitiveType": "Double", - "Required": false, + "Required": true, "UpdateType": "Mutable" } } @@ -731,6 +731,12 @@ "Required": true, "UpdateType": "Mutable" }, + "MaxBatchSize": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appsync-functionconfiguration.html#cfn-appsync-functionconfiguration-maxbatchsize", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appsync-functionconfiguration.html#cfn-appsync-functionconfiguration-name", "PrimitiveType": "String", @@ -906,6 +912,12 @@ "Required": false, "UpdateType": "Mutable" }, + "MaxBatchSize": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appsync-resolver.html#cfn-appsync-resolver-maxbatchsize", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, "PipelineConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appsync-resolver.html#cfn-appsync-resolver-pipelineconfig", "Required": false, diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ApplicationAutoScaling.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ApplicationAutoScaling.json index d7446ebdd23a1..e293c4ccc5b36 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ApplicationAutoScaling.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ApplicationAutoScaling.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::ApplicationAutoScaling::ScalableTarget.ScalableTargetAction": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationautoscaling-scalabletarget-scalabletargetaction.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ApplicationInsights.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ApplicationInsights.json index 8ba87de8c90d0..3e45ca2239297 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ApplicationInsights.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ApplicationInsights.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::ApplicationInsights::Application.Alarm": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationinsights-application-alarm.html", @@ -105,6 +105,18 @@ "Type": "List", "UpdateType": "Mutable" }, + "HAClusterPrometheusExporter": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationinsights-application-configurationdetails.html#cfn-applicationinsights-application-configurationdetails-haclusterprometheusexporter", + "Required": false, + "Type": "HAClusterPrometheusExporter", + "UpdateType": "Mutable" + }, + "HANAPrometheusExporter": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationinsights-application-configurationdetails.html#cfn-applicationinsights-application-configurationdetails-hanaprometheusexporter", + "Required": false, + "Type": "HANAPrometheusExporter", + "UpdateType": "Mutable" + }, "JMXPrometheusExporter": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationinsights-application-configurationdetails.html#cfn-applicationinsights-application-configurationdetails-jmxprometheusexporter", "Required": false, @@ -145,6 +157,52 @@ } } }, + "AWS::ApplicationInsights::Application.HAClusterPrometheusExporter": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationinsights-application-haclusterprometheusexporter.html", + "Properties": { + "PrometheusPort": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationinsights-application-haclusterprometheusexporter.html#cfn-applicationinsights-application-haclusterprometheusexporter-prometheusport", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::ApplicationInsights::Application.HANAPrometheusExporter": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationinsights-application-hanaprometheusexporter.html", + "Properties": { + "AgreeToInstallHANADBClient": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationinsights-application-hanaprometheusexporter.html#cfn-applicationinsights-application-hanaprometheusexporter-agreetoinstallhanadbclient", + "PrimitiveType": "Boolean", + "Required": true, + "UpdateType": "Mutable" + }, + "HANAPort": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationinsights-application-hanaprometheusexporter.html#cfn-applicationinsights-application-hanaprometheusexporter-hanaport", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "HANASID": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationinsights-application-hanaprometheusexporter.html#cfn-applicationinsights-application-hanaprometheusexporter-hanasid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "HANASecretName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationinsights-application-hanaprometheusexporter.html#cfn-applicationinsights-application-hanaprometheusexporter-hanasecretname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "PrometheusPort": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationinsights-application-hanaprometheusexporter.html#cfn-applicationinsights-application-hanaprometheusexporter-prometheusport", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::ApplicationInsights::Application.JMXPrometheusExporter": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationinsights-application-jmxprometheusexporter.html", "Properties": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Athena.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Athena.json index 5e3a904ad5a2a..94f0ebae32ad8 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Athena.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Athena.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Athena::WorkGroup.EncryptionConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-athena-workgroup-encryptionconfiguration.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AuditManager.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AuditManager.json index bc1a5d1b53989..148c421e3b3dc 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AuditManager.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AuditManager.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::AuditManager::Assessment.AWSAccount": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-auditmanager-assessment-awsaccount.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AutoScaling.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AutoScaling.json index 9c43095e9b581..252c1ca26a746 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AutoScaling.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AutoScaling.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::AutoScaling::AutoScalingGroup.AcceleratorCountRequest": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-autoscalinggroup-acceleratorcountrequest.html", @@ -520,101 +520,101 @@ } }, "AWS::AutoScaling::LaunchConfiguration.BlockDevice": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-template.html", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevice.html", "Properties": { "DeleteOnTermination": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-template.html#cfn-as-launchconfig-blockdev-template-deleteonterm", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevice.html#cfn-autoscaling-launchconfiguration-blockdevice-deleteontermination", "PrimitiveType": "Boolean", "Required": false, - "UpdateType": "Mutable" + "UpdateType": "Immutable" }, "Encrypted": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-template.html#cfn-as-launchconfig-blockdev-template-encrypted", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevice.html#cfn-autoscaling-launchconfiguration-blockdevice-encrypted", "PrimitiveType": "Boolean", "Required": false, - "UpdateType": "Mutable" + "UpdateType": "Immutable" }, "Iops": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-template.html#cfn-as-launchconfig-blockdev-template-iops", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevice.html#cfn-autoscaling-launchconfiguration-blockdevice-iops", "PrimitiveType": "Integer", "Required": false, - "UpdateType": "Mutable" + "UpdateType": "Immutable" }, "SnapshotId": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-template.html#cfn-as-launchconfig-blockdev-template-snapshotid", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevice.html#cfn-autoscaling-launchconfiguration-blockdevice-snapshotid", "PrimitiveType": "String", "Required": false, - "UpdateType": "Mutable" + "UpdateType": "Immutable" }, "Throughput": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-template.html#cfn-as-launchconfig-blockdev-template-throughput", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevice.html#cfn-autoscaling-launchconfiguration-blockdevice-throughput", "PrimitiveType": "Integer", "Required": false, - "UpdateType": "Mutable" + "UpdateType": "Immutable" }, "VolumeSize": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-template.html#cfn-as-launchconfig-blockdev-template-volumesize", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevice.html#cfn-autoscaling-launchconfiguration-blockdevice-volumesize", "PrimitiveType": "Integer", "Required": false, - "UpdateType": "Mutable" + "UpdateType": "Immutable" }, "VolumeType": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-template.html#cfn-as-launchconfig-blockdev-template-volumetype", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevice.html#cfn-autoscaling-launchconfiguration-blockdevice-volumetype", "PrimitiveType": "String", "Required": false, - "UpdateType": "Mutable" + "UpdateType": "Immutable" } } }, "AWS::AutoScaling::LaunchConfiguration.BlockDeviceMapping": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-mapping.html", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevicemapping.html", "Properties": { "DeviceName": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-mapping.html#cfn-as-launchconfig-blockdev-mapping-devicename", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevicemapping.html#cfn-autoscaling-launchconfiguration-blockdevicemapping-devicename", "PrimitiveType": "String", "Required": true, - "UpdateType": "Mutable" + "UpdateType": "Immutable" }, "Ebs": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-mapping.html#cfn-as-launchconfig-blockdev-mapping-ebs", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevicemapping.html#cfn-autoscaling-launchconfiguration-blockdevicemapping-ebs", "Required": false, "Type": "BlockDevice", - "UpdateType": "Mutable" + "UpdateType": "Immutable" }, "NoDevice": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-mapping.html#cfn-as-launchconfig-blockdev-mapping-nodevice", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevicemapping.html#cfn-autoscaling-launchconfiguration-blockdevicemapping-nodevice", "PrimitiveType": "Boolean", "Required": false, - "UpdateType": "Mutable" + "UpdateType": "Immutable" }, "VirtualName": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig-blockdev-mapping.html#cfn-as-launchconfig-blockdev-mapping-virtualname", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-blockdevicemapping.html#cfn-autoscaling-launchconfiguration-blockdevicemapping-virtualname", "PrimitiveType": "String", "Required": false, - "UpdateType": "Mutable" + "UpdateType": "Immutable" } } }, "AWS::AutoScaling::LaunchConfiguration.MetadataOptions": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfig-metadataoptions.html", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-metadataoptions.html", "Properties": { "HttpEndpoint": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfig-metadataoptions.html#cfn-autoscaling-launchconfig-metadataoptions-httpendpoint", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-metadataoptions.html#cfn-autoscaling-launchconfiguration-metadataoptions-httpendpoint", "PrimitiveType": "String", "Required": false, - "UpdateType": "Mutable" + "UpdateType": "Immutable" }, "HttpPutResponseHopLimit": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfig-metadataoptions.html#cfn-autoscaling-launchconfig-metadataoptions-httpputresponsehoplimit", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-metadataoptions.html#cfn-autoscaling-launchconfiguration-metadataoptions-httpputresponsehoplimit", "PrimitiveType": "Integer", "Required": false, - "UpdateType": "Mutable" + "UpdateType": "Immutable" }, "HttpTokens": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfig-metadataoptions.html#cfn-autoscaling-launchconfig-metadataoptions-httptokens", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-launchconfiguration-metadataoptions.html#cfn-autoscaling-launchconfiguration-metadataoptions-httptokens", "PrimitiveType": "String", "Required": false, - "UpdateType": "Mutable" + "UpdateType": "Immutable" } } }, @@ -857,6 +857,17 @@ "UpdateType": "Mutable" } } + }, + "AWS::AutoScaling::WarmPool.InstanceReusePolicy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-warmpool-instancereusepolicy.html", + "Properties": { + "ReuseOnScaleIn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-warmpool-instancereusepolicy.html#cfn-autoscaling-warmpool-instancereusepolicy-reuseonscalein", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + } + } } }, "ResourceTypes": { @@ -1063,16 +1074,16 @@ } }, "AWS::AutoScaling::LaunchConfiguration": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html", "Properties": { "AssociatePublicIpAddress": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cf-as-launchconfig-associatepubip", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-associatepublicipaddress", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Immutable" }, "BlockDeviceMappings": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-blockdevicemappings", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-blockdevicemappings", "DuplicatesAllowed": false, "ItemType": "BlockDeviceMapping", "Required": false, @@ -1080,107 +1091,105 @@ "UpdateType": "Immutable" }, "ClassicLinkVPCId": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-classiclinkvpcid", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-classiclinkvpcid", "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" }, "ClassicLinkVPCSecurityGroups": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-classiclinkvpcsecuritygroups", - "DuplicatesAllowed": false, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-classiclinkvpcsecuritygroups", "PrimitiveItemType": "String", "Required": false, "Type": "List", "UpdateType": "Immutable" }, "EbsOptimized": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-ebsoptimized", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-ebsoptimized", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Immutable" }, "IamInstanceProfile": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-iaminstanceprofile", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-iaminstanceprofile", "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" }, "ImageId": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-imageid", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-imageid", "PrimitiveType": "String", "Required": true, "UpdateType": "Immutable" }, "InstanceId": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-instanceid", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-instanceid", "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" }, "InstanceMonitoring": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-instancemonitoring", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-instancemonitoring", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Immutable" }, "InstanceType": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-instancetype", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-instancetype", "PrimitiveType": "String", "Required": true, "UpdateType": "Immutable" }, "KernelId": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-kernelid", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-kernelid", "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" }, "KeyName": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-keyname", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-keyname", "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" }, "LaunchConfigurationName": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-autoscaling-launchconfig-launchconfigurationname", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-launchconfigurationname", "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" }, "MetadataOptions": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-autoscaling-launchconfig-metadataoptions", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-metadataoptions", "Required": false, "Type": "MetadataOptions", "UpdateType": "Immutable" }, "PlacementTenancy": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-placementtenancy", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-placementtenancy", "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" }, "RamDiskId": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-ramdiskid", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-ramdiskid", "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" }, "SecurityGroups": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-securitygroups", - "DuplicatesAllowed": false, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-securitygroups", "PrimitiveItemType": "String", "Required": false, "Type": "List", "UpdateType": "Immutable" }, "SpotPrice": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-spotprice", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-spotprice", "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" }, "UserData": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-launchconfig.html#cfn-as-launchconfig-userdata", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html#cfn-autoscaling-launchconfiguration-userdata", "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" @@ -1375,6 +1384,12 @@ "Required": true, "UpdateType": "Immutable" }, + "InstanceReusePolicy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-warmpool.html#cfn-autoscaling-warmpool-instancereusepolicy", + "Required": false, + "Type": "InstanceReusePolicy", + "UpdateType": "Mutable" + }, "MaxGroupPreparedCapacity": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-warmpool.html#cfn-autoscaling-warmpool-maxgrouppreparedcapacity", "PrimitiveType": "Integer", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AutoScalingPlans.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AutoScalingPlans.json index aea4ab907b61b..2fe5b380dc94e 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AutoScalingPlans.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AutoScalingPlans.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::AutoScalingPlans::ScalingPlan.ApplicationSource": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscalingplans-scalingplan-applicationsource.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Backup.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Backup.json index 77977f9608c48..b464677915e0f 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Backup.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Backup.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Backup::BackupPlan.AdvancedBackupSettingResourceType": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-backup-backupplan-advancedbackupsettingresourcetype.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Batch.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Batch.json index 44101f2061be2..030b57376dddd 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Batch.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Batch.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Batch::ComputeEnvironment.ComputeResources": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-batch-computeenvironment-computeresources.html", @@ -24,6 +24,7 @@ }, "Ec2Configuration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-batch-computeenvironment-computeresources.html#cfn-batch-computeenvironment-computeresources-ec2configuration", + "DuplicatesAllowed": true, "ItemType": "Ec2ConfigurationObject", "Required": false, "Type": "List", @@ -49,6 +50,7 @@ }, "InstanceTypes": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-batch-computeenvironment-computeresources.html#cfn-batch-computeenvironment-computeresources-instancetypes", + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": false, "Type": "List", @@ -80,6 +82,7 @@ }, "SecurityGroupIds": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-batch-computeenvironment-computeresources.html#cfn-batch-computeenvironment-computeresources-securitygroupids", + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": false, "Type": "List", @@ -93,6 +96,7 @@ }, "Subnets": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-batch-computeenvironment-computeresources.html#cfn-batch-computeenvironment-computeresources-subnets", + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": true, "Type": "List", @@ -100,8 +104,9 @@ }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-batch-computeenvironment-computeresources.html#cfn-batch-computeenvironment-computeresources-tags", - "PrimitiveType": "Json", + "PrimitiveItemType": "String", "Required": false, + "Type": "Map", "UpdateType": "Immutable" }, "Type": { @@ -764,6 +769,11 @@ }, "ResourceTypes": { "AWS::Batch::ComputeEnvironment": { + "Attributes": { + "ComputeEnvironmentArn": { + "PrimitiveType": "String" + } + }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-batch-computeenvironment.html", "Properties": { "ComputeEnvironmentName": { @@ -792,8 +802,9 @@ }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-batch-computeenvironment.html#cfn-batch-computeenvironment-tags", - "PrimitiveType": "Json", + "PrimitiveItemType": "String", "Required": false, + "Type": "Map", "UpdateType": "Immutable" }, "Type": { @@ -883,10 +894,16 @@ } }, "AWS::Batch::JobQueue": { + "Attributes": { + "JobQueueArn": { + "PrimitiveType": "String" + } + }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-batch-jobqueue.html", "Properties": { "ComputeEnvironmentOrder": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-batch-jobqueue.html#cfn-batch-jobqueue-computeenvironmentorder", + "DuplicatesAllowed": true, "ItemType": "ComputeEnvironmentOrder", "Required": true, "Type": "List", @@ -918,8 +935,9 @@ }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-batch-jobqueue.html#cfn-batch-jobqueue-tags", - "PrimitiveType": "Json", + "PrimitiveItemType": "String", "Required": false, + "Type": "Map", "UpdateType": "Immutable" } } diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Budgets.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Budgets.json index c3a91feac2430..bc0181fd7cb71 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Budgets.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Budgets.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Budgets::Budget.BudgetData": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-budgets-budget-budgetdata.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CE.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CE.json index c845dd4d8257c..94f588735922a 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CE.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CE.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::CE::AnomalySubscription.Subscriber": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ce-anomalysubscription-subscriber.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CUR.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CUR.json index 96712fa1540db..0c34f22a9bef8 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CUR.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CUR.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": {}, "ResourceTypes": { "AWS::CUR::ReportDefinition": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Cassandra.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Cassandra.json index 6514b1e6c3502..b38156c872f07 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Cassandra.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Cassandra.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Cassandra::Table.BillingMode": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cassandra-table-billingmode.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CertificateManager.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CertificateManager.json index 3c0a283eab7bc..433c3209a53de 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CertificateManager.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CertificateManager.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::CertificateManager::Account.ExpiryEventsConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-certificatemanager-account-expiryeventsconfiguration.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Chatbot.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Chatbot.json index 551b2a5b07d9e..ce6ae472603e3 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Chatbot.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Chatbot.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": {}, "ResourceTypes": { "AWS::Chatbot::SlackChannelConfiguration": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Cloud9.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Cloud9.json index 3bfd4f76645eb..4b235db28f38f 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Cloud9.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Cloud9.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Cloud9::EnvironmentEC2.Repository": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloud9-environmentec2-repository.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CloudFormation.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CloudFormation.json index 4bd9d2c6f43ea..d685db09bf9ff 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CloudFormation.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CloudFormation.json @@ -1,6 +1,23 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { + "AWS::CloudFormation::HookVersion.LoggingConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudformation-hookversion-loggingconfig.html", + "Properties": { + "LogGroupName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudformation-hookversion-loggingconfig.html#cfn-cloudformation-hookversion-loggingconfig-loggroupname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "LogRoleArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudformation-hookversion-loggingconfig.html#cfn-cloudformation-hookversion-loggingconfig-logrolearn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, "AWS::CloudFormation::ResourceVersion.LoggingConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudformation-resourceversion-loggingconfig.html", "Properties": { @@ -173,6 +190,114 @@ } } }, + "AWS::CloudFormation::HookDefaultVersion": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-hookdefaultversion.html", + "Properties": { + "TypeName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-hookdefaultversion.html#cfn-cloudformation-hookdefaultversion-typename", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "TypeVersionArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-hookdefaultversion.html#cfn-cloudformation-hookdefaultversion-typeversionarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "VersionId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-hookdefaultversion.html#cfn-cloudformation-hookdefaultversion-versionid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::CloudFormation::HookTypeConfig": { + "Attributes": { + "ConfigurationArn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-hooktypeconfig.html", + "Properties": { + "Configuration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-hooktypeconfig.html#cfn-cloudformation-hooktypeconfig-configuration", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "ConfigurationAlias": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-hooktypeconfig.html#cfn-cloudformation-hooktypeconfig-configurationalias", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "TypeArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-hooktypeconfig.html#cfn-cloudformation-hooktypeconfig-typearn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "TypeName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-hooktypeconfig.html#cfn-cloudformation-hooktypeconfig-typename", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::CloudFormation::HookVersion": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "IsDefaultVersion": { + "PrimitiveType": "Boolean" + }, + "TypeArn": { + "PrimitiveType": "String" + }, + "VersionId": { + "PrimitiveType": "String" + }, + "Visibility": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-hookversion.html", + "Properties": { + "ExecutionRoleArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-hookversion.html#cfn-cloudformation-hookversion-executionrolearn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "LoggingConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-hookversion.html#cfn-cloudformation-hookversion-loggingconfig", + "Required": false, + "Type": "LoggingConfig", + "UpdateType": "Immutable" + }, + "SchemaHandlerPackage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-hookversion.html#cfn-cloudformation-hookversion-schemahandlerpackage", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "TypeName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-hookversion.html#cfn-cloudformation-hookversion-typename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, "AWS::CloudFormation::Macro": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-macro.html", "Properties": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CloudFront.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CloudFront.json index 58decc870c5e3..656deb3502c15 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CloudFront.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CloudFront.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::CloudFront::CachePolicy.CachePolicyConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-cachepolicy-cachepolicyconfig.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CloudTrail.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CloudTrail.json index f2c3e8cf57327..41ddfa657b264 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CloudTrail.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CloudTrail.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::CloudTrail::Trail.DataResource": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudtrail-trail-dataresource.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CloudWatch.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CloudWatch.json index b647823545234..a1f3d2218275c 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CloudWatch.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CloudWatch.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::CloudWatch::Alarm.Dimension": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cw-dimension.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeArtifact.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeArtifact.json index cd9fe58868013..6c308d14ff7ba 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeArtifact.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeArtifact.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": {}, "ResourceTypes": { "AWS::CodeArtifact::Domain": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeBuild.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeBuild.json index 2d80bb05dd971..a9e9c34ed0341 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeBuild.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeBuild.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::CodeBuild::Project.Artifacts": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-project-artifacts.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeCommit.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeCommit.json index 5bcf4d3267ffc..4793e8e82a3d8 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeCommit.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeCommit.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::CodeCommit::Repository.Code": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codecommit-repository-code.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeDeploy.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeDeploy.json index 8daa134b5db39..d81fdbe772c03 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeDeploy.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeDeploy.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::CodeDeploy::DeploymentConfig.MinimumHealthyHosts": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-minimumhealthyhosts.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeGuruProfiler.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeGuruProfiler.json index 01b9fa402ef2d..1635cbfbdd0bd 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeGuruProfiler.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeGuruProfiler.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::CodeGuruProfiler::ProfilingGroup.Channel": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codeguruprofiler-profilinggroup-channel.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeGuruReviewer.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeGuruReviewer.json index 262b69c5d8187..2f13015284503 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeGuruReviewer.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeGuruReviewer.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": {}, "ResourceTypes": { "AWS::CodeGuruReviewer::RepositoryAssociation": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodePipeline.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodePipeline.json index d57d7264a14e6..51ceeacfad4a9 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodePipeline.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodePipeline.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::CodePipeline::CustomActionType.ArtifactDetails": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codepipeline-customactiontype-artifactdetails.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeStar.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeStar.json index 34811ba5325e2..df7e36cafa60c 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeStar.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeStar.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::CodeStar::GitHubRepository.Code": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codestar-githubrepository-code.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeStarConnections.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeStarConnections.json index 335ce16afd430..58ae562a3e4fa 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeStarConnections.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeStarConnections.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": {}, "ResourceTypes": { "AWS::CodeStarConnections::Connection": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeStarNotifications.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeStarNotifications.json index 2374007281db7..a911558d09eb7 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeStarNotifications.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CodeStarNotifications.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::CodeStarNotifications::NotificationRule.Target": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codestarnotifications-notificationrule-target.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Cognito.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Cognito.json index 4eaeb23910578..b474fa1c95549 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Cognito.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Cognito.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Cognito::IdentityPool.CognitoIdentityProvider": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-identitypool-cognitoidentityprovider.html", @@ -509,6 +509,12 @@ "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" + }, + "SnsRegion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-smsconfiguration.html#cfn-cognito-userpool-smsconfiguration-snsregion", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" } } }, diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Config.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Config.json index f85295ef6bbae..1740f78e508e6 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Config.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Config.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Config::ConfigRule.Scope": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-config-configrule-scope.html", @@ -185,6 +185,80 @@ } } }, + "AWS::Config::OrganizationConfigRule.OrganizationCustomCodeRuleMetadata": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-config-organizationconfigrule-organizationcustomcoderulemetadata.html", + "Properties": { + "CodeText": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-config-organizationconfigrule-organizationcustomcoderulemetadata.html#cfn-config-organizationconfigrule-organizationcustomcoderulemetadata-codetext", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "DebugLogDeliveryAccounts": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-config-organizationconfigrule-organizationcustomcoderulemetadata.html#cfn-config-organizationconfigrule-organizationcustomcoderulemetadata-debuglogdeliveryaccounts", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-config-organizationconfigrule-organizationcustomcoderulemetadata.html#cfn-config-organizationconfigrule-organizationcustomcoderulemetadata-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "InputParameters": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-config-organizationconfigrule-organizationcustomcoderulemetadata.html#cfn-config-organizationconfigrule-organizationcustomcoderulemetadata-inputparameters", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "MaximumExecutionFrequency": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-config-organizationconfigrule-organizationcustomcoderulemetadata.html#cfn-config-organizationconfigrule-organizationcustomcoderulemetadata-maximumexecutionfrequency", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "OrganizationConfigRuleTriggerTypes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-config-organizationconfigrule-organizationcustomcoderulemetadata.html#cfn-config-organizationconfigrule-organizationcustomcoderulemetadata-organizationconfigruletriggertypes", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "ResourceIdScope": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-config-organizationconfigrule-organizationcustomcoderulemetadata.html#cfn-config-organizationconfigrule-organizationcustomcoderulemetadata-resourceidscope", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ResourceTypesScope": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-config-organizationconfigrule-organizationcustomcoderulemetadata.html#cfn-config-organizationconfigrule-organizationcustomcoderulemetadata-resourcetypesscope", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Runtime": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-config-organizationconfigrule-organizationcustomcoderulemetadata.html#cfn-config-organizationconfigrule-organizationcustomcoderulemetadata-runtime", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "TagKeyScope": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-config-organizationconfigrule-organizationcustomcoderulemetadata.html#cfn-config-organizationconfigrule-organizationcustomcoderulemetadata-tagkeyscope", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "TagValueScope": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-config-organizationconfigrule-organizationcustomcoderulemetadata.html#cfn-config-organizationconfigrule-organizationcustomcoderulemetadata-tagvaluescope", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::Config::OrganizationConfigRule.OrganizationCustomRuleMetadata": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-config-organizationconfigrule-organizationcustomrulemetadata.html", "Properties": { @@ -629,6 +703,12 @@ "Required": true, "UpdateType": "Immutable" }, + "OrganizationCustomCodeRuleMetadata": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-config-organizationconfigrule.html#cfn-config-organizationconfigrule-organizationcustomcoderulemetadata", + "Required": false, + "Type": "OrganizationCustomCodeRuleMetadata", + "UpdateType": "Mutable" + }, "OrganizationCustomRuleMetadata": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-config-organizationconfigrule.html#cfn-config-organizationconfigrule-organizationcustomrulemetadata", "Required": false, diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Connect.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Connect.json index 3cc5b84d16b49..513ca562655a1 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Connect.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Connect.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Connect::HoursOfOperation.HoursOfOperationConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-connect-hoursofoperation-hoursofoperationconfig.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CustomerProfiles.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CustomerProfiles.json index 161c0c9be0e0c..6837feffce48c 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CustomerProfiles.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CustomerProfiles.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::CustomerProfiles::Integration.ConnectorOperator": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-customerprofiles-integration-connectoroperator.html", @@ -100,6 +100,23 @@ } } }, + "AWS::CustomerProfiles::Integration.ObjectTypeMapping": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-customerprofiles-integration-objecttypemapping.html", + "Properties": { + "Key": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-customerprofiles-integration-objecttypemapping.html#cfn-customerprofiles-integration-objecttypemapping-key", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-customerprofiles-integration-objecttypemapping.html#cfn-customerprofiles-integration-objecttypemapping-value", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::CustomerProfiles::Integration.S3SourceProperties": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-customerprofiles-integration-s3sourceproperties.html", "Properties": { @@ -504,7 +521,14 @@ "ObjectTypeName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-customerprofiles-integration.html#cfn-customerprofiles-integration-objecttypename", "PrimitiveType": "String", - "Required": true, + "Required": false, + "UpdateType": "Mutable" + }, + "ObjectTypeNames": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-customerprofiles-integration.html#cfn-customerprofiles-integration-objecttypenames", + "ItemType": "ObjectTypeMapping", + "Required": false, + "Type": "List", "UpdateType": "Mutable" }, "Tags": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DAX.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DAX.json index 0976d67b62ddb..e583ea5409238 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DAX.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DAX.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::DAX::Cluster.SSESpecification": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dax-cluster-ssespecification.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DLM.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DLM.json index f2e70234d08ad..6fdc7417b7ad6 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DLM.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DLM.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::DLM::LifecyclePolicy.Action": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-action.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DMS.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DMS.json index 5276f2f91da55..27a47d082781c 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DMS.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DMS.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::DMS::Endpoint.DocDbSettings": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-docdbsettings.html", @@ -58,6 +58,89 @@ } } }, + "AWS::DMS::Endpoint.GcpMySQLSettings": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-gcpmysqlsettings.html", + "Properties": { + "AfterConnectScript": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-gcpmysqlsettings.html#cfn-dms-endpoint-gcpmysqlsettings-afterconnectscript", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "CleanSourceMetadataOnMismatch": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-gcpmysqlsettings.html#cfn-dms-endpoint-gcpmysqlsettings-cleansourcemetadataonmismatch", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "DatabaseName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-gcpmysqlsettings.html#cfn-dms-endpoint-gcpmysqlsettings-databasename", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "EventsPollInterval": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-gcpmysqlsettings.html#cfn-dms-endpoint-gcpmysqlsettings-eventspollinterval", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "MaxFileSize": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-gcpmysqlsettings.html#cfn-dms-endpoint-gcpmysqlsettings-maxfilesize", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "ParallelLoadThreads": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-gcpmysqlsettings.html#cfn-dms-endpoint-gcpmysqlsettings-parallelloadthreads", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "Password": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-gcpmysqlsettings.html#cfn-dms-endpoint-gcpmysqlsettings-password", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Port": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-gcpmysqlsettings.html#cfn-dms-endpoint-gcpmysqlsettings-port", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "SecretsManagerAccessRoleArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-gcpmysqlsettings.html#cfn-dms-endpoint-gcpmysqlsettings-secretsmanageraccessrolearn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "SecretsManagerSecretId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-gcpmysqlsettings.html#cfn-dms-endpoint-gcpmysqlsettings-secretsmanagersecretid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ServerName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-gcpmysqlsettings.html#cfn-dms-endpoint-gcpmysqlsettings-servername", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ServerTimezone": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-gcpmysqlsettings.html#cfn-dms-endpoint-gcpmysqlsettings-servertimezone", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Username": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-gcpmysqlsettings.html#cfn-dms-endpoint-gcpmysqlsettings-username", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::DMS::Endpoint.IbmDb2Settings": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-ibmdb2settings.html", "Properties": { @@ -506,6 +589,12 @@ "AWS::DMS::Endpoint.S3Settings": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html", "Properties": { + "AddColumnName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-addcolumnname", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "BucketFolder": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-bucketfolder", "PrimitiveType": "String", @@ -518,6 +607,42 @@ "Required": false, "UpdateType": "Mutable" }, + "CannedAclForObjects": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-cannedaclforobjects", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "CdcInsertsAndUpdates": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-cdcinsertsandupdates", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "CdcInsertsOnly": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-cdcinsertsonly", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "CdcMaxBatchInterval": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-cdcmaxbatchinterval", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "CdcMinFileSize": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-cdcminfilesize", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "CdcPath": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-cdcpath", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "CompressionType": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-compressiontype", "PrimitiveType": "String", @@ -530,23 +655,167 @@ "Required": false, "UpdateType": "Mutable" }, + "CsvNoSupValue": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-csvnosupvalue", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "CsvNullValue": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-csvnullvalue", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "CsvRowDelimiter": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-csvrowdelimiter", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" }, + "DataFormat": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-dataformat", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DataPageSize": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-datapagesize", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "DatePartitionDelimiter": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-datepartitiondelimiter", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DatePartitionEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-datepartitionenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "DatePartitionSequence": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-datepartitionsequence", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DatePartitionTimezone": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-datepartitiontimezone", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DictPageSizeLimit": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-dictpagesizelimit", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "EnableStatistics": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-enablestatistics", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "EncodingType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-encodingtype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "EncryptionMode": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-encryptionmode", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "ExternalTableDefinition": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-externaltabledefinition", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" }, + "IgnoreHeaderRows": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-ignoreheaderrows", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "IncludeOpForFullLoad": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-includeopforfullload", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "MaxFileSize": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-maxfilesize", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "ParquetTimestampInMillisecond": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-parquettimestampinmillisecond", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "ParquetVersion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-parquetversion", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "PreserveTransactions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-preservetransactions", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "Rfc4180": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-rfc4180", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "RowGroupLength": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-rowgrouplength", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "ServerSideEncryptionKmsKeyId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-serversideencryptionkmskeyid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "ServiceAccessRoleArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-serviceaccessrolearn", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" + }, + "TimestampColumnName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-timestampcolumnname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "UseCsvNoSupValue": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-usecsvnosupvalue", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "UseTaskStartTimeForFullLoadTimestamp": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html#cfn-dms-endpoint-s3settings-usetaskstarttimeforfullloadtimestamp", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" } } }, @@ -654,6 +923,12 @@ "Required": false, "UpdateType": "Mutable" }, + "GcpMySQLSettings": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dms-endpoint.html#cfn-dms-endpoint-gcpmysqlsettings", + "Required": false, + "Type": "GcpMySQLSettings", + "UpdateType": "Mutable" + }, "IbmDb2Settings": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dms-endpoint.html#cfn-dms-endpoint-ibmdb2settings", "Required": false, diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DataBrew.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DataBrew.json index caf8ded62e758..9cfbf6e3f0f0a 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DataBrew.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DataBrew.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::DataBrew::Dataset.CsvOptions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-dataset-csvoptions.html", @@ -586,6 +586,12 @@ "Required": true, "UpdateType": "Mutable" }, + "BucketOwner": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-outputlocation.html#cfn-databrew-job-outputlocation-bucketowner", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Key": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-outputlocation.html#cfn-databrew-job-outputlocation-key", "PrimitiveType": "String", @@ -595,7 +601,10 @@ } }, "AWS::DataBrew::Job.ParameterMap": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-parametermap.html" + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-parametermap.html", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" }, "AWS::DataBrew::Job.ProfileConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-profileconfiguration.html", @@ -654,6 +663,12 @@ "Required": true, "UpdateType": "Mutable" }, + "BucketOwner": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-s3location.html#cfn-databrew-job-s3location-bucketowner", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Key": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-s3location.html#cfn-databrew-job-s3location-key", "PrimitiveType": "String", @@ -813,7 +828,10 @@ } }, "AWS::DataBrew::Recipe.ParameterMap": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-recipe-parametermap.html" + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-recipe-parametermap.html", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" }, "AWS::DataBrew::Recipe.RecipeParameters": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-recipe-recipeparameters.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DataPipeline.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DataPipeline.json index 24401d66d6908..74f8c3801fba7 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DataPipeline.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DataPipeline.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::DataPipeline::Pipeline.Field": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-datapipeline-pipeline-pipelineobjects-fields.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DataSync.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DataSync.json index 47e807718de45..b32a5dfeb1953 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DataSync.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DataSync.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::DataSync::LocationEFS.Ec2Config": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-datasync-locationefs-ec2config.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Detective.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Detective.json index 6fbae0972f88d..01927748131b8 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Detective.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Detective.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": {}, "ResourceTypes": { "AWS::Detective::Graph": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DevOpsGuru.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DevOpsGuru.json index 729518e4850bc..470e295d84b22 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DevOpsGuru.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DevOpsGuru.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::DevOpsGuru::NotificationChannel.NotificationChannelConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-devopsguru-notificationchannel-notificationchannelconfig.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DirectoryService.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DirectoryService.json index 3460e06ec7729..dbbf2a161b115 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DirectoryService.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DirectoryService.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::DirectoryService::MicrosoftAD.VpcSettings": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-directoryservice-microsoftad-vpcsettings.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DocDB.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DocDB.json index e3ef6d0414a90..aefa025101844 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DocDB.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DocDB.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": {}, "ResourceTypes": { "AWS::DocDB::DBCluster": { @@ -32,6 +32,12 @@ "Required": false, "UpdateType": "Mutable" }, + "CopyTagsToSnapshot": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-docdb-dbcluster.html#cfn-docdb-dbcluster-copytagstosnapshot", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "DBClusterIdentifier": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-docdb-dbcluster.html#cfn-docdb-dbcluster-dbclusteridentifier", "PrimitiveType": "String", @@ -78,13 +84,13 @@ "MasterUserPassword": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-docdb-dbcluster.html#cfn-docdb-dbcluster-masteruserpassword", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "MasterUsername": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-docdb-dbcluster.html#cfn-docdb-dbcluster-masterusername", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Immutable" }, "Port": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DynamoDB.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DynamoDB.json index d948da8ac30fe..cb1fa0f2d9cf9 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DynamoDB.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_DynamoDB.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::DynamoDB::GlobalTable.AttributeDefinition": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-globaltable-attributedefinition.html", @@ -253,6 +253,12 @@ "Type": "ReplicaSSESpecification", "UpdateType": "Mutable" }, + "TableClass": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-globaltable-replicaspecification.html#cfn-dynamodb-globaltable-replicaspecification-tableclass", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-globaltable-replicaspecification.html#cfn-dynamodb-globaltable-replicaspecification-tags", "DuplicatesAllowed": false, diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EC2.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EC2.json index 029236531fb32..b6fd5af8ae437 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EC2.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EC2.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::EC2::CapacityReservation.TagSpecification": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-capacityreservation-tagspecification.html", @@ -142,6 +142,23 @@ } } }, + "AWS::EC2::ClientVpnEndpoint.ClientLoginBannerOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-clientvpnendpoint-clientloginbanneroptions.html", + "Properties": { + "BannerText": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-clientvpnendpoint-clientloginbanneroptions.html#cfn-ec2-clientvpnendpoint-clientloginbanneroptions-bannertext", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Enabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-clientvpnendpoint-clientloginbanneroptions.html#cfn-ec2-clientvpnendpoint-clientloginbanneroptions-enabled", + "PrimitiveType": "Boolean", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::EC2::ClientVpnEndpoint.ConnectionLogOptions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-clientvpnendpoint-connectionlogoptions.html", "Properties": { @@ -1154,6 +1171,29 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-instance-nodevice.html", "Properties": {} }, + "AWS::EC2::Instance.PrivateDnsNameOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-instance-privatednsnameoptions.html", + "Properties": { + "EnableResourceNameDnsAAAARecord": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-instance-privatednsnameoptions.html#cfn-ec2-instance-privatednsnameoptions-enableresourcenamednsaaaarecord", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "EnableResourceNameDnsARecord": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-instance-privatednsnameoptions.html#cfn-ec2-instance-privatednsnameoptions-enableresourcenamednsarecord", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "HostnameType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-instance-privatednsnameoptions.html#cfn-ec2-instance-privatednsnameoptions-hostnametype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::EC2::Instance.PrivateIpAddressSpecification": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-network-interface-privateipspec.html", "Properties": { @@ -1469,6 +1509,144 @@ } } }, + "AWS::EC2::LaunchTemplate.InstanceRequirements": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html", + "Properties": { + "AcceleratorCount": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-acceleratorcount", + "Required": false, + "Type": "AcceleratorCount", + "UpdateType": "Mutable" + }, + "AcceleratorManufacturers": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-acceleratormanufacturers", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "AcceleratorNames": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-acceleratornames", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "AcceleratorTotalMemoryMiB": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-acceleratortotalmemorymib", + "Required": false, + "Type": "AcceleratorTotalMemoryMiB", + "UpdateType": "Mutable" + }, + "AcceleratorTypes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-acceleratortypes", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "BareMetal": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-baremetal", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "BaselineEbsBandwidthMbps": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-baselineebsbandwidthmbps", + "Required": false, + "Type": "BaselineEbsBandwidthMbps", + "UpdateType": "Mutable" + }, + "BurstablePerformance": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-burstableperformance", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "CpuManufacturers": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-cpumanufacturers", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "ExcludedInstanceTypes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-excludedinstancetypes", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "InstanceGenerations": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-instancegenerations", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "LocalStorage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-localstorage", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "LocalStorageTypes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-localstoragetypes", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "MemoryGiBPerVCpu": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-memorygibpervcpu", + "Required": false, + "Type": "MemoryGiBPerVCpu", + "UpdateType": "Mutable" + }, + "MemoryMiB": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-memorymib", + "Required": false, + "Type": "MemoryMiB", + "UpdateType": "Mutable" + }, + "NetworkInterfaceCount": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-networkinterfacecount", + "Required": false, + "Type": "NetworkInterfaceCount", + "UpdateType": "Mutable" + }, + "OnDemandMaxPricePercentageOverLowestPrice": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-ondemandmaxpricepercentageoverlowestprice", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "RequireHibernateSupport": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-requirehibernatesupport", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "SpotMaxPricePercentageOverLowestPrice": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-spotmaxpricepercentageoverlowestprice", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "TotalLocalStorageGB": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-totallocalstoragegb", + "Required": false, + "Type": "TotalLocalStorageGB", + "UpdateType": "Mutable" + }, + "VCpuCount": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-instancerequirements.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements-vcpucount", + "Required": false, + "Type": "VCpuCount", + "UpdateType": "Mutable" + } + } + }, "AWS::EC2::LaunchTemplate.Ipv6Add": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-ipv6add.html", "Properties": { @@ -1570,6 +1748,12 @@ "Type": "InstanceMarketOptions", "UpdateType": "Mutable" }, + "InstanceRequirements": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata.html#cfn-ec2-launchtemplate-launchtemplatedata-instancerequirements", + "Required": false, + "Type": "InstanceRequirements", + "UpdateType": "Mutable" + }, "InstanceType": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata.html#cfn-ec2-launchtemplate-launchtemplatedata-instancetype", "PrimitiveType": "String", @@ -1620,6 +1804,12 @@ "Type": "Placement", "UpdateType": "Mutable" }, + "PrivateDnsNameOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata.html#cfn-ec2-launchtemplate-launchtemplatedata-privatednsnameoptions", + "Required": false, + "Type": "PrivateDnsNameOptions", + "UpdateType": "Mutable" + }, "RamDiskId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata.html#cfn-ec2-launchtemplate-launchtemplatedata-ramdiskid", "PrimitiveType": "String", @@ -1761,6 +1951,12 @@ "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" + }, + "InstanceMetadataTags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-metadataoptions.html#cfn-ec2-launchtemplate-launchtemplatedata-metadataoptions-instancemetadatatags", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" } } }, @@ -1943,6 +2139,29 @@ } } }, + "AWS::EC2::LaunchTemplate.PrivateDnsNameOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-privatednsnameoptions.html", + "Properties": { + "EnableResourceNameDnsAAAARecord": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-privatednsnameoptions.html#cfn-ec2-launchtemplate-launchtemplatedata-privatednsnameoptions-enableresourcenamednsaaaarecord", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "EnableResourceNameDnsARecord": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-privatednsnameoptions.html#cfn-ec2-launchtemplate-launchtemplatedata-privatednsnameoptions-enableresourcenamednsarecord", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "HostnameType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-privatednsnameoptions.html#cfn-ec2-launchtemplate-launchtemplatedata-privatednsnameoptions-hostnametype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::EC2::LaunchTemplate.PrivateIpAdd": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-privateipadd.html", "Properties": { @@ -2081,6 +2300,131 @@ } } }, + "AWS::EC2::NetworkInsightsAccessScope.AccessScopePathRequest": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsaccessscope-accessscopepathrequest.html", + "Properties": { + "Destination": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsaccessscope-accessscopepathrequest.html#cfn-ec2-networkinsightsaccessscope-accessscopepathrequest-destination", + "Required": false, + "Type": "PathStatementRequest", + "UpdateType": "Immutable" + }, + "Source": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsaccessscope-accessscopepathrequest.html#cfn-ec2-networkinsightsaccessscope-accessscopepathrequest-source", + "Required": false, + "Type": "PathStatementRequest", + "UpdateType": "Immutable" + }, + "ThroughResources": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsaccessscope-accessscopepathrequest.html#cfn-ec2-networkinsightsaccessscope-accessscopepathrequest-throughresources", + "ItemType": "ThroughResourcesStatementRequest", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + } + } + }, + "AWS::EC2::NetworkInsightsAccessScope.PacketHeaderStatementRequest": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsaccessscope-packetheaderstatementrequest.html", + "Properties": { + "DestinationAddresses": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsaccessscope-packetheaderstatementrequest.html#cfn-ec2-networkinsightsaccessscope-packetheaderstatementrequest-destinationaddresses", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "DestinationPorts": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsaccessscope-packetheaderstatementrequest.html#cfn-ec2-networkinsightsaccessscope-packetheaderstatementrequest-destinationports", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "DestinationPrefixLists": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsaccessscope-packetheaderstatementrequest.html#cfn-ec2-networkinsightsaccessscope-packetheaderstatementrequest-destinationprefixlists", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "Protocols": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsaccessscope-packetheaderstatementrequest.html#cfn-ec2-networkinsightsaccessscope-packetheaderstatementrequest-protocols", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "SourceAddresses": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsaccessscope-packetheaderstatementrequest.html#cfn-ec2-networkinsightsaccessscope-packetheaderstatementrequest-sourceaddresses", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "SourcePorts": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsaccessscope-packetheaderstatementrequest.html#cfn-ec2-networkinsightsaccessscope-packetheaderstatementrequest-sourceports", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "SourcePrefixLists": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsaccessscope-packetheaderstatementrequest.html#cfn-ec2-networkinsightsaccessscope-packetheaderstatementrequest-sourceprefixlists", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + } + } + }, + "AWS::EC2::NetworkInsightsAccessScope.PathStatementRequest": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsaccessscope-pathstatementrequest.html", + "Properties": { + "PacketHeaderStatement": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsaccessscope-pathstatementrequest.html#cfn-ec2-networkinsightsaccessscope-pathstatementrequest-packetheaderstatement", + "Required": false, + "Type": "PacketHeaderStatementRequest", + "UpdateType": "Immutable" + }, + "ResourceStatement": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsaccessscope-pathstatementrequest.html#cfn-ec2-networkinsightsaccessscope-pathstatementrequest-resourcestatement", + "Required": false, + "Type": "ResourceStatementRequest", + "UpdateType": "Immutable" + } + } + }, + "AWS::EC2::NetworkInsightsAccessScope.ResourceStatementRequest": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsaccessscope-resourcestatementrequest.html", + "Properties": { + "ResourceTypes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsaccessscope-resourcestatementrequest.html#cfn-ec2-networkinsightsaccessscope-resourcestatementrequest-resourcetypes", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "Resources": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsaccessscope-resourcestatementrequest.html#cfn-ec2-networkinsightsaccessscope-resourcestatementrequest-resources", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + } + } + }, + "AWS::EC2::NetworkInsightsAccessScope.ThroughResourcesStatementRequest": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsaccessscope-throughresourcesstatementrequest.html", + "Properties": { + "ResourceStatement": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsaccessscope-throughresourcesstatementrequest.html#cfn-ec2-networkinsightsaccessscope-throughresourcesstatementrequest-resourcestatement", + "Required": false, + "Type": "ResourceStatementRequest", + "UpdateType": "Immutable" + } + } + }, "AWS::EC2::NetworkInsightsAnalysis.AlternatePathHint": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinsightsanalysis-alternatepathhint.html", "Properties": { @@ -3343,6 +3687,12 @@ "Required": false, "UpdateType": "Immutable" }, + "Priority": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-spotfleet-launchtemplateoverrides.html#cfn-ec2-spotfleet-launchtemplateoverrides-priority", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Immutable" + }, "SpotPrice": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-spotfleet-launchtemplateoverrides.html#cfn-ec2-spotfleet-launchtemplateoverrides-spotprice", "PrimitiveType": "String", @@ -3849,6 +4199,29 @@ } } }, + "AWS::EC2::Subnet.PrivateDnsNameOptionsOnLaunch": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-subnet-privatednsnameoptionsonlaunch.html", + "Properties": { + "EnableResourceNameDnsAAAARecord": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-subnet-privatednsnameoptionsonlaunch.html#cfn-ec2-subnet-privatednsnameoptionsonlaunch-enableresourcenamednsaaaarecord", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "EnableResourceNameDnsARecord": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-subnet-privatednsnameoptionsonlaunch.html#cfn-ec2-subnet-privatednsnameoptionsonlaunch-enableresourcenamednsarecord", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "HostnameType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-subnet-privatednsnameoptionsonlaunch.html#cfn-ec2-subnet-privatednsnameoptionsonlaunch-hostnametype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::EC2::TrafficMirrorFilterRule.TrafficMirrorPortRange": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-trafficmirrorfilterrule-trafficmirrorportrange.html", "Properties": { @@ -4163,6 +4536,12 @@ "Type": "ClientConnectOptions", "UpdateType": "Mutable" }, + "ClientLoginBannerOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-clientvpnendpoint.html#cfn-ec2-clientvpnendpoint-clientloginbanneroptions", + "Required": false, + "Type": "ClientLoginBannerOptions", + "UpdateType": "Mutable" + }, "ConnectionLogOptions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-clientvpnendpoint.html#cfn-ec2-clientvpnendpoint-connectionlogoptions", "Required": true, @@ -4201,6 +4580,12 @@ "Required": true, "UpdateType": "Mutable" }, + "SessionTimeoutHours": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-clientvpnendpoint.html#cfn-ec2-clientvpnendpoint-sessiontimeouthours", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, "SplitTunnel": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-clientvpnendpoint.html#cfn-ec2-clientvpnendpoint-splittunnel", "PrimitiveType": "Boolean", @@ -4580,6 +4965,12 @@ "Required": false, "UpdateType": "Immutable" }, + "DestinationOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-flowlog.html#cfn-ec2-flowlog-destinationoptions", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Immutable" + }, "LogDestination": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-flowlog.html#cfn-ec2-flowlog-logdestination", "PrimitiveType": "String", @@ -4661,6 +5052,11 @@ } }, "AWS::EC2::Host": { + "Attributes": { + "HostId": { + "PrimitiveType": "String" + } + }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-host.html", "Properties": { "AutoPlacement": { @@ -4893,6 +5289,9 @@ "IpamScopeId": { "PrimitiveType": "String" }, + "IpamScopeType": { + "PrimitiveType": "String" + }, "IsDefault": { "PrimitiveType": "Boolean" }, @@ -4914,12 +5313,6 @@ "Required": true, "UpdateType": "Immutable" }, - "IpamScopeType": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipamscope.html#cfn-ec2-ipamscope-ipamscopetype", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Immutable" - }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipamscope.html#cfn-ec2-ipamscope-tags", "DuplicatesAllowed": false, @@ -5124,6 +5517,12 @@ "Required": false, "UpdateType": "Immutable" }, + "PrivateDnsNameOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-instance.html#cfn-ec2-instance-privatednsnameoptions", + "Required": false, + "Type": "PrivateDnsNameOptions", + "UpdateType": "Conditional" + }, "PrivateIpAddress": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-instance.html#cfn-ec2-instance-privateipaddress", "PrimitiveType": "String", @@ -5444,6 +5843,90 @@ } } }, + "AWS::EC2::NetworkInsightsAccessScope": { + "Attributes": { + "CreatedDate": { + "PrimitiveType": "String" + }, + "NetworkInsightsAccessScopeArn": { + "PrimitiveType": "String" + }, + "NetworkInsightsAccessScopeId": { + "PrimitiveType": "String" + }, + "UpdatedDate": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinsightsaccessscope.html", + "Properties": { + "ExcludePaths": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinsightsaccessscope.html#cfn-ec2-networkinsightsaccessscope-excludepaths", + "ItemType": "AccessScopePathRequest", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "MatchPaths": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinsightsaccessscope.html#cfn-ec2-networkinsightsaccessscope-matchpaths", + "ItemType": "AccessScopePathRequest", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinsightsaccessscope.html#cfn-ec2-networkinsightsaccessscope-tags", + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::EC2::NetworkInsightsAccessScopeAnalysis": { + "Attributes": { + "AnalyzedEniCount": { + "PrimitiveType": "Integer" + }, + "EndDate": { + "PrimitiveType": "String" + }, + "FindingsFound": { + "PrimitiveType": "String" + }, + "NetworkInsightsAccessScopeAnalysisArn": { + "PrimitiveType": "String" + }, + "NetworkInsightsAccessScopeAnalysisId": { + "PrimitiveType": "String" + }, + "StartDate": { + "PrimitiveType": "String" + }, + "Status": { + "PrimitiveType": "String" + }, + "StatusMessage": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinsightsaccessscopeanalysis.html", + "Properties": { + "NetworkInsightsAccessScopeId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinsightsaccessscopeanalysis.html#cfn-ec2-networkinsightsaccessscopeanalysis-networkinsightsaccessscopeid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinsightsaccessscopeanalysis.html#cfn-ec2-networkinsightsaccessscopeanalysis-tags", + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::EC2::NetworkInsightsAnalysis": { "Attributes": { "AlternatePathHints": { @@ -6117,17 +6600,35 @@ "Required": false, "UpdateType": "Immutable" }, + "AvailabilityZoneId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnet.html#cfn-ec2-subnet-availabilityzoneid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "CidrBlock": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnet.html#cfn-ec2-subnet-cidrblock", "PrimitiveType": "String", "Required": true, "UpdateType": "Immutable" }, + "EnableDns64": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnet.html#cfn-ec2-subnet-enabledns64", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "Ipv6CidrBlock": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnet.html#cfn-ec2-subnet-ipv6cidrblock", "PrimitiveType": "String", "Required": false, - "UpdateType": "Mutable" + "UpdateType": "Conditional" + }, + "Ipv6Native": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnet.html#cfn-ec2-subnet-ipv6native", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" }, "MapPublicIpOnLaunch": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnet.html#cfn-ec2-subnet-mappubliciponlaunch", @@ -6141,6 +6642,12 @@ "Required": false, "UpdateType": "Immutable" }, + "PrivateDnsNameOptionsOnLaunch": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnet.html#cfn-ec2-subnet-privatednsnameoptionsonlaunch", + "Required": false, + "Type": "PrivateDnsNameOptionsOnLaunch", + "UpdateType": "Mutable" + }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnet.html#cfn-ec2-subnet-tags", "DuplicatesAllowed": true, @@ -6956,6 +7463,18 @@ "Required": false, "UpdateType": "Mutable" }, + "Ipv4IpamPoolId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpc.html#cfn-ec2-vpc-ipv4ipampoolid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Ipv4NetmaskLength": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpc.html#cfn-ec2-vpc-ipv4netmasklength", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpc.html#cfn-aws-ec2-vpc-tags", "DuplicatesAllowed": true, @@ -6981,12 +7500,36 @@ "Required": false, "UpdateType": "Immutable" }, + "Ipv4IpamPoolId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpccidrblock.html#cfn-ec2-vpccidrblock-ipv4ipampoolid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Ipv4NetmaskLength": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpccidrblock.html#cfn-ec2-vpccidrblock-ipv4netmasklength", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + }, "Ipv6CidrBlock": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpccidrblock.html#cfn-ec2-vpccidrblock-ipv6cidrblock", "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" }, + "Ipv6IpamPoolId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpccidrblock.html#cfn-ec2-vpccidrblock-ipv6ipampoolid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Ipv6NetmaskLength": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpccidrblock.html#cfn-ec2-vpccidrblock-ipv6netmasklength", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + }, "Ipv6Pool": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpccidrblock.html#cfn-ec2-vpccidrblock-ipv6pool", "PrimitiveType": "String", @@ -7334,10 +7877,15 @@ } }, "AWS::EC2::VPNGatewayRoutePropagation": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpn-gatewayrouteprop.html", + "Attributes": { + "Id": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpngatewayroutepropagation.html", "Properties": { "RouteTableIds": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpn-gatewayrouteprop.html#cfn-ec2-vpngatewayrouteprop-routetableids", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpngatewayroutepropagation.html#cfn-ec2-vpngatewayroutepropagation-routetableids", "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": true, @@ -7345,7 +7893,7 @@ "UpdateType": "Mutable" }, "VpnGatewayId": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpn-gatewayrouteprop.html#cfn-ec2-vpngatewayrouteprop-vpngatewayid", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpngatewayroutepropagation.html#cfn-ec2-vpngatewayroutepropagation-vpngatewayid", "PrimitiveType": "String", "Required": true, "UpdateType": "Mutable" diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ECR.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ECR.json index 02fcf1110ec4d..0625883f82429 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ECR.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ECR.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::ECR::ReplicationConfiguration.ReplicationConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecr-replicationconfiguration-replicationconfiguration.html", @@ -149,6 +149,23 @@ } } }, + "AWS::ECR::PullThroughCacheRule": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecr-pullthroughcacherule.html", + "Properties": { + "EcrRepositoryPrefix": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecr-pullthroughcacherule.html#cfn-ecr-pullthroughcacherule-ecrrepositoryprefix", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "UpstreamRegistryUrl": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecr-pullthroughcacherule.html#cfn-ecr-pullthroughcacherule-upstreamregistryurl", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, "AWS::ECR::RegistryPolicy": { "Attributes": { "RegistryId": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ECS.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ECS.json index 4e28f618b223c..360d81e1b7142 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ECS.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ECS.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::ECS::CapacityProvider.AutoScalingGroupProvider": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-capacityprovider-autoscalinggroupprovider.html", @@ -756,7 +756,6 @@ "Properties": { "AuthorizationConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-taskdefinition-efsvolumeconfiguration.html#cfn-ecs-taskdefinition-efsvolumeconfiguration-authorizationconfig", - "PrimitiveType": "Json", "Required": false, "Type": "AuthorizationConfig", "UpdateType": "Immutable" diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EFS.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EFS.json index 203a10c8ecad9..6a259b4f46c9d 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EFS.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EFS.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::EFS::AccessPoint.AccessPointTag": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-efs-accesspoint-accesspointtag.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EKS.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EKS.json index e051d03d82d22..7548426c07313 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EKS.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EKS.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::EKS::Cluster.ClusterLogging": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-clusterlogging.html", @@ -183,6 +183,7 @@ }, "SourceSecurityGroups": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-nodegroup-remoteaccess.html#cfn-eks-nodegroup-remoteaccess-sourcesecuritygroups", + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": false, "Type": "List", @@ -195,19 +196,19 @@ "Properties": { "DesiredSize": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-nodegroup-scalingconfig.html#cfn-eks-nodegroup-scalingconfig-desiredsize", - "PrimitiveType": "Double", + "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "MaxSize": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-nodegroup-scalingconfig.html#cfn-eks-nodegroup-scalingconfig-maxsize", - "PrimitiveType": "Double", + "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "MinSize": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-nodegroup-scalingconfig.html#cfn-eks-nodegroup-scalingconfig-minsize", - "PrimitiveType": "Double", + "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" } @@ -440,6 +441,9 @@ "ClusterName": { "PrimitiveType": "String" }, + "Id": { + "PrimitiveType": "String" + }, "NodegroupName": { "PrimitiveType": "String" } @@ -466,7 +470,7 @@ }, "DiskSize": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-nodegroup.html#cfn-eks-nodegroup-disksize", - "PrimitiveType": "Double", + "PrimitiveType": "Integer", "Required": false, "UpdateType": "Immutable" }, @@ -478,6 +482,7 @@ }, "InstanceTypes": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-nodegroup.html#cfn-eks-nodegroup-instancetypes", + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": false, "Type": "List", @@ -527,6 +532,7 @@ }, "Subnets": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-nodegroup.html#cfn-eks-nodegroup-subnets", + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": true, "Type": "List", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EMR.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EMR.json index 1efac6ff55966..1432ef80ecf27 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EMR.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EMR.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::EMR::Cluster.Application": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticmapreduce-cluster-application.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EMRContainers.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EMRContainers.json index eaed9da59e333..673dd57298e8b 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EMRContainers.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EMRContainers.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::EMRContainers::VirtualCluster.ContainerInfo": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-emrcontainers-virtualcluster-containerinfo.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ElastiCache.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ElastiCache.json index b42c9124e8865..8f237e8d56d67 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ElastiCache.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ElastiCache.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::ElastiCache::CacheCluster.CloudWatchLogsDestinationDetails": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticache-cachecluster-cloudwatchlogsdestinationdetails.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ElasticBeanstalk.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ElasticBeanstalk.json index eeecbd98e2da7..e170d32a54a59 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ElasticBeanstalk.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ElasticBeanstalk.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::ElasticBeanstalk::Application.ApplicationResourceLifecycleConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticbeanstalk-application-applicationresourcelifecycleconfig.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ElasticLoadBalancing.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ElasticLoadBalancing.json index 5f1a7e6239094..cc2ae935f0d3d 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ElasticLoadBalancing.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ElasticLoadBalancing.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::ElasticLoadBalancing::LoadBalancer.AccessLoggingPolicy": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-elb-accessloggingpolicy.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ElasticLoadBalancingV2.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ElasticLoadBalancingV2.json index f69dbb286de7e..3313a50b6d7be 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ElasticLoadBalancingV2.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ElasticLoadBalancingV2.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::ElasticLoadBalancingV2::Listener.Action": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticloadbalancingv2-listener-action.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Elasticsearch.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Elasticsearch.json index af467b0f95f95..1e7702e95d714 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Elasticsearch.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Elasticsearch.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Elasticsearch::Domain.AdvancedSecurityOptionsInput": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticsearch-domain-advancedsecurityoptionsinput.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EventSchemas.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EventSchemas.json index 67a5730b33d03..3de5c94353450 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EventSchemas.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EventSchemas.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::EventSchemas::Discoverer.TagsEntry": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eventschemas-discoverer-tagsentry.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Events.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Events.json index c95cdfeb22e6a..bcd4745ad72a3 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Events.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Events.json @@ -1,6 +1,23 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { + "AWS::Events::EventBus.TagEntry": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-events-eventbus-tagentry.html", + "Properties": { + "Key": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-events-eventbus-tagentry.html#cfn-events-eventbus-tagentry-key", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-events-eventbus-tagentry.html#cfn-events-eventbus-tagentry-value", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::Events::EventBusPolicy.Condition": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-events-eventbuspolicy-condition.html", "Properties": { @@ -427,6 +444,36 @@ } } }, + "AWS::Events::Rule.SageMakerPipelineParameter": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-events-rule-sagemakerpipelineparameter.html", + "Properties": { + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-events-rule-sagemakerpipelineparameter.html#cfn-events-rule-sagemakerpipelineparameter-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-events-rule-sagemakerpipelineparameter.html#cfn-events-rule-sagemakerpipelineparameter-value", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::Events::Rule.SageMakerPipelineParameters": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-events-rule-sagemakerpipelineparameters.html", + "Properties": { + "PipelineParameterList": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-events-rule-sagemakerpipelineparameters.html#cfn-events-rule-sagemakerpipelineparameters-pipelineparameterlist", + "DuplicatesAllowed": false, + "ItemType": "SageMakerPipelineParameter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::Events::Rule.SqsParameters": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-events-rule-sqsparameters.html", "Properties": { @@ -542,6 +589,12 @@ "Type": "RunCommandParameters", "UpdateType": "Mutable" }, + "SageMakerPipelineParameters": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-events-rule-target.html#cfn-events-rule-target-sagemakerpipelineparameters", + "Required": false, + "Type": "SageMakerPipelineParameters", + "UpdateType": "Mutable" + }, "SqsParameters": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-events-rule-target.html#cfn-events-rule-target-sqsparameters", "Required": false, @@ -703,6 +756,13 @@ "PrimitiveType": "String", "Required": true, "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-eventbus.html#cfn-events-eventbus-tags", + "ItemType": "TagEntry", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" } } }, diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Evidently.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Evidently.json index 67002e39a1f7e..10858c925fda1 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Evidently.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Evidently.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Evidently::Experiment.MetricGoalObject": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-evidently-experiment-metricgoalobject.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_FIS.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_FIS.json index 9d7ddbe36cf2f..a909ce22be5bb 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_FIS.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_FIS.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::FIS::ExperimentTemplate.ExperimentTemplateAction": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fis-experimenttemplate-experimenttemplateaction.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_FMS.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_FMS.json index c571c6477ec36..566bf7e12abbe 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_FMS.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_FMS.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::FMS::Policy.IEMap": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fms-policy-iemap.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_FSx.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_FSx.json index 546d2cc9afd10..4adee3734f16f 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_FSx.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_FSx.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::FSx::FileSystem.AuditLogConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-windowsconfiguration-auditlogconfiguration.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_FinSpace.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_FinSpace.json index db128afd96461..b9bcd64a1cfd7 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_FinSpace.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_FinSpace.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::FinSpace::Environment.FederationParameters": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-finspace-environment-federationparameters.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Forecast.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Forecast.json new file mode 100644 index 0000000000000..87a156e573f83 --- /dev/null +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Forecast.json @@ -0,0 +1,95 @@ +{ + "$version": "57.0.0", + "PropertyTypes": {}, + "ResourceTypes": { + "AWS::Forecast::Dataset": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-forecast-dataset.html", + "Properties": { + "DataFrequency": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-forecast-dataset.html#cfn-forecast-dataset-datafrequency", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DatasetName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-forecast-dataset.html#cfn-forecast-dataset-datasetname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "DatasetType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-forecast-dataset.html#cfn-forecast-dataset-datasettype", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Domain": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-forecast-dataset.html#cfn-forecast-dataset-domain", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "EncryptionConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-forecast-dataset.html#cfn-forecast-dataset-encryptionconfig", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" + }, + "Schema": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-forecast-dataset.html#cfn-forecast-dataset-schema", + "PrimitiveType": "Json", + "Required": true, + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-forecast-dataset.html#cfn-forecast-dataset-tags", + "PrimitiveItemType": "Json", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::Forecast::DatasetGroup": { + "Attributes": { + "DatasetGroupArn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-forecast-datasetgroup.html", + "Properties": { + "DatasetArns": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-forecast-datasetgroup.html#cfn-forecast-datasetgroup-datasetarns", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "DatasetGroupName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-forecast-datasetgroup.html#cfn-forecast-datasetgroup-datasetgroupname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Domain": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-forecast-datasetgroup.html#cfn-forecast-datasetgroup-domain", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-forecast-datasetgroup.html#cfn-forecast-datasetgroup-tags", + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + } + } +} diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_FraudDetector.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_FraudDetector.json index 861094a8d1426..ee9f81d89ac31 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_FraudDetector.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_FraudDetector.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::FraudDetector::Detector.EntityType": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-frauddetector-detector-entitytype.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_GameLift.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_GameLift.json index aecfdc02cebe3..b95018ea7344b 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_GameLift.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_GameLift.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::GameLift::Alias.RoutingStrategy": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-gamelift-alias-routingstrategy.html", @@ -699,6 +699,13 @@ "Type": "PriorityConfiguration", "UpdateType": "Mutable" }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-gamelift-gamesessionqueue.html#cfn-gamelift-gamesessionqueue-tags", + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, "TimeoutInSeconds": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-gamelift-gamesessionqueue.html#cfn-gamelift-gamesessionqueue-timeoutinseconds", "PrimitiveType": "Integer", @@ -803,6 +810,13 @@ "PrimitiveType": "String", "Required": true, "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-gamelift-matchmakingconfiguration.html#cfn-gamelift-matchmakingconfiguration-tags", + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" } } }, @@ -828,6 +842,13 @@ "PrimitiveType": "String", "Required": true, "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-gamelift-matchmakingruleset.html#cfn-gamelift-matchmakingruleset-tags", + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" } } }, @@ -854,6 +875,13 @@ "Type": "S3Location", "UpdateType": "Mutable" }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-gamelift-script.html#cfn-gamelift-script-tags", + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, "Version": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-gamelift-script.html#cfn-gamelift-script-version", "PrimitiveType": "String", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_GlobalAccelerator.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_GlobalAccelerator.json index 297f342d67056..f38ae114ee3c3 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_GlobalAccelerator.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_GlobalAccelerator.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::GlobalAccelerator::EndpointGroup.EndpointConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-globalaccelerator-endpointgroup-endpointconfiguration.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Glue.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Glue.json index fd2957ddc0e39..5d227db144d95 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Glue.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Glue.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Glue::Classifier.CsvClassifier": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-classifier-csvclassifier.html", @@ -237,6 +237,23 @@ } } }, + "AWS::Glue::Crawler.MongoDBTarget": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-crawler-mongodbtarget.html", + "Properties": { + "ConnectionName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-crawler-mongodbtarget.html#cfn-glue-crawler-mongodbtarget-connectionname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Path": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-crawler-mongodbtarget.html#cfn-glue-crawler-mongodbtarget-path", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::Glue::Crawler.RecrawlPolicy": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-crawler-recrawlpolicy.html", "Properties": { @@ -257,6 +274,18 @@ "Required": false, "UpdateType": "Mutable" }, + "DlqEventQueueArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-crawler-s3target.html#cfn-glue-crawler-s3target-dlqeventqueuearn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "EventQueueArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-crawler-s3target.html#cfn-glue-crawler-s3target-eventqueuearn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Exclusions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-crawler-s3target.html#cfn-glue-crawler-s3target-exclusions", "PrimitiveItemType": "String", @@ -269,6 +298,12 @@ "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" + }, + "SampleSize": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-crawler-s3target.html#cfn-glue-crawler-s3target-samplesize", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" } } }, @@ -324,6 +359,13 @@ "Type": "List", "UpdateType": "Mutable" }, + "MongoDBTargets": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-crawler-targets.html#cfn-glue-crawler-targets-mongodbtargets", + "ItemType": "MongoDBTarget", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, "S3Targets": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-crawler-targets.html#cfn-glue-crawler-targets-s3targets", "ItemType": "S3Target", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Greengrass.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Greengrass.json index 0fa105153b10f..1f9bcced95e49 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Greengrass.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Greengrass.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Greengrass::ConnectorDefinition.Connector": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-greengrass-connectordefinition-connector.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_GreengrassV2.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_GreengrassV2.json index bdafce1241adb..ee5d83f4faa58 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_GreengrassV2.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_GreengrassV2.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::GreengrassV2::ComponentVersion.ComponentDependencyRequirement": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-greengrassv2-componentversion-componentdependencyrequirement.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_GroundStation.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_GroundStation.json index 0df34b7bb7808..547349c862d50 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_GroundStation.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_GroundStation.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::GroundStation::Config.AntennaDownlinkConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-groundstation-config-antennadownlinkconfig.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_GuardDuty.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_GuardDuty.json index ab6824be6e9ea..9d61bcc34f653 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_GuardDuty.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_GuardDuty.json @@ -1,9 +1,15 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::GuardDuty::Detector.CFNDataSourceConfigurations": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-guardduty-detector-cfndatasourceconfigurations.html", "Properties": { + "Kubernetes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-guardduty-detector-cfndatasourceconfigurations.html#cfn-guardduty-detector-cfndatasourceconfigurations-kubernetes", + "Required": false, + "Type": "CFNKubernetesConfiguration", + "UpdateType": "Mutable" + }, "S3Logs": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-guardduty-detector-cfndatasourceconfigurations.html#cfn-guardduty-detector-cfndatasourceconfigurations-s3logs", "Required": false, @@ -12,6 +18,28 @@ } } }, + "AWS::GuardDuty::Detector.CFNKubernetesAuditLogsConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-guardduty-detector-cfnkubernetesauditlogsconfiguration.html", + "Properties": { + "Enable": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-guardduty-detector-cfnkubernetesauditlogsconfiguration.html#cfn-guardduty-detector-cfnkubernetesauditlogsconfiguration-enable", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::GuardDuty::Detector.CFNKubernetesConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-guardduty-detector-cfnkubernetesconfiguration.html", + "Properties": { + "AuditLogs": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-guardduty-detector-cfnkubernetesconfiguration.html#cfn-guardduty-detector-cfnkubernetesconfiguration-auditlogs", + "Required": false, + "Type": "CFNKubernetesAuditLogsConfiguration", + "UpdateType": "Mutable" + } + } + }, "AWS::GuardDuty::Detector.CFNS3LogsConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-guardduty-detector-cfns3logsconfiguration.html", "Properties": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_HealthLake.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_HealthLake.json index a0f20a542876e..935d2b32a72b5 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_HealthLake.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_HealthLake.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::HealthLake::FHIRDatastore.KmsEncryptionConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-healthlake-fhirdatastore-kmsencryptionconfig.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IAM.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IAM.json index 60c268330a901..4db7fd63afb96 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IAM.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IAM.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::IAM::Group.Policy": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-policy.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IVS.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IVS.json index ab81fecb7c027..d4dba2943ea86 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IVS.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IVS.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::IVS::RecordingConfiguration.DestinationConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ivs-recordingconfiguration-destinationconfiguration.html", @@ -22,6 +22,23 @@ "UpdateType": "Immutable" } } + }, + "AWS::IVS::RecordingConfiguration.ThumbnailConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ivs-recordingconfiguration-thumbnailconfiguration.html", + "Properties": { + "RecordingMode": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ivs-recordingconfiguration-thumbnailconfiguration.html#cfn-ivs-recordingconfiguration-thumbnailconfiguration-recordingmode", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "TargetIntervalSeconds": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ivs-recordingconfiguration-thumbnailconfiguration.html#cfn-ivs-recordingconfiguration-thumbnailconfiguration-targetintervalseconds", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + } + } } }, "ResourceTypes": { @@ -142,6 +159,12 @@ "Required": false, "Type": "List", "UpdateType": "Mutable" + }, + "ThumbnailConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ivs-recordingconfiguration.html#cfn-ivs-recordingconfiguration-thumbnailconfiguration", + "Required": false, + "Type": "ThumbnailConfiguration", + "UpdateType": "Immutable" } } }, diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ImageBuilder.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ImageBuilder.json index 333abcdb0b80e..255dc2671549c 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ImageBuilder.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ImageBuilder.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::ImageBuilder::ContainerRecipe.ComponentConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-containerrecipe-componentconfiguration.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Inspector.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Inspector.json index a5e6752d83dd9..c01dcd13668e2 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Inspector.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Inspector.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": {}, "ResourceTypes": { "AWS::Inspector::AssessmentTarget": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_InspectorV2.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_InspectorV2.json new file mode 100644 index 0000000000000..fb4cf7605c533 --- /dev/null +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_InspectorV2.json @@ -0,0 +1,395 @@ +{ + "$version": "57.0.0", + "PropertyTypes": { + "AWS::InspectorV2::Filter.DateFilter": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-datefilter.html", + "Properties": { + "EndInclusive": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-datefilter.html#cfn-inspectorv2-filter-datefilter-endinclusive", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "StartInclusive": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-datefilter.html#cfn-inspectorv2-filter-datefilter-startinclusive", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::InspectorV2::Filter.FilterCriteria": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html", + "Properties": { + "AwsAccountId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-awsaccountid", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "ComponentId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-componentid", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "ComponentType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-componenttype", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Ec2InstanceImageId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-ec2instanceimageid", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Ec2InstanceSubnetId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-ec2instancesubnetid", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Ec2InstanceVpcId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-ec2instancevpcid", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "EcrImageArchitecture": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-ecrimagearchitecture", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "EcrImageHash": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-ecrimagehash", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "EcrImagePushedAt": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-ecrimagepushedat", + "ItemType": "DateFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "EcrImageRegistry": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-ecrimageregistry", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "EcrImageRepositoryName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-ecrimagerepositoryname", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "EcrImageTags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-ecrimagetags", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "FindingArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-findingarn", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "FindingStatus": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-findingstatus", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "FindingType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-findingtype", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "FirstObservedAt": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-firstobservedat", + "ItemType": "DateFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "InspectorScore": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-inspectorscore", + "ItemType": "NumberFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "LastObservedAt": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-lastobservedat", + "ItemType": "DateFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "NetworkProtocol": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-networkprotocol", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "PortRange": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-portrange", + "ItemType": "PortRangeFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "RelatedVulnerabilities": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-relatedvulnerabilities", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "ResourceId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-resourceid", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "ResourceTags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-resourcetags", + "ItemType": "MapFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "ResourceType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-resourcetype", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Severity": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-severity", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Title": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-title", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "UpdatedAt": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-updatedat", + "ItemType": "DateFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "VendorSeverity": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-vendorseverity", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "VulnerabilityId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-vulnerabilityid", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "VulnerabilitySource": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-vulnerabilitysource", + "ItemType": "StringFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "VulnerablePackages": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-filtercriteria.html#cfn-inspectorv2-filter-filtercriteria-vulnerablepackages", + "ItemType": "PackageFilter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::InspectorV2::Filter.MapFilter": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-mapfilter.html", + "Properties": { + "Comparison": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-mapfilter.html#cfn-inspectorv2-filter-mapfilter-comparison", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Key": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-mapfilter.html#cfn-inspectorv2-filter-mapfilter-key", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-mapfilter.html#cfn-inspectorv2-filter-mapfilter-value", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::InspectorV2::Filter.NumberFilter": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-numberfilter.html", + "Properties": { + "LowerInclusive": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-numberfilter.html#cfn-inspectorv2-filter-numberfilter-lowerinclusive", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Mutable" + }, + "UpperInclusive": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-numberfilter.html#cfn-inspectorv2-filter-numberfilter-upperinclusive", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::InspectorV2::Filter.PackageFilter": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-packagefilter.html", + "Properties": { + "Architecture": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-packagefilter.html#cfn-inspectorv2-filter-packagefilter-architecture", + "Required": false, + "Type": "StringFilter", + "UpdateType": "Mutable" + }, + "Epoch": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-packagefilter.html#cfn-inspectorv2-filter-packagefilter-epoch", + "Required": false, + "Type": "NumberFilter", + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-packagefilter.html#cfn-inspectorv2-filter-packagefilter-name", + "Required": false, + "Type": "StringFilter", + "UpdateType": "Mutable" + }, + "Release": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-packagefilter.html#cfn-inspectorv2-filter-packagefilter-release", + "Required": false, + "Type": "StringFilter", + "UpdateType": "Mutable" + }, + "SourceLayerHash": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-packagefilter.html#cfn-inspectorv2-filter-packagefilter-sourcelayerhash", + "Required": false, + "Type": "StringFilter", + "UpdateType": "Mutable" + }, + "Version": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-packagefilter.html#cfn-inspectorv2-filter-packagefilter-version", + "Required": false, + "Type": "StringFilter", + "UpdateType": "Mutable" + } + } + }, + "AWS::InspectorV2::Filter.PortRangeFilter": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-portrangefilter.html", + "Properties": { + "BeginInclusive": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-portrangefilter.html#cfn-inspectorv2-filter-portrangefilter-begininclusive", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "EndInclusive": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-portrangefilter.html#cfn-inspectorv2-filter-portrangefilter-endinclusive", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::InspectorV2::Filter.StringFilter": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-stringfilter.html", + "Properties": { + "Comparison": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-stringfilter.html#cfn-inspectorv2-filter-stringfilter-comparison", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-inspectorv2-filter-stringfilter.html#cfn-inspectorv2-filter-stringfilter-value", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + } + }, + "ResourceTypes": { + "AWS::InspectorV2::Filter": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-inspectorv2-filter.html", + "Properties": { + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-inspectorv2-filter.html#cfn-inspectorv2-filter-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "FilterAction": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-inspectorv2-filter.html#cfn-inspectorv2-filter-filteraction", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "FilterCriteria": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-inspectorv2-filter.html#cfn-inspectorv2-filter-filtercriteria", + "Required": true, + "Type": "FilterCriteria", + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-inspectorv2-filter.html#cfn-inspectorv2-filter-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + } + } +} diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoT.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoT.json index ab0a274500ab1..f6bed623c55a0 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoT.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoT.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::IoT::AccountAuditConfiguration.AuditCheckConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditcheckconfiguration.html", @@ -1615,6 +1615,12 @@ "Required": false, "UpdateType": "Immutable" }, + "EnableCachingForHttp": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-authorizer.html#cfn-iot-authorizer-enablecachingforhttp", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "SigningDisabled": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-authorizer.html#cfn-iot-authorizer-signingdisabled", "PrimitiveType": "Boolean", @@ -1952,6 +1958,12 @@ "Required": false, "UpdateType": "Immutable" }, + "JobExecutionsRetryConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-jobtemplate.html#cfn-iot-jobtemplate-jobexecutionsretryconfig", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" + }, "JobExecutionsRolloutConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-jobtemplate.html#cfn-iot-jobtemplate-jobexecutionsrolloutconfig", "PrimitiveType": "Json", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoT1Click.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoT1Click.json index 68c4bd9063204..064eb3dd65d28 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoT1Click.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoT1Click.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::IoT1Click::Project.DeviceTemplate": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot1click-project-devicetemplate.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTAnalytics.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTAnalytics.json index 08043644d063e..ded9a89820bc3 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTAnalytics.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTAnalytics.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::IoTAnalytics::Channel.ChannelStorage": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-channel-channelstorage.html", @@ -60,7 +60,9 @@ }, "AWS::IoTAnalytics::Channel.ServiceManagedS3": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-channel-servicemanageds3.html", - "Properties": {} + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" }, "AWS::IoTAnalytics::Dataset.Action": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-action.html", @@ -108,6 +110,7 @@ }, "Variables": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-containeraction.html#cfn-iotanalytics-dataset-containeraction-variables", + "DuplicatesAllowed": true, "ItemType": "Variable", "Required": false, "Type": "List", @@ -150,12 +153,12 @@ } }, "AWS::IoTAnalytics::Dataset.DatasetContentVersionValue": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-variable-datasetcontentversionvalue.html", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-datasetcontentversionvalue.html", "Properties": { "DatasetName": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-variable-datasetcontentversionvalue.html#cfn-iotanalytics-dataset-variable-datasetcontentversionvalue-datasetname", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-datasetcontentversionvalue.html#cfn-iotanalytics-dataset-datasetcontentversionvalue-datasetname", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" } } @@ -262,12 +265,12 @@ } }, "AWS::IoTAnalytics::Dataset.OutputFileUriValue": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-variable-outputfileurivalue.html", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-outputfileurivalue.html", "Properties": { "FileName": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-variable-outputfileurivalue.html#cfn-iotanalytics-dataset-variable-outputfileurivalue-filename", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-outputfileurivalue.html#cfn-iotanalytics-dataset-outputfileurivalue-filename", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" } } @@ -277,6 +280,7 @@ "Properties": { "Filters": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-queryaction.html#cfn-iotanalytics-dataset-queryaction-filters", + "DuplicatesAllowed": true, "ItemType": "Filter", "Required": false, "Type": "List", @@ -313,13 +317,13 @@ "NumberOfDays": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-retentionperiod.html#cfn-iotanalytics-dataset-retentionperiod-numberofdays", "PrimitiveType": "Integer", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Unlimited": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-retentionperiod.html#cfn-iotanalytics-dataset-retentionperiod-unlimited", "PrimitiveType": "Boolean", - "Required": true, + "Required": false, "UpdateType": "Mutable" } } @@ -354,10 +358,10 @@ } }, "AWS::IoTAnalytics::Dataset.Schedule": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-trigger-schedule.html", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-schedule.html", "Properties": { "ScheduleExpression": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-trigger-schedule.html#cfn-iotanalytics-dataset-trigger-schedule-scheduleexpression", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-schedule.html#cfn-iotanalytics-dataset-schedule-scheduleexpression", "PrimitiveType": "String", "Required": true, "UpdateType": "Mutable" @@ -523,6 +527,7 @@ "Properties": { "Partitions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-datastorepartitions.html#cfn-iotanalytics-datastore-datastorepartitions-partitions", + "DuplicatesAllowed": true, "ItemType": "DatastorePartition", "Required": false, "Type": "List", @@ -575,7 +580,7 @@ "Properties": { "CustomerManagedS3Storage": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-iotsitewisemultilayerstorage.html#cfn-iotanalytics-datastore-iotsitewisemultilayerstorage-customermanageds3storage", - "Required": true, + "Required": false, "Type": "CustomerManagedS3Storage", "UpdateType": "Mutable" } @@ -583,7 +588,9 @@ }, "AWS::IoTAnalytics::Datastore.JsonConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-jsonconfiguration.html", - "Properties": {} + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" }, "AWS::IoTAnalytics::Datastore.ParquetConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-parquetconfiguration.html", @@ -629,6 +636,7 @@ "Properties": { "Columns": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-schemadefinition.html#cfn-iotanalytics-datastore-schemadefinition-columns", + "DuplicatesAllowed": true, "ItemType": "Column", "Required": false, "Type": "List", @@ -638,7 +646,9 @@ }, "AWS::IoTAnalytics::Datastore.ServiceManagedS3": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-servicemanageds3.html", - "Properties": {} + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" }, "AWS::IoTAnalytics::Datastore.TimestampPartition": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-timestamppartition.html", @@ -727,14 +737,15 @@ "Properties": { "Attributes": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-addattributes.html#cfn-iotanalytics-pipeline-addattributes-attributes", - "PrimitiveType": "Json", - "Required": false, + "PrimitiveItemType": "String", + "Required": true, + "Type": "Map", "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-addattributes.html#cfn-iotanalytics-pipeline-addattributes-name", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Next": { @@ -751,13 +762,13 @@ "ChannelName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-channel.html#cfn-iotanalytics-pipeline-channel-channelname", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-channel.html#cfn-iotanalytics-pipeline-channel-name", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Next": { @@ -774,13 +785,13 @@ "DatastoreName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-datastore.html#cfn-iotanalytics-pipeline-datastore-datastorename", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-datastore.html#cfn-iotanalytics-pipeline-datastore-name", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" } } @@ -791,13 +802,13 @@ "Attribute": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-deviceregistryenrich.html#cfn-iotanalytics-pipeline-deviceregistryenrich-attribute", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-deviceregistryenrich.html#cfn-iotanalytics-pipeline-deviceregistryenrich-name", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Next": { @@ -809,13 +820,13 @@ "RoleArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-deviceregistryenrich.html#cfn-iotanalytics-pipeline-deviceregistryenrich-rolearn", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "ThingName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-deviceregistryenrich.html#cfn-iotanalytics-pipeline-deviceregistryenrich-thingname", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" } } @@ -826,13 +837,13 @@ "Attribute": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-deviceshadowenrich.html#cfn-iotanalytics-pipeline-deviceshadowenrich-attribute", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-deviceshadowenrich.html#cfn-iotanalytics-pipeline-deviceshadowenrich-name", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Next": { @@ -844,13 +855,13 @@ "RoleArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-deviceshadowenrich.html#cfn-iotanalytics-pipeline-deviceshadowenrich-rolearn", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "ThingName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-deviceshadowenrich.html#cfn-iotanalytics-pipeline-deviceshadowenrich-thingname", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" } } @@ -861,13 +872,13 @@ "Filter": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-filter.html#cfn-iotanalytics-pipeline-filter-filter", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-filter.html#cfn-iotanalytics-pipeline-filter-name", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Next": { @@ -884,19 +895,19 @@ "BatchSize": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-lambda.html#cfn-iotanalytics-pipeline-lambda-batchsize", "PrimitiveType": "Integer", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "LambdaName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-lambda.html#cfn-iotanalytics-pipeline-lambda-lambdaname", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-lambda.html#cfn-iotanalytics-pipeline-lambda-name", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Next": { @@ -913,19 +924,19 @@ "Attribute": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-math.html#cfn-iotanalytics-pipeline-math-attribute", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Math": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-math.html#cfn-iotanalytics-pipeline-math-math", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-math.html#cfn-iotanalytics-pipeline-math-name", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Next": { @@ -943,14 +954,14 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-removeattributes.html#cfn-iotanalytics-pipeline-removeattributes-attributes", "DuplicatesAllowed": true, "PrimitiveItemType": "String", - "Required": false, + "Required": true, "Type": "List", "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-removeattributes.html#cfn-iotanalytics-pipeline-removeattributes-name", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Next": { @@ -968,14 +979,14 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-selectattributes.html#cfn-iotanalytics-pipeline-selectattributes-attributes", "DuplicatesAllowed": true, "PrimitiveItemType": "String", - "Required": false, + "Required": true, "Type": "List", "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-selectattributes.html#cfn-iotanalytics-pipeline-selectattributes-name", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Next": { @@ -989,6 +1000,11 @@ }, "ResourceTypes": { "AWS::IoTAnalytics::Channel": { + "Attributes": { + "Id": { + "PrimitiveType": "String" + } + }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-channel.html", "Properties": { "ChannelName": { @@ -1011,6 +1027,7 @@ }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-channel.html#cfn-iotanalytics-channel-tags", + "DuplicatesAllowed": true, "ItemType": "Tag", "Required": false, "Type": "List", @@ -1019,10 +1036,16 @@ } }, "AWS::IoTAnalytics::Dataset": { + "Attributes": { + "Id": { + "PrimitiveType": "String" + } + }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-dataset.html", "Properties": { "Actions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-dataset.html#cfn-iotanalytics-dataset-actions", + "DuplicatesAllowed": true, "ItemType": "Action", "Required": true, "Type": "List", @@ -1030,6 +1053,7 @@ }, "ContentDeliveryRules": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-dataset.html#cfn-iotanalytics-dataset-contentdeliveryrules", + "DuplicatesAllowed": true, "ItemType": "DatasetContentDeliveryRule", "Required": false, "Type": "List", @@ -1043,6 +1067,7 @@ }, "LateDataRules": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-dataset.html#cfn-iotanalytics-dataset-latedatarules", + "DuplicatesAllowed": true, "ItemType": "LateDataRule", "Required": false, "Type": "List", @@ -1056,6 +1081,7 @@ }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-dataset.html#cfn-iotanalytics-dataset-tags", + "DuplicatesAllowed": true, "ItemType": "Tag", "Required": false, "Type": "List", @@ -1063,6 +1089,7 @@ }, "Triggers": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-dataset.html#cfn-iotanalytics-dataset-triggers", + "DuplicatesAllowed": true, "ItemType": "Trigger", "Required": false, "Type": "List", @@ -1077,6 +1104,11 @@ } }, "AWS::IoTAnalytics::Datastore": { + "Attributes": { + "Id": { + "PrimitiveType": "String" + } + }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-datastore.html", "Properties": { "DatastoreName": { @@ -1111,6 +1143,7 @@ }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-datastore.html#cfn-iotanalytics-datastore-tags", + "DuplicatesAllowed": true, "ItemType": "Tag", "Required": false, "Type": "List", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTCoreDeviceAdvisor.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTCoreDeviceAdvisor.json index 33c5ed7118618..c84766cc03d00 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTCoreDeviceAdvisor.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTCoreDeviceAdvisor.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": {}, "ResourceTypes": { "AWS::IoTCoreDeviceAdvisor::SuiteDefinition": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTEvents.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTEvents.json index d324214079d98..850fa8b3cfc4c 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTEvents.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTEvents.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::IoTEvents::DetectorModel.Action": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotevents-detectormodel-action.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTFleetHub.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTFleetHub.json index 839260e26e193..23d24f86e2f84 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTFleetHub.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTFleetHub.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": {}, "ResourceTypes": { "AWS::IoTFleetHub::Application": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTSiteWise.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTSiteWise.json index 6947456a73779..17ae0a10f0503 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTSiteWise.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTSiteWise.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::IoTSiteWise::AccessPolicy.AccessPolicyIdentity": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-accesspolicy-accesspolicyidentity.html", @@ -396,9 +396,15 @@ "Properties": { "Greengrass": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-gateway-gatewayplatform.html#cfn-iotsitewise-gateway-gatewayplatform-greengrass", - "Required": true, + "Required": false, "Type": "Greengrass", "UpdateType": "Immutable" + }, + "GreengrassV2": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-gateway-gatewayplatform.html#cfn-iotsitewise-gateway-gatewayplatform-greengrassv2", + "Required": false, + "Type": "GreengrassV2", + "UpdateType": "Immutable" } } }, @@ -412,6 +418,17 @@ "UpdateType": "Immutable" } } + }, + "AWS::IoTSiteWise::Gateway.GreengrassV2": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-gateway-greengrassv2.html", + "Properties": { + "CoreDeviceThingName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-gateway-greengrassv2.html#cfn-iotsitewise-gateway-greengrassv2-coredevicethingname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } } }, "ResourceTypes": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTThingsGraph.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTThingsGraph.json index ba4ff225e79f2..be9dc25d946cd 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTThingsGraph.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTThingsGraph.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::IoTThingsGraph::FlowTemplate.DefinitionDocument": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotthingsgraph-flowtemplate-definitiondocument.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTWireless.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTWireless.json index 7a74fee371eb8..8970e4064a67b 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTWireless.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoTWireless.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::IoTWireless::DeviceProfile.LoRaWANDeviceProfile": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-deviceprofile-lorawandeviceprofile.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KMS.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KMS.json index dccc1229fc1c2..96f537fb510de 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KMS.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KMS.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": {}, "ResourceTypes": { "AWS::KMS::Alias": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KafkaConnect.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KafkaConnect.json new file mode 100644 index 0000000000000..29d4ae2599272 --- /dev/null +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KafkaConnect.json @@ -0,0 +1,390 @@ +{ + "$version": "57.0.0", + "PropertyTypes": { + "AWS::KafkaConnect::Connector.ApacheKafkaCluster": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-apachekafkacluster.html", + "Properties": { + "BootstrapServers": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-apachekafkacluster.html#cfn-kafkaconnect-connector-apachekafkacluster-bootstrapservers", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Vpc": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-apachekafkacluster.html#cfn-kafkaconnect-connector-apachekafkacluster-vpc", + "Required": true, + "Type": "Vpc", + "UpdateType": "Immutable" + } + } + }, + "AWS::KafkaConnect::Connector.AutoScaling": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-autoscaling.html", + "Properties": { + "MaxWorkerCount": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-autoscaling.html#cfn-kafkaconnect-connector-autoscaling-maxworkercount", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + }, + "McuCount": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-autoscaling.html#cfn-kafkaconnect-connector-autoscaling-mcucount", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + }, + "MinWorkerCount": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-autoscaling.html#cfn-kafkaconnect-connector-autoscaling-minworkercount", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + }, + "ScaleInPolicy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-autoscaling.html#cfn-kafkaconnect-connector-autoscaling-scaleinpolicy", + "Required": true, + "Type": "ScaleInPolicy", + "UpdateType": "Mutable" + }, + "ScaleOutPolicy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-autoscaling.html#cfn-kafkaconnect-connector-autoscaling-scaleoutpolicy", + "Required": true, + "Type": "ScaleOutPolicy", + "UpdateType": "Mutable" + } + } + }, + "AWS::KafkaConnect::Connector.Capacity": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-capacity.html", + "Properties": { + "AutoScaling": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-capacity.html#cfn-kafkaconnect-connector-capacity-autoscaling", + "Required": false, + "Type": "AutoScaling", + "UpdateType": "Mutable" + }, + "ProvisionedCapacity": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-capacity.html#cfn-kafkaconnect-connector-capacity-provisionedcapacity", + "Required": false, + "Type": "ProvisionedCapacity", + "UpdateType": "Mutable" + } + } + }, + "AWS::KafkaConnect::Connector.CloudWatchLogsLogDelivery": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-cloudwatchlogslogdelivery.html", + "Properties": { + "Enabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-cloudwatchlogslogdelivery.html#cfn-kafkaconnect-connector-cloudwatchlogslogdelivery-enabled", + "PrimitiveType": "Boolean", + "Required": true, + "UpdateType": "Immutable" + }, + "LogGroup": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-cloudwatchlogslogdelivery.html#cfn-kafkaconnect-connector-cloudwatchlogslogdelivery-loggroup", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, + "AWS::KafkaConnect::Connector.CustomPlugin": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-customplugin.html", + "Properties": { + "CustomPluginArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-customplugin.html#cfn-kafkaconnect-connector-customplugin-custompluginarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Revision": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-customplugin.html#cfn-kafkaconnect-connector-customplugin-revision", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::KafkaConnect::Connector.FirehoseLogDelivery": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-firehoselogdelivery.html", + "Properties": { + "DeliveryStream": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-firehoselogdelivery.html#cfn-kafkaconnect-connector-firehoselogdelivery-deliverystream", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Enabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-firehoselogdelivery.html#cfn-kafkaconnect-connector-firehoselogdelivery-enabled", + "PrimitiveType": "Boolean", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::KafkaConnect::Connector.KafkaCluster": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-kafkacluster.html", + "Properties": { + "ApacheKafkaCluster": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-kafkacluster.html#cfn-kafkaconnect-connector-kafkacluster-apachekafkacluster", + "Required": true, + "Type": "ApacheKafkaCluster", + "UpdateType": "Immutable" + } + } + }, + "AWS::KafkaConnect::Connector.KafkaClusterClientAuthentication": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-kafkaclusterclientauthentication.html", + "Properties": { + "AuthenticationType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-kafkaclusterclientauthentication.html#cfn-kafkaconnect-connector-kafkaclusterclientauthentication-authenticationtype", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::KafkaConnect::Connector.KafkaClusterEncryptionInTransit": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-kafkaclusterencryptionintransit.html", + "Properties": { + "EncryptionType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-kafkaclusterencryptionintransit.html#cfn-kafkaconnect-connector-kafkaclusterencryptionintransit-encryptiontype", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::KafkaConnect::Connector.LogDelivery": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-logdelivery.html", + "Properties": { + "WorkerLogDelivery": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-logdelivery.html#cfn-kafkaconnect-connector-logdelivery-workerlogdelivery", + "Required": true, + "Type": "WorkerLogDelivery", + "UpdateType": "Immutable" + } + } + }, + "AWS::KafkaConnect::Connector.Plugin": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-plugin.html", + "Properties": { + "CustomPlugin": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-plugin.html#cfn-kafkaconnect-connector-plugin-customplugin", + "Required": true, + "Type": "CustomPlugin", + "UpdateType": "Immutable" + } + } + }, + "AWS::KafkaConnect::Connector.ProvisionedCapacity": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-provisionedcapacity.html", + "Properties": { + "McuCount": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-provisionedcapacity.html#cfn-kafkaconnect-connector-provisionedcapacity-mcucount", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "WorkerCount": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-provisionedcapacity.html#cfn-kafkaconnect-connector-provisionedcapacity-workercount", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::KafkaConnect::Connector.S3LogDelivery": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-s3logdelivery.html", + "Properties": { + "Bucket": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-s3logdelivery.html#cfn-kafkaconnect-connector-s3logdelivery-bucket", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Enabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-s3logdelivery.html#cfn-kafkaconnect-connector-s3logdelivery-enabled", + "PrimitiveType": "Boolean", + "Required": true, + "UpdateType": "Immutable" + }, + "Prefix": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-s3logdelivery.html#cfn-kafkaconnect-connector-s3logdelivery-prefix", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, + "AWS::KafkaConnect::Connector.ScaleInPolicy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-scaleinpolicy.html", + "Properties": { + "CpuUtilizationPercentage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-scaleinpolicy.html#cfn-kafkaconnect-connector-scaleinpolicy-cpuutilizationpercentage", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::KafkaConnect::Connector.ScaleOutPolicy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-scaleoutpolicy.html", + "Properties": { + "CpuUtilizationPercentage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-scaleoutpolicy.html#cfn-kafkaconnect-connector-scaleoutpolicy-cpuutilizationpercentage", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::KafkaConnect::Connector.Vpc": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-vpc.html", + "Properties": { + "SecurityGroups": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-vpc.html#cfn-kafkaconnect-connector-vpc-securitygroups", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": true, + "Type": "List", + "UpdateType": "Immutable" + }, + "Subnets": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-vpc.html#cfn-kafkaconnect-connector-vpc-subnets", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": true, + "Type": "List", + "UpdateType": "Immutable" + } + } + }, + "AWS::KafkaConnect::Connector.WorkerConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-workerconfiguration.html", + "Properties": { + "Revision": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-workerconfiguration.html#cfn-kafkaconnect-connector-workerconfiguration-revision", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Immutable" + }, + "WorkerConfigurationArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-workerconfiguration.html#cfn-kafkaconnect-connector-workerconfiguration-workerconfigurationarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::KafkaConnect::Connector.WorkerLogDelivery": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-workerlogdelivery.html", + "Properties": { + "CloudWatchLogs": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-workerlogdelivery.html#cfn-kafkaconnect-connector-workerlogdelivery-cloudwatchlogs", + "Required": false, + "Type": "CloudWatchLogsLogDelivery", + "UpdateType": "Immutable" + }, + "Firehose": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-workerlogdelivery.html#cfn-kafkaconnect-connector-workerlogdelivery-firehose", + "Required": false, + "Type": "FirehoseLogDelivery", + "UpdateType": "Immutable" + }, + "S3": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kafkaconnect-connector-workerlogdelivery.html#cfn-kafkaconnect-connector-workerlogdelivery-s3", + "Required": false, + "Type": "S3LogDelivery", + "UpdateType": "Immutable" + } + } + } + }, + "ResourceTypes": { + "AWS::KafkaConnect::Connector": { + "Attributes": { + "ConnectorArn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kafkaconnect-connector.html", + "Properties": { + "Capacity": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kafkaconnect-connector.html#cfn-kafkaconnect-connector-capacity", + "Required": true, + "Type": "Capacity", + "UpdateType": "Mutable" + }, + "ConnectorConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kafkaconnect-connector.html#cfn-kafkaconnect-connector-connectorconfiguration", + "PrimitiveItemType": "String", + "Required": true, + "Type": "Map", + "UpdateType": "Immutable" + }, + "ConnectorDescription": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kafkaconnect-connector.html#cfn-kafkaconnect-connector-connectordescription", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "ConnectorName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kafkaconnect-connector.html#cfn-kafkaconnect-connector-connectorname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "KafkaCluster": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kafkaconnect-connector.html#cfn-kafkaconnect-connector-kafkacluster", + "Required": true, + "Type": "KafkaCluster", + "UpdateType": "Immutable" + }, + "KafkaClusterClientAuthentication": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kafkaconnect-connector.html#cfn-kafkaconnect-connector-kafkaclusterclientauthentication", + "Required": true, + "Type": "KafkaClusterClientAuthentication", + "UpdateType": "Immutable" + }, + "KafkaClusterEncryptionInTransit": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kafkaconnect-connector.html#cfn-kafkaconnect-connector-kafkaclusterencryptionintransit", + "Required": true, + "Type": "KafkaClusterEncryptionInTransit", + "UpdateType": "Immutable" + }, + "KafkaConnectVersion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kafkaconnect-connector.html#cfn-kafkaconnect-connector-kafkaconnectversion", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "LogDelivery": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kafkaconnect-connector.html#cfn-kafkaconnect-connector-logdelivery", + "Required": false, + "Type": "LogDelivery", + "UpdateType": "Immutable" + }, + "Plugins": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kafkaconnect-connector.html#cfn-kafkaconnect-connector-plugins", + "DuplicatesAllowed": false, + "ItemType": "Plugin", + "Required": true, + "Type": "List", + "UpdateType": "Immutable" + }, + "ServiceExecutionRoleArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kafkaconnect-connector.html#cfn-kafkaconnect-connector-serviceexecutionrolearn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "WorkerConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kafkaconnect-connector.html#cfn-kafkaconnect-connector-workerconfiguration", + "Required": false, + "Type": "WorkerConfiguration", + "UpdateType": "Immutable" + } + } + } + } +} diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Kendra.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Kendra.json index 8b7a644d13bea..b5b70c9126ede 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Kendra.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Kendra.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Kendra::DataSource.AccessControlListConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-accesscontrollistconfiguration.html", @@ -334,6 +334,36 @@ } } }, + "AWS::Kendra::DataSource.CustomDocumentEnrichmentConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-customdocumentenrichmentconfiguration.html", + "Properties": { + "InlineConfigurations": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-customdocumentenrichmentconfiguration.html#cfn-kendra-datasource-customdocumentenrichmentconfiguration-inlineconfigurations", + "ItemType": "InlineCustomDocumentEnrichmentConfiguration", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "PostExtractionHookConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-customdocumentenrichmentconfiguration.html#cfn-kendra-datasource-customdocumentenrichmentconfiguration-postextractionhookconfiguration", + "Required": false, + "Type": "HookConfiguration", + "UpdateType": "Mutable" + }, + "PreExtractionHookConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-customdocumentenrichmentconfiguration.html#cfn-kendra-datasource-customdocumentenrichmentconfiguration-preextractionhookconfiguration", + "Required": false, + "Type": "HookConfiguration", + "UpdateType": "Mutable" + }, + "RoleArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-customdocumentenrichmentconfiguration.html#cfn-kendra-datasource-customdocumentenrichmentconfiguration-rolearn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::Kendra::DataSource.DataSourceConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-datasourceconfiguration.html", "Properties": { @@ -482,6 +512,82 @@ } } }, + "AWS::Kendra::DataSource.DocumentAttributeCondition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-documentattributecondition.html", + "Properties": { + "ConditionDocumentAttributeKey": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-documentattributecondition.html#cfn-kendra-datasource-documentattributecondition-conditiondocumentattributekey", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "ConditionOnValue": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-documentattributecondition.html#cfn-kendra-datasource-documentattributecondition-conditiononvalue", + "Required": false, + "Type": "DocumentAttributeValue", + "UpdateType": "Mutable" + }, + "Operator": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-documentattributecondition.html#cfn-kendra-datasource-documentattributecondition-operator", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::Kendra::DataSource.DocumentAttributeTarget": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-documentattributetarget.html", + "Properties": { + "TargetDocumentAttributeKey": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-documentattributetarget.html#cfn-kendra-datasource-documentattributetarget-targetdocumentattributekey", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "TargetDocumentAttributeValue": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-documentattributetarget.html#cfn-kendra-datasource-documentattributetarget-targetdocumentattributevalue", + "Required": false, + "Type": "DocumentAttributeValue", + "UpdateType": "Mutable" + }, + "TargetDocumentAttributeValueDeletion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-documentattributetarget.html#cfn-kendra-datasource-documentattributetarget-targetdocumentattributevaluedeletion", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::Kendra::DataSource.DocumentAttributeValue": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-documentattributevalue.html", + "Properties": { + "DateValue": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-documentattributevalue.html#cfn-kendra-datasource-documentattributevalue-datevalue", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "LongValue": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-documentattributevalue.html#cfn-kendra-datasource-documentattributevalue-longvalue", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "StringListValue": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-documentattributevalue.html#cfn-kendra-datasource-documentattributevalue-stringlistvalue", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "StringValue": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-documentattributevalue.html#cfn-kendra-datasource-documentattributevalue-stringvalue", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::Kendra::DataSource.DocumentsMetadataConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-documentsmetadataconfiguration.html", "Properties": { @@ -546,6 +652,52 @@ } } }, + "AWS::Kendra::DataSource.HookConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-hookconfiguration.html", + "Properties": { + "InvocationCondition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-hookconfiguration.html#cfn-kendra-datasource-hookconfiguration-invocationcondition", + "Required": false, + "Type": "DocumentAttributeCondition", + "UpdateType": "Mutable" + }, + "LambdaArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-hookconfiguration.html#cfn-kendra-datasource-hookconfiguration-lambdaarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "S3Bucket": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-hookconfiguration.html#cfn-kendra-datasource-hookconfiguration-s3bucket", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::Kendra::DataSource.InlineCustomDocumentEnrichmentConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-inlinecustomdocumentenrichmentconfiguration.html", + "Properties": { + "Condition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-inlinecustomdocumentenrichmentconfiguration.html#cfn-kendra-datasource-inlinecustomdocumentenrichmentconfiguration-condition", + "Required": false, + "Type": "DocumentAttributeCondition", + "UpdateType": "Mutable" + }, + "DocumentContentDeletion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-inlinecustomdocumentenrichmentconfiguration.html#cfn-kendra-datasource-inlinecustomdocumentenrichmentconfiguration-documentcontentdeletion", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "Target": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-inlinecustomdocumentenrichmentconfiguration.html#cfn-kendra-datasource-inlinecustomdocumentenrichmentconfiguration-target", + "Required": false, + "Type": "DocumentAttributeTarget", + "UpdateType": "Mutable" + } + } + }, "AWS::Kendra::DataSource.OneDriveConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kendra-datasource-onedriveconfiguration.html", "Properties": { @@ -1582,6 +1734,12 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kendra-datasource.html", "Properties": { + "CustomDocumentEnrichmentConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kendra-datasource.html#cfn-kendra-datasource-customdocumentenrichmentconfiguration", + "Required": false, + "Type": "CustomDocumentEnrichmentConfiguration", + "UpdateType": "Mutable" + }, "DataSourceConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kendra-datasource.html#cfn-kendra-datasource-datasourceconfiguration", "Required": false, diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Kinesis.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Kinesis.json index 5adc18f973763..2b616b2058f3d 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Kinesis.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Kinesis.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Kinesis::Stream.StreamEncryption": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kinesis-stream-streamencryption.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KinesisAnalytics.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KinesisAnalytics.json index 43cc269ee6ee0..efa03d9788fca 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KinesisAnalytics.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KinesisAnalytics.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::KinesisAnalytics::Application.CSVMappingParameters": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kinesisanalytics-application-csvmappingparameters.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KinesisAnalyticsV2.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KinesisAnalyticsV2.json index dbd0a8d9a2138..34e3a5beb1e1d 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KinesisAnalyticsV2.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KinesisAnalyticsV2.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::KinesisAnalyticsV2::Application.ApplicationCodeConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kinesisanalyticsv2-application-applicationcodeconfiguration.html", @@ -173,13 +173,6 @@ } } }, - "AWS::KinesisAnalyticsV2::Application.CustomArtifactsConfiguration": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kinesisanalyticsv2-application-customartifactsconfiguration.html", - "ItemType": "CustomArtifactConfiguration", - "Required": false, - "Type": "List", - "UpdateType": "Mutable" - }, "AWS::KinesisAnalyticsV2::Application.DeployAsApplicationConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kinesisanalyticsv2-application-deployasapplicationconfiguration.html", "Properties": { @@ -580,8 +573,9 @@ }, "CustomArtifactsConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kinesisanalyticsv2-application-zeppelinapplicationconfiguration.html#cfn-kinesisanalyticsv2-application-zeppelinapplicationconfiguration-customartifactsconfiguration", + "ItemType": "CustomArtifactConfiguration", "Required": false, - "Type": "CustomArtifactsConfiguration", + "Type": "List", "UpdateType": "Mutable" }, "DeployAsApplicationConfiguration": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KinesisFirehose.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KinesisFirehose.json index 12805dd4b8543..cd454177af1e6 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KinesisFirehose.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KinesisFirehose.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::KinesisFirehose::DeliveryStream.AmazonopensearchserviceBufferingHints": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-kinesisfirehose-deliverystream-amazonopensearchservicebufferinghints.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KinesisVideo.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KinesisVideo.json new file mode 100644 index 0000000000000..5e5a4947580d0 --- /dev/null +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_KinesisVideo.json @@ -0,0 +1,90 @@ +{ + "$version": "57.0.0", + "PropertyTypes": {}, + "ResourceTypes": { + "AWS::KinesisVideo::SignalingChannel": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kinesisvideo-signalingchannel.html", + "Properties": { + "MessageTtlSeconds": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kinesisvideo-signalingchannel.html#cfn-kinesisvideo-signalingchannel-messagettlseconds", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kinesisvideo-signalingchannel.html#cfn-kinesisvideo-signalingchannel-name", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kinesisvideo-signalingchannel.html#cfn-kinesisvideo-signalingchannel-tags", + "DuplicatesAllowed": true, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Type": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kinesisvideo-signalingchannel.html#cfn-kinesisvideo-signalingchannel-type", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::KinesisVideo::Stream": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kinesisvideo-stream.html", + "Properties": { + "DataRetentionInHours": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kinesisvideo-stream.html#cfn-kinesisvideo-stream-dataretentioninhours", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "DeviceName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kinesisvideo-stream.html#cfn-kinesisvideo-stream-devicename", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "KmsKeyId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kinesisvideo-stream.html#cfn-kinesisvideo-stream-kmskeyid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "MediaType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kinesisvideo-stream.html#cfn-kinesisvideo-stream-mediatype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kinesisvideo-stream.html#cfn-kinesisvideo-stream-name", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kinesisvideo-stream.html#cfn-kinesisvideo-stream-tags", + "DuplicatesAllowed": true, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + } + } +} diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_LakeFormation.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_LakeFormation.json index 3469bd4537b61..2ce9f02860ce8 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_LakeFormation.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_LakeFormation.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::LakeFormation::DataLakeSettings.Admins": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lakeformation-datalakesettings-admins.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Lambda.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Lambda.json index 3f19bf4e27cd4..db1bf7fb3ec28 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Lambda.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Lambda.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Lambda::Alias.AliasRoutingConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-alias-aliasroutingconfiguration.html", @@ -128,6 +128,30 @@ } } }, + "AWS::Lambda::EventSourceMapping.Filter": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-eventsourcemapping-filter.html", + "Properties": { + "Pattern": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-eventsourcemapping-filter.html#cfn-lambda-eventsourcemapping-filter-pattern", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::Lambda::EventSourceMapping.FilterCriteria": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-eventsourcemapping-filtercriteria.html", + "Properties": { + "Filters": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-eventsourcemapping-filtercriteria.html#cfn-lambda-eventsourcemapping-filtercriteria-filters", + "DuplicatesAllowed": false, + "ItemType": "Filter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::Lambda::EventSourceMapping.OnFailure": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-eventsourcemapping-onfailure.html", "Properties": { @@ -484,8 +508,8 @@ }, "FilterCriteria": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html#cfn-lambda-eventsourcemapping-filtercriteria", - "PrimitiveType": "Json", "Required": false, + "Type": "FilterCriteria", "UpdateType": "Mutable" }, "FunctionName": { @@ -558,7 +582,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html#cfn-lambda-eventsourcemapping-startingpositiontimestamp", "PrimitiveType": "Double", "Required": false, - "UpdateType": "Mutable" + "UpdateType": "Immutable" }, "Topics": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html#cfn-lambda-eventsourcemapping-topics", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Lex.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Lex.json index d549b8d0ceea5..d1a7dc8db36b7 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Lex.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Lex.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Lex::Bot.BotLocale": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lex-bot-botlocale.html", @@ -972,14 +972,14 @@ "Properties": { "BotAliasLocaleSetting": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lex-botalias-botaliaslocalesettingsitem.html#cfn-lex-botalias-botaliaslocalesettingsitem-botaliaslocalesetting", - "Required": false, + "Required": true, "Type": "BotAliasLocaleSettings", "UpdateType": "Mutable" }, "LocaleId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lex-botalias-botaliaslocalesettingsitem.html#cfn-lex-botalias-botaliaslocalesettingsitem-localeid", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" } } @@ -1077,6 +1077,7 @@ "CloudWatch": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lex-botalias-textlogdestination.html#cfn-lex-botalias-textlogdestination-cloudwatch", "Required": false, + "Type": "CloudWatchLogGroupLogDestination", "UpdateType": "Mutable" } } @@ -1127,7 +1128,10 @@ } }, "AWS::Lex::ResourcePolicy.Policy": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lex-resourcepolicy-policy.html" + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lex-resourcepolicy-policy.html", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" } }, "ResourceTypes": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_LicenseManager.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_LicenseManager.json index 3237b87d3480f..ae278e75b88b8 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_LicenseManager.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_LicenseManager.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::LicenseManager::License.BorrowConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-licensemanager-license-borrowconfiguration.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Lightsail.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Lightsail.json index 902b100affa10..26a1216f50b52 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Lightsail.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Lightsail.json @@ -1,6 +1,200 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { + "AWS::Lightsail::Bucket.AccessRules": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-bucket-accessrules.html", + "Properties": { + "AllowPublicOverrides": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-bucket-accessrules.html#cfn-lightsail-bucket-accessrules-allowpublicoverrides", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "GetObject": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-bucket-accessrules.html#cfn-lightsail-bucket-accessrules-getobject", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::Lightsail::Container.Container": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-container.html", + "Properties": { + "Command": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-container.html#cfn-lightsail-container-container-command", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "ContainerName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-container.html#cfn-lightsail-container-container-containername", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Environment": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-container.html#cfn-lightsail-container-container-environment", + "DuplicatesAllowed": false, + "ItemType": "EnvironmentVariable", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Image": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-container.html#cfn-lightsail-container-container-image", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Ports": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-container.html#cfn-lightsail-container-container-ports", + "DuplicatesAllowed": false, + "ItemType": "PortInfo", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::Lightsail::Container.ContainerServiceDeployment": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-containerservicedeployment.html", + "Properties": { + "Containers": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-containerservicedeployment.html#cfn-lightsail-container-containerservicedeployment-containers", + "DuplicatesAllowed": false, + "ItemType": "Container", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "PublicEndpoint": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-containerservicedeployment.html#cfn-lightsail-container-containerservicedeployment-publicendpoint", + "Required": false, + "Type": "PublicEndpoint", + "UpdateType": "Mutable" + } + } + }, + "AWS::Lightsail::Container.EnvironmentVariable": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-environmentvariable.html", + "Properties": { + "Value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-environmentvariable.html#cfn-lightsail-container-environmentvariable-value", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Variable": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-environmentvariable.html#cfn-lightsail-container-environmentvariable-variable", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::Lightsail::Container.HealthCheckConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-healthcheckconfig.html", + "Properties": { + "HealthyThreshold": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-healthcheckconfig.html#cfn-lightsail-container-healthcheckconfig-healthythreshold", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "IntervalSeconds": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-healthcheckconfig.html#cfn-lightsail-container-healthcheckconfig-intervalseconds", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "Path": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-healthcheckconfig.html#cfn-lightsail-container-healthcheckconfig-path", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "SuccessCodes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-healthcheckconfig.html#cfn-lightsail-container-healthcheckconfig-successcodes", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "TimeoutSeconds": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-healthcheckconfig.html#cfn-lightsail-container-healthcheckconfig-timeoutseconds", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "UnhealthyThreshold": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-healthcheckconfig.html#cfn-lightsail-container-healthcheckconfig-unhealthythreshold", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::Lightsail::Container.PortInfo": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-portinfo.html", + "Properties": { + "Port": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-portinfo.html#cfn-lightsail-container-portinfo-port", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Protocol": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-portinfo.html#cfn-lightsail-container-portinfo-protocol", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::Lightsail::Container.PublicDomainName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-publicdomainname.html", + "Properties": { + "CertificateName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-publicdomainname.html#cfn-lightsail-container-publicdomainname-certificatename", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DomainNames": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-publicdomainname.html#cfn-lightsail-container-publicdomainname-domainnames", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::Lightsail::Container.PublicEndpoint": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-publicendpoint.html", + "Properties": { + "ContainerName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-publicendpoint.html#cfn-lightsail-container-publicendpoint-containername", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ContainerPort": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-publicendpoint.html#cfn-lightsail-container-publicendpoint-containerport", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "HealthCheckConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-publicendpoint.html#cfn-lightsail-container-publicendpoint-healthcheckconfig", + "Required": false, + "Type": "HealthCheckConfig", + "UpdateType": "Mutable" + } + } + }, "AWS::Lightsail::Database.RelationalDatabaseParameter": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-database-relationaldatabaseparameter.html", "Properties": { @@ -88,6 +282,167 @@ } } }, + "AWS::Lightsail::Distribution.CacheBehavior": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cachebehavior.html", + "Properties": { + "Behavior": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cachebehavior.html#cfn-lightsail-distribution-cachebehavior-behavior", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::Lightsail::Distribution.CacheBehaviorPerPath": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cachebehaviorperpath.html", + "Properties": { + "Behavior": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cachebehaviorperpath.html#cfn-lightsail-distribution-cachebehaviorperpath-behavior", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Path": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cachebehaviorperpath.html#cfn-lightsail-distribution-cachebehaviorperpath-path", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::Lightsail::Distribution.CacheSettings": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cachesettings.html", + "Properties": { + "AllowedHTTPMethods": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cachesettings.html#cfn-lightsail-distribution-cachesettings-allowedhttpmethods", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "CachedHTTPMethods": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cachesettings.html#cfn-lightsail-distribution-cachesettings-cachedhttpmethods", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DefaultTTL": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cachesettings.html#cfn-lightsail-distribution-cachesettings-defaultttl", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "ForwardedCookies": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cachesettings.html#cfn-lightsail-distribution-cachesettings-forwardedcookies", + "Required": false, + "Type": "CookieObject", + "UpdateType": "Mutable" + }, + "ForwardedHeaders": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cachesettings.html#cfn-lightsail-distribution-cachesettings-forwardedheaders", + "Required": false, + "Type": "HeaderObject", + "UpdateType": "Mutable" + }, + "ForwardedQueryStrings": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cachesettings.html#cfn-lightsail-distribution-cachesettings-forwardedquerystrings", + "Required": false, + "Type": "QueryStringObject", + "UpdateType": "Mutable" + }, + "MaximumTTL": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cachesettings.html#cfn-lightsail-distribution-cachesettings-maximumttl", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "MinimumTTL": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cachesettings.html#cfn-lightsail-distribution-cachesettings-minimumttl", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::Lightsail::Distribution.CookieObject": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cookieobject.html", + "Properties": { + "CookiesAllowList": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cookieobject.html#cfn-lightsail-distribution-cookieobject-cookiesallowlist", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Option": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cookieobject.html#cfn-lightsail-distribution-cookieobject-option", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::Lightsail::Distribution.HeaderObject": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-headerobject.html", + "Properties": { + "HeadersAllowList": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-headerobject.html#cfn-lightsail-distribution-headerobject-headersallowlist", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Option": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-headerobject.html#cfn-lightsail-distribution-headerobject-option", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::Lightsail::Distribution.InputOrigin": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-inputorigin.html", + "Properties": { + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-inputorigin.html#cfn-lightsail-distribution-inputorigin-name", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ProtocolPolicy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-inputorigin.html#cfn-lightsail-distribution-inputorigin-protocolpolicy", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "RegionName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-inputorigin.html#cfn-lightsail-distribution-inputorigin-regionname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::Lightsail::Distribution.QueryStringObject": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-querystringobject.html", + "Properties": { + "Option": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-querystringobject.html#cfn-lightsail-distribution-querystringobject-option", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "QueryStringsAllowList": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-querystringobject.html#cfn-lightsail-distribution-querystringobject-querystringsallowlist", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::Lightsail::Instance.AddOn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-instance-addon.html", "Properties": { @@ -328,33 +683,280 @@ } }, "ResourceTypes": { - "AWS::Lightsail::Database": { + "AWS::Lightsail::Alarm": { "Attributes": { - "DatabaseArn": { + "AlarmArn": { + "PrimitiveType": "String" + }, + "State": { "PrimitiveType": "String" } }, - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-database.html", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-alarm.html", "Properties": { - "AvailabilityZone": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-database.html#cfn-lightsail-database-availabilityzone", + "AlarmName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-alarm.html#cfn-lightsail-alarm-alarmname", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" }, - "BackupRetention": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-database.html#cfn-lightsail-database-backupretention", - "PrimitiveType": "Boolean", - "Required": false, - "UpdateType": "Mutable" - }, - "CaCertificateIdentifier": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-database.html#cfn-lightsail-database-cacertificateidentifier", + "ComparisonOperator": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-alarm.html#cfn-lightsail-alarm-comparisonoperator", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, - "MasterDatabaseName": { + "ContactProtocols": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-alarm.html#cfn-lightsail-alarm-contactprotocols", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "DatapointsToAlarm": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-alarm.html#cfn-lightsail-alarm-datapointstoalarm", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "EvaluationPeriods": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-alarm.html#cfn-lightsail-alarm-evaluationperiods", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + }, + "MetricName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-alarm.html#cfn-lightsail-alarm-metricname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "MonitoredResourceName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-alarm.html#cfn-lightsail-alarm-monitoredresourcename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "NotificationEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-alarm.html#cfn-lightsail-alarm-notificationenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "NotificationTriggers": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-alarm.html#cfn-lightsail-alarm-notificationtriggers", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Threshold": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-alarm.html#cfn-lightsail-alarm-threshold", + "PrimitiveType": "Double", + "Required": true, + "UpdateType": "Mutable" + }, + "TreatMissingData": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-alarm.html#cfn-lightsail-alarm-treatmissingdata", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::Lightsail::Bucket": { + "Attributes": { + "AbleToUpdateBundle": { + "PrimitiveType": "Boolean" + }, + "BucketArn": { + "PrimitiveType": "String" + }, + "Url": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-bucket.html", + "Properties": { + "AccessRules": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-bucket.html#cfn-lightsail-bucket-accessrules", + "Required": false, + "Type": "AccessRules", + "UpdateType": "Mutable" + }, + "BucketName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-bucket.html#cfn-lightsail-bucket-bucketname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "BundleId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-bucket.html#cfn-lightsail-bucket-bundleid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "ObjectVersioning": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-bucket.html#cfn-lightsail-bucket-objectversioning", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "ReadOnlyAccessAccounts": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-bucket.html#cfn-lightsail-bucket-readonlyaccessaccounts", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "ResourcesReceivingAccess": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-bucket.html#cfn-lightsail-bucket-resourcesreceivingaccess", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-bucket.html#cfn-lightsail-bucket-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::Lightsail::Certificate": { + "Attributes": { + "CertificateArn": { + "PrimitiveType": "String" + }, + "Status": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-certificate.html", + "Properties": { + "CertificateName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-certificate.html#cfn-lightsail-certificate-certificatename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "DomainName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-certificate.html#cfn-lightsail-certificate-domainname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "SubjectAlternativeNames": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-certificate.html#cfn-lightsail-certificate-subjectalternativenames", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-certificate.html#cfn-lightsail-certificate-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::Lightsail::Container": { + "Attributes": { + "ContainerArn": { + "PrimitiveType": "String" + }, + "Url": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-container.html", + "Properties": { + "ContainerServiceDeployment": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-container.html#cfn-lightsail-container-containerservicedeployment", + "Required": false, + "Type": "ContainerServiceDeployment", + "UpdateType": "Mutable" + }, + "IsDisabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-container.html#cfn-lightsail-container-isdisabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "Power": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-container.html#cfn-lightsail-container-power", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "PublicDomainNames": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-container.html#cfn-lightsail-container-publicdomainnames", + "DuplicatesAllowed": false, + "ItemType": "PublicDomainName", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Scale": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-container.html#cfn-lightsail-container-scale", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + }, + "ServiceName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-container.html#cfn-lightsail-container-servicename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-container.html#cfn-lightsail-container-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::Lightsail::Database": { + "Attributes": { + "DatabaseArn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-database.html", + "Properties": { + "AvailabilityZone": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-database.html#cfn-lightsail-database-availabilityzone", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "BackupRetention": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-database.html#cfn-lightsail-database-backupretention", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "CaCertificateIdentifier": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-database.html#cfn-lightsail-database-cacertificateidentifier", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "MasterDatabaseName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-database.html#cfn-lightsail-database-masterdatabasename", "PrimitiveType": "String", "Required": true, @@ -499,6 +1101,86 @@ } } }, + "AWS::Lightsail::Distribution": { + "Attributes": { + "AbleToUpdateBundle": { + "PrimitiveType": "Boolean" + }, + "DistributionArn": { + "PrimitiveType": "String" + }, + "Status": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-distribution.html", + "Properties": { + "BundleId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-distribution.html#cfn-lightsail-distribution-bundleid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "CacheBehaviorSettings": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-distribution.html#cfn-lightsail-distribution-cachebehaviorsettings", + "Required": false, + "Type": "CacheSettings", + "UpdateType": "Mutable" + }, + "CacheBehaviors": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-distribution.html#cfn-lightsail-distribution-cachebehaviors", + "DuplicatesAllowed": false, + "ItemType": "CacheBehaviorPerPath", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "CertificateName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-distribution.html#cfn-lightsail-distribution-certificatename", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DefaultCacheBehavior": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-distribution.html#cfn-lightsail-distribution-defaultcachebehavior", + "Required": true, + "Type": "CacheBehavior", + "UpdateType": "Mutable" + }, + "DistributionName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-distribution.html#cfn-lightsail-distribution-distributionname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "IpAddressType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-distribution.html#cfn-lightsail-distribution-ipaddresstype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "IsEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-distribution.html#cfn-lightsail-distribution-isenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "Origin": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-distribution.html#cfn-lightsail-distribution-origin", + "Required": true, + "Type": "InputOrigin", + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-distribution.html#cfn-lightsail-distribution-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::Lightsail::Instance": { "Attributes": { "Hardware.CpuCount": { @@ -614,6 +1296,113 @@ } } }, + "AWS::Lightsail::LoadBalancer": { + "Attributes": { + "LoadBalancerArn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-loadbalancer.html", + "Properties": { + "AttachedInstances": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-loadbalancer.html#cfn-lightsail-loadbalancer-attachedinstances", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "HealthCheckPath": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-loadbalancer.html#cfn-lightsail-loadbalancer-healthcheckpath", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "InstancePort": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-loadbalancer.html#cfn-lightsail-loadbalancer-instanceport", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Immutable" + }, + "IpAddressType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-loadbalancer.html#cfn-lightsail-loadbalancer-ipaddresstype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "LoadBalancerName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-loadbalancer.html#cfn-lightsail-loadbalancer-loadbalancername", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "SessionStickinessEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-loadbalancer.html#cfn-lightsail-loadbalancer-sessionstickinessenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "SessionStickinessLBCookieDurationSeconds": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-loadbalancer.html#cfn-lightsail-loadbalancer-sessionstickinesslbcookiedurationseconds", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-loadbalancer.html#cfn-lightsail-loadbalancer-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::Lightsail::LoadBalancerTlsCertificate": { + "Attributes": { + "LoadBalancerTlsCertificateArn": { + "PrimitiveType": "String" + }, + "Status": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-loadbalancertlscertificate.html", + "Properties": { + "CertificateAlternativeNames": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-loadbalancertlscertificate.html#cfn-lightsail-loadbalancertlscertificate-certificatealternativenames", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "CertificateDomainName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-loadbalancertlscertificate.html#cfn-lightsail-loadbalancertlscertificate-certificatedomainname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "CertificateName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-loadbalancertlscertificate.html#cfn-lightsail-loadbalancertlscertificate-certificatename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "IsAttached": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-loadbalancertlscertificate.html#cfn-lightsail-loadbalancertlscertificate-isattached", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "LoadBalancerName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-loadbalancertlscertificate.html#cfn-lightsail-loadbalancertlscertificate-loadbalancername", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, "AWS::Lightsail::StaticIp": { "Attributes": { "IpAddress": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Location.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Location.json index 4d767f5282b3d..f44a3b59c2139 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Location.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Location.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Location::Map.MapConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-location-map-mapconfiguration.html", @@ -63,7 +63,7 @@ "PricingPlan": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-location-geofencecollection.html#cfn-location-geofencecollection-pricingplan", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Immutable" }, "PricingPlanDataSource": { @@ -115,7 +115,7 @@ "PricingPlan": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-location-map.html#cfn-location-map-pricingplan", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Immutable" } } @@ -164,7 +164,7 @@ "PricingPlan": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-location-placeindex.html#cfn-location-placeindex-pricingplan", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Immutable" } } @@ -207,7 +207,7 @@ "PricingPlan": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-location-routecalculator.html#cfn-location-routecalculator-pricingplan", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Immutable" } } @@ -250,7 +250,7 @@ "PricingPlan": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-location-tracker.html#cfn-location-tracker-pricingplan", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Immutable" }, "PricingPlanDataSource": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Logs.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Logs.json index fc07ea9f614f2..e1dcc52121fb9 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Logs.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Logs.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Logs::MetricFilter.MetricTransformation": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-logs-metricfilter-metrictransformation.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_LookoutEquipment.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_LookoutEquipment.json index 1b7a74838f773..5fca006330187 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_LookoutEquipment.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_LookoutEquipment.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": {}, "ResourceTypes": { "AWS::LookoutEquipment::InferenceScheduler": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_LookoutMetrics.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_LookoutMetrics.json index c131262d72c13..b24f52901e7ef 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_LookoutMetrics.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_LookoutMetrics.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::LookoutMetrics::Alert.Action": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lookoutmetrics-alert-action.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_LookoutVision.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_LookoutVision.json index 7aa7ee90d896e..433b30fa768b5 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_LookoutVision.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_LookoutVision.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": {}, "ResourceTypes": { "AWS::LookoutVision::Project": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MSK.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MSK.json index b59094231d5b1..040ce87554047 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MSK.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MSK.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::MSK::Cluster.BrokerLogs": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-msk-cluster-brokerlogs.html", @@ -63,7 +63,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-msk-cluster-brokernodegroupinfo.html#cfn-msk-cluster-brokernodegroupinfo-storageinfo", "Required": false, "Type": "StorageInfo", - "UpdateType": "Immutable" + "UpdateType": "Mutable" } } }, @@ -138,11 +138,17 @@ "AWS::MSK::Cluster.EBSStorageInfo": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-msk-cluster-ebsstorageinfo.html", "Properties": { + "ProvisionedThroughput": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-msk-cluster-ebsstorageinfo.html#cfn-msk-cluster-ebsstorageinfo-provisionedthroughput", + "Required": false, + "Type": "ProvisionedThroughput", + "UpdateType": "Mutable" + }, "VolumeSize": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-msk-cluster-ebsstorageinfo.html#cfn-msk-cluster-ebsstorageinfo-volumesize", "PrimitiveType": "Integer", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" } } }, @@ -280,6 +286,23 @@ } } }, + "AWS::MSK::Cluster.ProvisionedThroughput": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-msk-cluster-provisionedthroughput.html", + "Properties": { + "Enabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-msk-cluster-provisionedthroughput.html#cfn-msk-cluster-provisionedthroughput-enabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "VolumeThroughput": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-msk-cluster-provisionedthroughput.html#cfn-msk-cluster-provisionedthroughput-volumethroughput", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::MSK::Cluster.PublicAccess": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-msk-cluster-publicaccess.html", "Properties": { @@ -349,7 +372,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-msk-cluster-storageinfo.html#cfn-msk-cluster-storageinfo-ebsstorageinfo", "Required": false, "Type": "EBSStorageInfo", - "UpdateType": "Immutable" + "UpdateType": "Mutable" } } }, diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MWAA.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MWAA.json index a4b0cc2d0cac3..52cd5a967118e 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MWAA.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MWAA.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::MWAA::Environment.LoggingConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-mwaa-environment-loggingconfiguration.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Macie.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Macie.json index fc77dc43a7452..0e8db89e5cb4b 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Macie.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Macie.json @@ -1,8 +1,11 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Macie::FindingsFilter.Criterion": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-macie-findingsfilter-criterion.html" + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-macie-findingsfilter-criterion.html", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" }, "AWS::Macie::FindingsFilter.FindingCriteria": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-macie-findingsfilter-findingcriteria.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ManagedBlockchain.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ManagedBlockchain.json index 2ea4dacbd3812..904271136bd21 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ManagedBlockchain.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ManagedBlockchain.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::ManagedBlockchain::Member.ApprovalThresholdPolicy": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-managedblockchain-member-approvalthresholdpolicy.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MediaConnect.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MediaConnect.json index c1011b3822fb8..45b553465f7c6 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MediaConnect.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MediaConnect.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::MediaConnect::Flow.Encryption": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-mediaconnect-flow-encryption.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MediaConvert.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MediaConvert.json index 789f20be61802..70f38aee563f6 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MediaConvert.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MediaConvert.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::MediaConvert::JobTemplate.AccelerationSettings": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-mediaconvert-jobtemplate-accelerationsettings.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MediaLive.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MediaLive.json index d6477927136a7..fff54e4f8a1f3 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MediaLive.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MediaLive.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::MediaLive::Channel.AacSettings": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-medialive-channel-aacsettings.html", @@ -2498,6 +2498,12 @@ "Required": false, "UpdateType": "Mutable" }, + "ProgramDateTimeClock": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-medialive-channel-hlsgroupsettings.html#cfn-medialive-channel-hlsgroupsettings-programdatetimeclock", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "ProgramDateTimePeriod": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-medialive-channel-hlsgroupsettings.html#cfn-medialive-channel-hlsgroupsettings-programdatetimeperiod", "PrimitiveType": "Integer", @@ -2900,6 +2906,12 @@ "Type": "NetworkInputSettings", "UpdateType": "Mutable" }, + "Scte35Pid": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-medialive-channel-inputsettings.html#cfn-medialive-channel-inputsettings-scte35pid", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, "Smpte2038DataPreference": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-medialive-channel-inputsettings.html#cfn-medialive-channel-inputsettings-smpte2038datapreference", "PrimitiveType": "String", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MediaPackage.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MediaPackage.json index 80e444a1ef2ab..58e92d64df10d 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MediaPackage.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MediaPackage.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::MediaPackage::Asset.EgressEndpoint": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-mediapackage-asset-egressendpoint.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MediaStore.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MediaStore.json index 4ec158f45b659..004af2d0ec006 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MediaStore.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MediaStore.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::MediaStore::Container.CorsRule": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-mediastore-container-corsrule.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MemoryDB.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MemoryDB.json index 253492dd950ea..2a1d8e58f9c7f 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MemoryDB.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_MemoryDB.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::MemoryDB::Cluster.Endpoint": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-memorydb-cluster-endpoint.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Neptune.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Neptune.json index 6e07877a6fa3b..f71d791a74327 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Neptune.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Neptune.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Neptune::DBCluster.DBClusterRole": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-neptune-dbcluster-dbclusterrole.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NetworkFirewall.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NetworkFirewall.json index a22f4a66f4e2a..57319227e2179 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NetworkFirewall.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NetworkFirewall.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::NetworkFirewall::Firewall.SubnetMapping": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-firewall-subnetmapping.html", @@ -56,7 +56,7 @@ "Properties": { "StatefulDefaultActions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-firewallpolicy-firewallpolicy.html#cfn-networkfirewall-firewallpolicy-firewallpolicy-statefuldefaultactions", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": false, "Type": "List", @@ -70,7 +70,7 @@ }, "StatefulRuleGroupReferences": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-firewallpolicy-firewallpolicy.html#cfn-networkfirewall-firewallpolicy-firewallpolicy-statefulrulegroupreferences", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "ItemType": "StatefulRuleGroupReference", "Required": false, "Type": "List", @@ -78,7 +78,7 @@ }, "StatelessCustomActions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-firewallpolicy-firewallpolicy.html#cfn-networkfirewall-firewallpolicy-firewallpolicy-statelesscustomactions", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "ItemType": "CustomAction", "Required": false, "Type": "List", @@ -86,7 +86,7 @@ }, "StatelessDefaultActions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-firewallpolicy-firewallpolicy.html#cfn-networkfirewall-firewallpolicy-firewallpolicy-statelessdefaultactions", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": true, "Type": "List", @@ -94,7 +94,7 @@ }, "StatelessFragmentDefaultActions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-firewallpolicy-firewallpolicy.html#cfn-networkfirewall-firewallpolicy-firewallpolicy-statelessfragmentdefaultactions", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": true, "Type": "List", @@ -102,7 +102,7 @@ }, "StatelessRuleGroupReferences": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-firewallpolicy-firewallpolicy.html#cfn-networkfirewall-firewallpolicy-firewallpolicy-statelessrulegroupreferences", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "ItemType": "StatelessRuleGroupReference", "Required": false, "Type": "List", @@ -115,7 +115,7 @@ "Properties": { "Dimensions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-firewallpolicy-publishmetricaction.html#cfn-networkfirewall-firewallpolicy-publishmetricaction-dimensions", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "ItemType": "Dimension", "Required": true, "Type": "List", @@ -300,7 +300,7 @@ "Properties": { "Definition": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-ipset.html#cfn-networkfirewall-rulegroup-ipset-definition", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": false, "Type": "List", @@ -313,7 +313,7 @@ "Properties": { "DestinationPorts": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-matchattributes.html#cfn-networkfirewall-rulegroup-matchattributes-destinationports", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "ItemType": "PortRange", "Required": false, "Type": "List", @@ -321,7 +321,7 @@ }, "Destinations": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-matchattributes.html#cfn-networkfirewall-rulegroup-matchattributes-destinations", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "ItemType": "Address", "Required": false, "Type": "List", @@ -329,7 +329,7 @@ }, "Protocols": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-matchattributes.html#cfn-networkfirewall-rulegroup-matchattributes-protocols", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "PrimitiveItemType": "Integer", "Required": false, "Type": "List", @@ -337,7 +337,7 @@ }, "SourcePorts": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-matchattributes.html#cfn-networkfirewall-rulegroup-matchattributes-sourceports", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "ItemType": "PortRange", "Required": false, "Type": "List", @@ -345,7 +345,7 @@ }, "Sources": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-matchattributes.html#cfn-networkfirewall-rulegroup-matchattributes-sources", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "ItemType": "Address", "Required": false, "Type": "List", @@ -353,7 +353,7 @@ }, "TCPFlags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-matchattributes.html#cfn-networkfirewall-rulegroup-matchattributes-tcpflags", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "ItemType": "TCPFlagField", "Required": false, "Type": "List", @@ -383,7 +383,7 @@ "Properties": { "Definition": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-portset.html#cfn-networkfirewall-rulegroup-portset-definition", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": false, "Type": "List", @@ -396,7 +396,7 @@ "Properties": { "Dimensions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-publishmetricaction.html#cfn-networkfirewall-rulegroup-publishmetricaction-dimensions", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "ItemType": "Dimension", "Required": true, "Type": "List", @@ -409,7 +409,7 @@ "Properties": { "Actions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-ruledefinition.html#cfn-networkfirewall-rulegroup-ruledefinition-actions", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": true, "Type": "List", @@ -457,7 +457,7 @@ }, "Settings": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-ruleoption.html#cfn-networkfirewall-rulegroup-ruleoption-settings", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": false, "Type": "List", @@ -501,7 +501,7 @@ }, "StatefulRules": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-rulessource.html#cfn-networkfirewall-rulegroup-rulessource-statefulrules", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "ItemType": "StatefulRule", "Required": false, "Type": "List", @@ -526,7 +526,7 @@ }, "TargetTypes": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-rulessourcelist.html#cfn-networkfirewall-rulegroup-rulessourcelist-targettypes", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": true, "Type": "List", @@ -534,7 +534,7 @@ }, "Targets": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-rulessourcelist.html#cfn-networkfirewall-rulegroup-rulessourcelist-targets", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": true, "Type": "List", @@ -559,7 +559,7 @@ }, "RuleOptions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-statefulrule.html#cfn-networkfirewall-rulegroup-statefulrule-ruleoptions", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "ItemType": "RuleOption", "Required": true, "Type": "List", @@ -600,7 +600,7 @@ "Properties": { "CustomActions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-statelessrulesandcustomactions.html#cfn-networkfirewall-rulegroup-statelessrulesandcustomactions-customactions", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "ItemType": "CustomAction", "Required": false, "Type": "List", @@ -608,7 +608,7 @@ }, "StatelessRules": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-statelessrulesandcustomactions.html#cfn-networkfirewall-rulegroup-statelessrulesandcustomactions-statelessrules", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "ItemType": "StatelessRule", "Required": true, "Type": "List", @@ -621,7 +621,7 @@ "Properties": { "Flags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-tcpflagfield.html#cfn-networkfirewall-rulegroup-tcpflagfield-flags", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": true, "Type": "List", @@ -629,7 +629,7 @@ }, "Masks": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-rulegroup-tcpflagfield.html#cfn-networkfirewall-rulegroup-tcpflagfield-masks", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": false, "Type": "List", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NetworkManager.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NetworkManager.json index 7e58784a9252e..4c33b482ba60c 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NetworkManager.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NetworkManager.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::NetworkManager::Device.Location": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkmanager-device-location.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NimbleStudio.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NimbleStudio.json index 216c612062463..e3885d7c83d38 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NimbleStudio.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NimbleStudio.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::NimbleStudio::LaunchProfile.StreamConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-nimblestudio-launchprofile-streamconfiguration.html", @@ -23,6 +23,18 @@ "Required": false, "UpdateType": "Mutable" }, + "MaxStoppedSessionLengthInMinutes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-nimblestudio-launchprofile-streamconfiguration.html#cfn-nimblestudio-launchprofile-streamconfiguration-maxstoppedsessionlengthinminutes", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Mutable" + }, + "SessionStorage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-nimblestudio-launchprofile-streamconfiguration.html#cfn-nimblestudio-launchprofile-streamconfiguration-sessionstorage", + "Required": false, + "Type": "StreamConfigurationSessionStorage", + "UpdateType": "Mutable" + }, "StreamingImageIds": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-nimblestudio-launchprofile-streamconfiguration.html#cfn-nimblestudio-launchprofile-streamconfiguration-streamingimageids", "PrimitiveItemType": "String", @@ -32,6 +44,41 @@ } } }, + "AWS::NimbleStudio::LaunchProfile.StreamConfigurationSessionStorage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-nimblestudio-launchprofile-streamconfigurationsessionstorage.html", + "Properties": { + "Mode": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-nimblestudio-launchprofile-streamconfigurationsessionstorage.html#cfn-nimblestudio-launchprofile-streamconfigurationsessionstorage-mode", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Root": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-nimblestudio-launchprofile-streamconfigurationsessionstorage.html#cfn-nimblestudio-launchprofile-streamconfigurationsessionstorage-root", + "Required": false, + "Type": "StreamingSessionStorageRoot", + "UpdateType": "Mutable" + } + } + }, + "AWS::NimbleStudio::LaunchProfile.StreamingSessionStorageRoot": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-nimblestudio-launchprofile-streamingsessionstorageroot.html", + "Properties": { + "Linux": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-nimblestudio-launchprofile-streamingsessionstorageroot.html#cfn-nimblestudio-launchprofile-streamingsessionstorageroot-linux", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Windows": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-nimblestudio-launchprofile-streamingsessionstorageroot.html#cfn-nimblestudio-launchprofile-streamingsessionstorageroot-windows", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::NimbleStudio::Studio.StudioEncryptionConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-nimblestudio-studio-studioencryptionconfiguration.html", "Properties": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_OpenSearchService.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_OpenSearchService.json index f4753b6728cb2..6bd4e87f89fd6 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_OpenSearchService.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_OpenSearchService.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::OpenSearchService::Domain.AdvancedSecurityOptionsInput": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-advancedsecurityoptionsinput.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_OpsWorks.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_OpsWorks.json index e9a7d969929e3..8cdffb43dedfd 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_OpsWorks.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_OpsWorks.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::OpsWorks::App.DataSource": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opsworks-app-datasource.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_OpsWorksCM.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_OpsWorksCM.json index 6407b0a13eff9..da29610e153e0 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_OpsWorksCM.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_OpsWorksCM.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::OpsWorksCM::Server.EngineAttribute": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opsworkscm-server-engineattribute.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Panorama.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Panorama.json index f7780de4ffa81..f4115d48e8fd2 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Panorama.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Panorama.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Panorama::ApplicationInstance.ManifestOverridesPayload": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-panorama-applicationinstance-manifestoverridespayload.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Pinpoint.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Pinpoint.json index f936ef091c57d..d062d36a42652 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Pinpoint.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Pinpoint.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Pinpoint::ApplicationSettings.CampaignHook": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-pinpoint-applicationsettings-campaignhook.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_PinpointEmail.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_PinpointEmail.json index 289dbb9112f78..ac71a239bf856 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_PinpointEmail.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_PinpointEmail.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::PinpointEmail::ConfigurationSet.DeliveryOptions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-pinpointemail-configurationset-deliveryoptions.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_QLDB.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_QLDB.json index 69ee160e8aed9..53aa7a31b02f7 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_QLDB.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_QLDB.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::QLDB::Stream.KinesisConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-qldb-stream-kinesisconfiguration.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_QuickSight.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_QuickSight.json index 7f6af37a51dd6..67d769dfb3e29 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_QuickSight.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_QuickSight.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::QuickSight::Analysis.AnalysisError": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-analysis-analysiserror.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RAM.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RAM.json index b7abd7455ed14..a050b1760160a 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RAM.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RAM.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": {}, "ResourceTypes": { "AWS::RAM::ResourceShare": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RDS.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RDS.json index be257feaff05a..462853e0b9402 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RDS.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RDS.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::RDS::DBCluster.DBClusterRole": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-dbcluster-dbclusterrole.html", @@ -913,9 +913,6 @@ }, "Endpoint": { "PrimitiveType": "String" - }, - "VpcId": { - "PrimitiveType": "String" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbproxy.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RUM.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RUM.json index e27e8b1929418..78610335fa62c 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RUM.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RUM.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::RUM::AppMonitor.AppMonitorConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rum-appmonitor-appmonitorconfiguration.html", @@ -84,13 +84,13 @@ "Domain": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rum-appmonitor.html#cfn-rum-appmonitor-domain", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rum-appmonitor.html#cfn-rum-appmonitor-name", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" }, "Tags": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Redshift.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Redshift.json index 7c30e088626f6..0224e5de29143 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Redshift.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Redshift.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Redshift::Cluster.Endpoint": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-redshift-cluster-endpoint.html", @@ -688,6 +688,7 @@ "PrimitiveType": "String" }, "EventCategoriesList": { + "DuplicatesAllowed": false, "PrimitiveItemType": "String", "Type": "List" }, @@ -712,6 +713,7 @@ }, "EventCategories": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-eventsubscription.html#cfn-redshift-eventsubscription-eventcategories", + "DuplicatesAllowed": false, "PrimitiveItemType": "String", "Required": false, "Type": "List", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RefactorSpaces.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RefactorSpaces.json index 27da931caf75f..b8609b274788b 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RefactorSpaces.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RefactorSpaces.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::RefactorSpaces::Application.ApiGatewayProxyInput": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-refactorspaces-application-apigatewayproxyinput.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Rekognition.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Rekognition.json index 12780b8a4fcf6..393ec110238b9 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Rekognition.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Rekognition.json @@ -1,7 +1,31 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": {}, "ResourceTypes": { + "AWS::Rekognition::Collection": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rekognition-collection.html", + "Properties": { + "CollectionId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rekognition-collection.html#cfn-rekognition-collection-collectionid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rekognition-collection.html#cfn-rekognition-collection-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::Rekognition::Project": { "Attributes": { "Arn": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ResilienceHub.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ResilienceHub.json index 7f57e440f6e3d..b59f73f3e47b7 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ResilienceHub.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ResilienceHub.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::ResilienceHub::App.PhysicalResourceId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resiliencehub-app-physicalresourceid.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ResourceGroups.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ResourceGroups.json index 6b20961efaba9..3a24795fea44d 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ResourceGroups.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ResourceGroups.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::ResourceGroups::Group.ConfigurationItem": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resourcegroups-group-configurationitem.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RoboMaker.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RoboMaker.json index f1dc11ec946cf..8dfdce78cb58e 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RoboMaker.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RoboMaker.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::RoboMaker::RobotApplication.RobotSoftwareSuite": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-robomaker-robotapplication-robotsoftwaresuite.html", @@ -13,7 +13,7 @@ "Version": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-robomaker-robotapplication-robotsoftwaresuite.html#cfn-robomaker-robotapplication-robotsoftwaresuite-version", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" } } @@ -70,7 +70,7 @@ "Version": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-robomaker-simulationapplication-robotsoftwaresuite.html#cfn-robomaker-simulationapplication-robotsoftwaresuite-version", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" } } @@ -87,7 +87,7 @@ "Version": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-robomaker-simulationapplication-simulationsoftwaresuite.html#cfn-robomaker-simulationapplication-simulationsoftwaresuite-version", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" } } @@ -198,6 +198,12 @@ "Required": false, "UpdateType": "Mutable" }, + "Environment": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-robomaker-robotapplication.html#cfn-robomaker-robotapplication-environment", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-robomaker-robotapplication.html#cfn-robomaker-robotapplication-name", "PrimitiveType": "String", @@ -208,19 +214,20 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-robomaker-robotapplication.html#cfn-robomaker-robotapplication-robotsoftwaresuite", "Required": true, "Type": "RobotSoftwareSuite", - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "Sources": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-robomaker-robotapplication.html#cfn-robomaker-robotapplication-sources", "ItemType": "SourceConfig", - "Required": true, + "Required": false, "Type": "List", "UpdateType": "Mutable" }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-robomaker-robotapplication.html#cfn-robomaker-robotapplication-tags", - "PrimitiveType": "Json", + "PrimitiveItemType": "String", "Required": false, + "Type": "Map", "UpdateType": "Mutable" } } diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Route53.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Route53.json index d5962dd8539df..9d0a3dce9054e 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Route53.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Route53.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Route53::HealthCheck.HealthCheckTag": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-healthchecktag.html", @@ -175,12 +175,6 @@ "Type": "AliasTarget", "UpdateType": "Mutable" }, - "Comment": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-recordset.html#cfn-route53-recordset-comment", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, "Failover": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-recordset.html#cfn-route53-recordset-failover", "PrimitiveType": "String", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Route53RecoveryControl.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Route53RecoveryControl.json index bfba22f4b7eb2..8948c2f8ece57 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Route53RecoveryControl.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Route53RecoveryControl.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Route53RecoveryControl::Cluster.ClusterEndpoint": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53recoverycontrol-cluster-clusterendpoint.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Route53RecoveryReadiness.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Route53RecoveryReadiness.json index ff940848a67ca..891c3809aa7cd 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Route53RecoveryReadiness.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Route53RecoveryReadiness.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Route53RecoveryReadiness::ResourceSet.DNSTargetResource": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53recoveryreadiness-resourceset-dnstargetresource.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Route53Resolver.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Route53Resolver.json index 56a57bba38b8e..366819350db7a 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Route53Resolver.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Route53Resolver.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Route53Resolver::FirewallRuleGroup.FirewallRule": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53resolver-firewallrulegroup-firewallrule.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_S3.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_S3.json index 13ac2d46ffcca..9dd70bdfefcf5 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_S3.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_S3.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::S3::AccessPoint.PublicAccessBlockConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-accesspoint-publicaccessblockconfiguration.html", @@ -1425,7 +1425,10 @@ } }, "AWS::S3::StorageLens.Encryption": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-storagelens-encryption.html" + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-storagelens-encryption.html", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" }, "AWS::S3::StorageLens.PrefixLevel": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-storagelens-prefixlevel.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_S3ObjectLambda.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_S3ObjectLambda.json index fdbb700a1ce2b..fd4c73fae620a 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_S3ObjectLambda.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_S3ObjectLambda.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::S3ObjectLambda::AccessPoint.ObjectLambdaConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3objectlambda-accesspoint-objectlambdaconfiguration.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_S3Outposts.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_S3Outposts.json index 9b946aa3347f9..46aef2579ca49 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_S3Outposts.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_S3Outposts.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::S3Outposts::AccessPoint.VpcConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3outposts-accesspoint-vpcconfiguration.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SDB.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SDB.json index 9644ed554308b..f71dca6a334c8 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SDB.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SDB.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": {}, "ResourceTypes": { "AWS::SDB::Domain": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SES.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SES.json index 397068c73789a..3b5cdb98888da 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SES.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SES.json @@ -1,11 +1,12 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::SES::ConfigurationSetEventDestination.CloudWatchDestination": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ses-configurationseteventdestination-cloudwatchdestination.html", "Properties": { "DimensionConfigurations": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ses-configurationseteventdestination-cloudwatchdestination.html#cfn-ses-configurationseteventdestination-cloudwatchdestination-dimensionconfigurations", + "DuplicatesAllowed": true, "ItemType": "DimensionConfiguration", "Required": false, "Type": "List", @@ -59,6 +60,7 @@ }, "MatchingEventTypes": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ses-configurationseteventdestination-eventdestination.html#cfn-ses-configurationseteventdestination-eventdestination-matchingeventtypes", + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": true, "Type": "List", @@ -409,7 +411,7 @@ "SubjectPart": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ses-template-template.html#cfn-ses-template-template-subjectpart", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "TemplateName": { @@ -440,6 +442,11 @@ } }, "AWS::SES::ConfigurationSetEventDestination": { + "Attributes": { + "Id": { + "PrimitiveType": "String" + } + }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ses-configurationseteventdestination.html", "Properties": { "ConfigurationSetName": { @@ -533,6 +540,11 @@ } }, "AWS::SES::Template": { + "Attributes": { + "Id": { + "PrimitiveType": "String" + } + }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ses-template.html", "Properties": { "Template": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SNS.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SNS.json index 02c7e403852b0..286eed74fcfce 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SNS.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SNS.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::SNS::Topic.Subscription": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sns-subscription.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SQS.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SQS.json index 6aab0c995c982..de585ddae1135 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SQS.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SQS.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": {}, "ResourceTypes": { "AWS::SQS::Queue": { @@ -9,90 +9,93 @@ }, "QueueName": { "PrimitiveType": "String" + }, + "QueueUrl": { + "PrimitiveType": "String" } }, - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html", "Properties": { "ContentBasedDeduplication": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-contentbaseddeduplication", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-contentbaseddeduplication", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Mutable" }, "DeduplicationScope": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-deduplicationscope", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-deduplicationscope", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" }, "DelaySeconds": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-delayseconds", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-delayseconds", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "FifoQueue": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-fifoqueue", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-fifoqueue", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Immutable" }, "FifoThroughputLimit": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-fifothroughputlimit", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-fifothroughputlimit", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" }, "KmsDataKeyReusePeriodSeconds": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-kmsdatakeyreuseperiodseconds", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-kmsdatakeyreuseperiodseconds", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "KmsMasterKeyId": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-kmsmasterkeyid", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-kmsmasterkeyid", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" }, "MaximumMessageSize": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-maxmesgsize", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-maximummessagesize", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "MessageRetentionPeriod": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-msgretentionperiod", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-messageretentionperiod", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "QueueName": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-name", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-queuename", "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" }, "ReceiveMessageWaitTimeSeconds": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-receivemsgwaittime", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-receivemessagewaittimeseconds", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "RedriveAllowPolicy": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-redriveallowpolicy", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-redriveallowpolicy", "PrimitiveType": "Json", "Required": false, "UpdateType": "Mutable" }, "RedrivePolicy": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-redrive", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-redrivepolicy", "PrimitiveType": "Json", "Required": false, "UpdateType": "Mutable" }, "Tags": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#cfn-sqs-queue-tags", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-tags", "DuplicatesAllowed": true, "ItemType": "Tag", "Required": false, @@ -100,7 +103,7 @@ "UpdateType": "Mutable" }, "VisibilityTimeout": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-visiblitytimeout", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html#cfn-sqs-queue-visibilitytimeout", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SSM.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SSM.json index 3d8dd0f30e8cf..6d6a39580ad8d 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SSM.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SSM.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::SSM::Association.InstanceAssociationOutputLocation": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-association-instanceassociationoutputlocation.html", @@ -112,6 +112,23 @@ } } }, + "AWS::SSM::MaintenanceWindowTask.CloudWatchOutputConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-maintenancewindowtask-cloudwatchoutputconfig.html", + "Properties": { + "CloudWatchLogGroupName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-maintenancewindowtask-cloudwatchoutputconfig.html#cfn-ssm-maintenancewindowtask-cloudwatchoutputconfig-cloudwatchloggroupname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "CloudWatchOutputEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-maintenancewindowtask-cloudwatchoutputconfig.html#cfn-ssm-maintenancewindowtask-cloudwatchoutputconfig-cloudwatchoutputenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::SSM::MaintenanceWindowTask.LoggingInfo": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-maintenancewindowtask-logginginfo.html", "Properties": { @@ -178,6 +195,12 @@ "AWS::SSM::MaintenanceWindowTask.MaintenanceWindowRunCommandParameters": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-maintenancewindowtask-maintenancewindowruncommandparameters.html", "Properties": { + "CloudWatchOutputConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-maintenancewindowtask-maintenancewindowruncommandparameters.html#cfn-ssm-maintenancewindowtask-maintenancewindowruncommandparameters-cloudwatchoutputconfig", + "Required": false, + "Type": "CloudWatchOutputConfig", + "UpdateType": "Mutable" + }, "Comment": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-maintenancewindowtask-maintenancewindowruncommandparameters.html#cfn-ssm-maintenancewindowtask-maintenancewindowruncommandparameters-comment", "PrimitiveType": "String", @@ -196,6 +219,12 @@ "Required": false, "UpdateType": "Mutable" }, + "DocumentVersion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-maintenancewindowtask-maintenancewindowruncommandparameters.html#cfn-ssm-maintenancewindowtask-maintenancewindowruncommandparameters-documentversion", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "NotificationConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-maintenancewindowtask-maintenancewindowruncommandparameters.html#cfn-ssm-maintenancewindowtask-maintenancewindowruncommandparameters-notificationconfig", "Required": false, @@ -377,7 +406,10 @@ } }, "AWS::SSM::PatchBaseline.PatchStringDate": { - "PrimitiveType": "String" + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-patchbaseline-patchstringdate.html", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" }, "AWS::SSM::PatchBaseline.Rule": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-patchbaseline-rule.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SSMContacts.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SSMContacts.json index 0e21e2dd610ce..9b25460f109b8 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SSMContacts.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SSMContacts.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::SSMContacts::Contact.ChannelTargetInfo": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssmcontacts-contact-channeltargetinfo.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SSMIncidents.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SSMIncidents.json index adb3f2ccd9b5b..1ab7ee56977f8 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SSMIncidents.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SSMIncidents.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::SSMIncidents::ReplicationSet.RegionConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssmincidents-replicationset-regionconfiguration.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SSO.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SSO.json index 393ac6eb62393..d6824eb79e157 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SSO.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SSO.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::SSO::InstanceAccessControlAttributeConfiguration.AccessControlAttribute": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sso-instanceaccesscontrolattributeconfiguration-accesscontrolattribute.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SageMaker.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SageMaker.json index 6c48ebab0d9ae..19ec06474df06 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SageMaker.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SageMaker.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::SageMaker::App.ResourceSpec": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-app-resourcespec.html", @@ -2674,7 +2674,6 @@ "Properties": { "Device": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-device.html#cfn-sagemaker-device-device", - "PrimitiveType": "Json", "Required": false, "Type": "Device", "UpdateType": "Mutable" @@ -3560,6 +3559,12 @@ "AWS::SageMaker::Pipeline": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-pipeline.html", "Properties": { + "ParallelismConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-pipeline.html#cfn-sagemaker-pipeline-parallelismconfiguration", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" + }, "PipelineDefinition": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-pipeline.html#cfn-sagemaker-pipeline-pipelinedefinition", "PrimitiveType": "Json", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SecretsManager.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SecretsManager.json index d403883bf7782..af2c61c10aae3 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SecretsManager.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SecretsManager.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::SecretsManager::RotationSchedule.HostedRotationLambda": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-rotationschedule-hostedrotationlambda.html", @@ -68,6 +68,18 @@ "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" + }, + "Duration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-rotationschedule-rotationrules.html#cfn-secretsmanager-rotationschedule-rotationrules-duration", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ScheduleExpression": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-rotationschedule-rotationrules.html#cfn-secretsmanager-rotationschedule-rotationrules-scheduleexpression", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" } } }, @@ -187,6 +199,12 @@ "Type": "HostedRotationLambda", "UpdateType": "Mutable" }, + "RotateImmediatelyOnUpdate": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-rotationschedule.html#cfn-secretsmanager-rotationschedule-rotateimmediatelyonupdate", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "RotationLambdaARN": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-rotationschedule.html#cfn-secretsmanager-rotationschedule-rotationlambdaarn", "PrimitiveType": "String", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SecurityHub.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SecurityHub.json index cab99669df416..149af566d50b2 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SecurityHub.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SecurityHub.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": {}, "ResourceTypes": { "AWS::SecurityHub::Hub": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ServiceCatalog.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ServiceCatalog.json index 11ceb02c1e8a9..6187140513e4b 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ServiceCatalog.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ServiceCatalog.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::ServiceCatalog::CloudFormationProduct.ProvisioningArtifactProperties": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicecatalog-cloudformationproduct-provisioningartifactproperties.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ServiceCatalogAppRegistry.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ServiceCatalogAppRegistry.json index dc7b2564fd7dd..72bfde85e55ce 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ServiceCatalogAppRegistry.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ServiceCatalogAppRegistry.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": {}, "ResourceTypes": { "AWS::ServiceCatalogAppRegistry::Application": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ServiceDiscovery.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ServiceDiscovery.json index a7ccbaca17162..7c522423621a5 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ServiceDiscovery.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ServiceDiscovery.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::ServiceDiscovery::PrivateDnsNamespace.PrivateDnsPropertiesMutable": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-privatednsnamespace-privatednspropertiesmutable.html", @@ -204,6 +204,9 @@ "Arn": { "PrimitiveType": "String" }, + "HostedZoneId": { + "PrimitiveType": "String" + }, "Id": { "PrimitiveType": "String" } @@ -248,6 +251,9 @@ "Arn": { "PrimitiveType": "String" }, + "HostedZoneId": { + "PrimitiveType": "String" + }, "Id": { "PrimitiveType": "String" } diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Signer.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Signer.json index 476cea51a1bff..c7d746876974e 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Signer.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Signer.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Signer::SigningProfile.SignatureValidityPeriod": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-signer-signingprofile-signaturevalidityperiod.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_StepFunctions.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_StepFunctions.json index 7766fb34ff2b1..3cd3765e740b6 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_StepFunctions.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_StepFunctions.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::StepFunctions::Activity.TagsEntry": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-activity-tagsentry.html", @@ -30,7 +30,10 @@ } }, "AWS::StepFunctions::StateMachine.Definition": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-definition.html" + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-definition.html", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" }, "AWS::StepFunctions::StateMachine.LogDestination": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-logdestination.html", @@ -205,7 +208,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html#cfn-stepfunctions-statemachine-statemachinetype", "PrimitiveType": "String", "Required": false, - "UpdateType": "Mutable" + "UpdateType": "Immutable" }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html#cfn-stepfunctions-statemachine-tags", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Synthetics.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Synthetics.json index e4af4726fc3c7..f72293374c3a9 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Synthetics.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Synthetics.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Synthetics::Canary.ArtifactConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-synthetics-canary-artifactconfig.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Timestream.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Timestream.json index 85528a70118c0..81eed7b51d5d9 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Timestream.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Timestream.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Timestream::ScheduledQuery.DimensionMapping": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-timestream-scheduledquery-dimensionmapping.html", @@ -365,6 +365,12 @@ "Required": true, "UpdateType": "Immutable" }, + "MagneticStoreWriteProperties": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-timestream-table.html#cfn-timestream-table-magneticstorewriteproperties", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" + }, "RetentionProperties": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-timestream-table.html#cfn-timestream-table-retentionproperties", "PrimitiveType": "Json", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Transfer.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Transfer.json index 1db8b7569e709..10d69a62cd8ef 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Transfer.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Transfer.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Transfer::Server.EndpointDetails": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-transfer-server-endpointdetails.html", @@ -69,7 +69,10 @@ } }, "AWS::Transfer::Server.Protocol": { - "PrimitiveType": "String" + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-transfer-server-protocol.html", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" }, "AWS::Transfer::Server.ProtocolDetails": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-transfer-server-protocoldetails.html", @@ -79,6 +82,12 @@ "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" + }, + "TlsSessionResumptionMode": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-transfer-server-protocoldetails.html#cfn-transfer-server-protocoldetails-tlssessionresumptionmode", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" } } }, @@ -153,7 +162,10 @@ } }, "AWS::Transfer::User.SshPublicKey": { - "PrimitiveType": "String" + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-transfer-user-sshpublickey.html", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" }, "AWS::Transfer::Workflow.WorkflowStep": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-transfer-workflow-workflowstep.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_WAF.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_WAF.json index 8d37cb0415d5f..b32171fa40dfc 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_WAF.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_WAF.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::WAF::ByteMatchSet.ByteMatchTuple": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-waf-bytematchset-bytematchtuples.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_WAFRegional.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_WAFRegional.json index 99b05bc44c1bf..ec92e998a053a 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_WAFRegional.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_WAFRegional.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::WAFRegional::ByteMatchSet.ByteMatchTuple": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafregional-bytematchset-bytematchtuple.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_WAFv2.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_WAFv2.json index 182513fb99ac8..487e628288ea0 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_WAFv2.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_WAFv2.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::WAFv2::LoggingConfiguration.FieldToMatch": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-loggingconfiguration-fieldtomatch.html", @@ -898,6 +898,17 @@ } } }, + "AWS::WAFv2::WebACL.FieldIdentifier": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-fieldidentifier.html", + "Properties": { + "Identifier": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-fieldidentifier.html#cfn-wafv2-webacl-fieldidentifier-identifier", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::WAFv2::WebACL.FieldToMatch": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-fieldtomatch.html", "Properties": { @@ -1106,6 +1117,35 @@ } } }, + "AWS::WAFv2::WebACL.ManagedRuleGroupConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-managedrulegroupconfig.html", + "Properties": { + "LoginPath": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-managedrulegroupconfig.html#cfn-wafv2-webacl-managedrulegroupconfig-loginpath", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "PasswordField": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-managedrulegroupconfig.html#cfn-wafv2-webacl-managedrulegroupconfig-passwordfield", + "Required": false, + "Type": "FieldIdentifier", + "UpdateType": "Mutable" + }, + "PayloadType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-managedrulegroupconfig.html#cfn-wafv2-webacl-managedrulegroupconfig-payloadtype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "UsernameField": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-managedrulegroupconfig.html#cfn-wafv2-webacl-managedrulegroupconfig-usernamefield", + "Required": false, + "Type": "FieldIdentifier", + "UpdateType": "Mutable" + } + } + }, "AWS::WAFv2::WebACL.ManagedRuleGroupStatement": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-managedrulegroupstatement.html", "Properties": { @@ -1116,6 +1156,13 @@ "Type": "List", "UpdateType": "Mutable" }, + "ManagedRuleGroupConfigs": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-managedrulegroupstatement.html#cfn-wafv2-webacl-managedrulegroupstatement-managedrulegroupconfigs", + "ItemType": "ManagedRuleGroupConfig", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-managedrulegroupstatement.html#cfn-wafv2-webacl-managedrulegroupstatement-name", "PrimitiveType": "String", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Wisdom.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Wisdom.json index be28ec3aa48cd..672d8426f633c 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Wisdom.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Wisdom.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::Wisdom::Assistant.ServerSideEncryptionConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wisdom-assistant-serversideencryptionconfiguration.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_WorkSpaces.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_WorkSpaces.json index b0be0061fce3e..cf29d5c6e85d4 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_WorkSpaces.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_WorkSpaces.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::WorkSpaces::ConnectionAlias.ConnectionAliasAssociation": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-workspaces-connectionalias-connectionaliasassociation.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_XRay.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_XRay.json index 47777483c0b07..babcdda175b6a 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_XRay.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_XRay.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "AWS::XRay::Group.InsightsConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-xray-group-insightsconfiguration.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_Alexa_ASK.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_Alexa_ASK.json index d16ed81e4dbf4..855e4b0e571ff 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_Alexa_ASK.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_Alexa_ASK.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "Alexa::ASK::Skill.AuthenticationConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ask-skill-authenticationconfiguration.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_Tag.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_Tag.json index 5cfd5af565437..db024dbb58fe5 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_Tag.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_Tag.json @@ -1,5 +1,5 @@ { - "$version": "51.0.0", + "$version": "57.0.0", "PropertyTypes": { "Tag": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/001_Version.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/001_Version.json index 376194e28bb48..2055344e0a733 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/001_Version.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/001_Version.json @@ -1,3 +1,3 @@ { - "ResourceSpecificationVersion": "51.0.0" + "ResourceSpecificationVersion": "57.0.0" } diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/660_Route53_HealthCheck_patch.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/660_Route53_HealthCheck_patch.json index b03232b4e72a5..4ff2061df1a22 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/660_Route53_HealthCheck_patch.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/660_Route53_HealthCheck_patch.json @@ -118,6 +118,12 @@ "Required": false, "UpdateType": "Mutable" }, + "RoutingControlArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-healthcheckconfig.html#cfn-route53-healthcheck-healthcheckconfig-routingcontrolarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "SearchString": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-healthcheckconfig.html#cfn-route53-healthcheck-healthcheckconfig-searchstring", "PrimitiveType": "String", @@ -154,4 +160,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/100_sam/000_official/spec.json b/packages/@aws-cdk/cfnspec/spec-source/specification/100_sam/000_official/spec.json index 5510475b3e7d2..71c213777a0cf 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/100_sam/000_official/spec.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/100_sam/000_official/spec.json @@ -1972,6 +1972,13 @@ "AWS::Serverless::Function": { "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction", "Properties": { + "Architectures": { + "Documentation": "https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html#sam-function-architectures", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, "AssumeRolePolicyDocument": { "Documentation": "https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html#sam-function-assumerolepolicydocument", "PrimitiveType": "Json", diff --git a/packages/@aws-cdk/cloud-assembly-schema/CONTRIBUTING.md b/packages/@aws-cdk/cloud-assembly-schema/CONTRIBUTING.md index 0938fdb9e465f..1047e61261058 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/CONTRIBUTING.md +++ b/packages/@aws-cdk/cloud-assembly-schema/CONTRIBUTING.md @@ -59,7 +59,7 @@ This means that breaking changes will be rejected. These include: - Changing the type of the property. In addition, the interfaces defined here are programatically exposed to users, via the `manifest` -property of the [`CloudAssembly`]((../cx-api/lib/cloud-assembly.ts)) class. This means that the following are +property of the [`CloudAssembly`](../cx-api/lib/cloud-assembly.ts) class. This means that the following are also considered breaking changes: - Changing a property from *required* to *optional*. diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/assets/docker-image-asset.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/assets/docker-image-asset.ts index 654e1aa032926..5b3efeee0b375 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/assets/docker-image-asset.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/assets/docker-image-asset.ts @@ -62,6 +62,15 @@ export interface DockerImageSource { * @default - No additional build arguments */ readonly dockerBuildArgs?: { [name: string]: string }; + + /** + * Networking mode for the RUN commands during build. _Requires Docker Engine API v1.25+_. + * + * Specify this property to build images on a specific networking mode. + * + * @default - no networking mode specified + */ + readonly networkMode?: string; } /** diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts index 9bf124c31c71d..4d98b3a29bb32 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts @@ -1,4 +1,37 @@ +/** + * Information needed to access an IAM role created + * as part of the bootstrap process + */ +export interface BootstrapRole { + /** + * The ARN of the IAM role created as part of bootrapping + * e.g. lookupRoleArn + */ + readonly arn: string; + + /** + * External ID to use when assuming the bootstrap role + * + * @default - No external ID + */ + readonly assumeRoleExternalId?: string; + + /** + * Version of bootstrap stack required to use this role + * + * @default - No bootstrap stack required + */ + readonly requiresBootstrapStackVersion?: number; + + /** + * Name of SSM parameter with bootstrap stack version + * + * @default - Discover SSM parameter by reading stack + */ + readonly bootstrapStackVersionSsmParameter?: string; +} + /** * Artifact properties for CloudFormation stacks. */ @@ -56,6 +89,13 @@ export interface AwsCloudFormationStackProperties { */ readonly cloudFormationExecutionRoleArn?: string; + /** + * The role to use to look up values from the target AWS account + * + * @default - No role is assumed (current credentials are used) + */ + readonly lookupRole?: BootstrapRole; + /** * If the stack template has already been included in the asset manifest, its asset URL * diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts index 97ede25af2498..e82f8ee6f5ee3 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts @@ -53,6 +53,11 @@ export enum ContextProvider { * KMS Key Provider */ KEY_PROVIDER = 'key-provider', + + /** + * A plugin provider (the actual plugin name will be in the properties) + */ + PLUGIN = 'plugin', } /** @@ -461,7 +466,24 @@ export interface KeyContextQuery { * Alias name used to search the Key */ readonly aliasName: string; +} + +/** + * Query input for plugins + * + * This alternate branch is necessary because it needs to be able to escape all type checking + * we do on on the cloud assembly -- we cannot know the properties that will be used a priori. + */ +export interface PluginContextQuery { + /** + * The name of the plugin + */ + readonly pluginName: string; + /** + * Arbitrary other arguments for the plugin + */ + [key: string]: any; } export type ContextQueryProperties = AmiContextQuery @@ -473,4 +495,6 @@ export type ContextQueryProperties = AmiContextQuery | LoadBalancerContextQuery | LoadBalancerListenerContextQuery | SecurityGroupContextQuery -| KeyContextQuery; +| KeyContextQuery +| PluginContextQuery; + diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts index 5e9e4e1517145..b58d02849bd9c 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts @@ -131,6 +131,13 @@ export interface ContainerImageAssetMetadataEntry extends BaseAssetMetadataEntry * @default - no file is passed */ readonly file?: string; + + /** + * Networking mode for the RUN commands during build. + * + * @default - no networking mode specified + */ + readonly networkMode?: string; } /** diff --git a/packages/@aws-cdk/cloud-assembly-schema/package.json b/packages/@aws-cdk/cloud-assembly-schema/package.json index ca0e96bd8df24..8ac885479f264 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/package.json +++ b/packages/@aws-cdk/cloud-assembly-schema/package.json @@ -62,12 +62,12 @@ "devDependencies": { "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "@types/mock-fs": "^4.13.1", "@types/semver": "^7.3.9", - "jest": "^27.4.5", + "jest": "^27.5.1", "mock-fs": "^4.14.0", - "typescript-json-schema": "^0.52.0" + "typescript-json-schema": "^0.53.0" }, "repository": { "url": "https://github.com/aws/aws-cdk.git", diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/assets.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/assets.schema.json index 995a895ad824d..40134a4e554a5 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/assets.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/assets.schema.json @@ -154,6 +154,10 @@ "additionalProperties": { "type": "string" } + }, + "networkMode": { + "description": "Networking mode for the RUN commands during build. _Requires Docker Engine API v1.25+_.\n\nSpecify this property to build images on a specific networking mode. (Default - no networking mode specified)", + "type": "string" } } }, diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json index 9241ae62ef0ff..19ab465985d24 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json @@ -226,6 +226,10 @@ "description": "Path to the Dockerfile (relative to the directory). (Default - no file is passed)", "type": "string" }, + "networkMode": { + "description": "Networking mode for the RUN commands during build. (Default - no networking mode specified)", + "type": "string" + }, "id": { "description": "Logical identifier for the asset", "type": "string" @@ -307,6 +311,10 @@ "description": "The role that is passed to CloudFormation to execute the change set (Default - No role is passed (currently assumed role/credentials are used))", "type": "string" }, + "lookupRole": { + "description": "The role to use to look up values from the target AWS account (Default - No role is assumed (current credentials are used))", + "$ref": "#/definitions/BootstrapRole" + }, "stackTemplateAssetObjectUrl": { "description": "If the stack template has already been included in the asset manifest, its asset URL (Default - Not uploaded yet, upload just before deploying)", "type": "string" @@ -328,6 +336,31 @@ "templateFile" ] }, + "BootstrapRole": { + "description": "Information needed to access an IAM role created\nas part of the bootstrap process", + "type": "object", + "properties": { + "arn": { + "description": "The ARN of the IAM role created as part of bootrapping\ne.g. lookupRoleArn", + "type": "string" + }, + "assumeRoleExternalId": { + "description": "External ID to use when assuming the bootstrap role (Default - No external ID)", + "type": "string" + }, + "requiresBootstrapStackVersion": { + "description": "Version of bootstrap stack required to use this role (Default - No bootstrap stack required)", + "type": "number" + }, + "bootstrapStackVersionSsmParameter": { + "description": "Name of SSM parameter with bootstrap stack version (Default - Discover SSM parameter by reading stack)", + "type": "string" + } + }, + "required": [ + "arn" + ] + }, "AssetManifestProperties": { "description": "Artifact properties for the Asset Manifest", "type": "object", @@ -423,6 +456,9 @@ }, { "$ref": "#/definitions/KeyContextQuery" + }, + { + "$ref": "#/definitions/PluginContextQuery" } ] } @@ -443,6 +479,7 @@ "key-provider", "load-balancer", "load-balancer-listener", + "plugin", "security-group", "ssm", "vpc-provider" @@ -598,7 +635,7 @@ } }, "returnAsymmetricSubnets": { - "description": "Whether to populate the subnetGroups field of the {@link VpcContextResponse},\nwhich contains potentially asymmetric subnet groups.", + "description": "Whether to populate the subnetGroups field of the{@linkVpcContextResponse},\nwhich contains potentially asymmetric subnet groups.", "default": false, "type": "boolean" }, @@ -805,6 +842,20 @@ "region" ] }, + "PluginContextQuery": { + "description": "Query input for plugins\n\nThis alternate branch is necessary because it needs to be able to escape all type checking\nwe do on on the cloud assembly -- we cannot know the properties that will be used a priori.", + "type": "object", + "additionalProperties": {}, + "properties": { + "pluginName": { + "description": "The name of the plugin", + "type": "string" + } + }, + "required": [ + "pluginName" + ] + }, "RuntimeInfo": { "description": "Information about the application's runtime components.", "type": "object", diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json index 01d4f111912e9..ae7a33e962d0b 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json @@ -1 +1 @@ -{"version":"15.0.0"} \ No newline at end of file +{"version":"16.0.0"} diff --git a/packages/@aws-cdk/cloudformation-diff/lib/format-table.ts b/packages/@aws-cdk/cloudformation-diff/lib/format-table.ts index 0799243ba954e..7e7afb98cfa53 100644 --- a/packages/@aws-cdk/cloudformation-diff/lib/format-table.ts +++ b/packages/@aws-cdk/cloudformation-diff/lib/format-table.ts @@ -1,5 +1,5 @@ -import * as colors from 'colors/safe'; -import * as stringWidth from 'string-width'; +import * as chalk from 'chalk'; +import stringWidth from 'string-width'; import * as table from 'table'; /** @@ -93,7 +93,7 @@ function sum(xs: number[]): number { } // What color the table is going to be -const tableColor = colors.gray; +const tableColor = chalk.gray; // Unicode table characters with a color const TABLE_BORDER_CHARACTERS = { diff --git a/packages/@aws-cdk/cloudformation-diff/lib/format.ts b/packages/@aws-cdk/cloudformation-diff/lib/format.ts index 7bc19f560d666..3dee563f8cf36 100644 --- a/packages/@aws-cdk/cloudformation-diff/lib/format.ts +++ b/packages/@aws-cdk/cloudformation-diff/lib/format.ts @@ -1,5 +1,5 @@ import { format } from 'util'; -import * as colors from 'colors/safe'; +import * as chalk from 'chalk'; import { Difference, isPropertyDifference, ResourceDifference, ResourceImpact } from './diff-template'; import { DifferenceCollection, TemplateDiff } from './diff/types'; import { deepEqual } from './diff/util'; @@ -75,10 +75,10 @@ function formatSecurityChangesWithBanner(formatter: Formatter, templateDiff: Tem formatter.printSectionFooter(); } -const ADDITION = colors.green('[+]'); -const CONTEXT = colors.grey('[ ]'); -const UPDATE = colors.yellow('[~]'); -const REMOVAL = colors.red('[-]'); +const ADDITION = chalk.green('[+]'); +const CONTEXT = chalk.grey('[ ]'); +const UPDATE = chalk.yellow('[~]'); +const REMOVAL = chalk.red('[-]'); class Formatter { constructor( @@ -93,11 +93,11 @@ class Formatter { } public print(fmt: string, ...args: any[]) { - this.stream.write(colors.white(format(fmt, ...args)) + '\n'); + this.stream.write(chalk.white(format(fmt, ...args)) + '\n'); } public warning(fmt: string, ...args: any[]) { - this.stream.write(colors.yellow(format(fmt, ...args)) + '\n'); + this.stream.write(chalk.yellow(format(fmt, ...args)) + '\n'); } public formatSection>( @@ -116,7 +116,7 @@ class Formatter { } public printSectionHeader(title: string) { - this.print(colors.underline(colors.bold(title))); + this.print(chalk.underline(chalk.bold(title))); } public printSectionFooter() { @@ -134,8 +134,8 @@ class Formatter { let value; - const oldValue = this.formatValue(diff.oldValue, colors.red); - const newValue = this.formatValue(diff.newValue, colors.green); + const oldValue = this.formatValue(diff.oldValue, chalk.red); + const newValue = this.formatValue(diff.newValue, chalk.green); if (diff.isAddition) { value = newValue; } else if (diff.isUpdate) { @@ -144,7 +144,7 @@ class Formatter { value = oldValue; } - this.print(`${this.formatPrefix(diff)} ${colors.cyan(type)} ${this.formatLogicalId(logicalId)}: ${value}`); + this.print(`${this.formatPrefix(diff)} ${chalk.cyan(type)} ${this.formatLogicalId(logicalId)}: ${value}`); } /** @@ -159,7 +159,7 @@ class Formatter { const resourceType = diff.isRemoval ? diff.oldResourceType : diff.newResourceType; // eslint-disable-next-line max-len - this.print(`${this.formatPrefix(diff)} ${this.formatValue(resourceType, colors.cyan)} ${this.formatLogicalId(logicalId)} ${this.formatImpact(diff.changeImpact)}`); + this.print(`${this.formatPrefix(diff)} ${this.formatValue(resourceType, chalk.cyan)} ${this.formatLogicalId(logicalId)} ${this.formatImpact(diff.changeImpact)}`); if (diff.isUpdate) { const differenceCount = diff.differenceCount; @@ -175,7 +175,7 @@ class Formatter { if (diff.isAddition) { return ADDITION; } if (diff.isUpdate) { return UPDATE; } if (diff.isRemoval) { return REMOVAL; } - return colors.white('[?]'); + return chalk.white('[?]'); } /** @@ -197,13 +197,13 @@ class Formatter { public formatImpact(impact: ResourceImpact) { switch (impact) { case ResourceImpact.MAY_REPLACE: - return colors.italic(colors.yellow('may be replaced')); + return chalk.italic(chalk.yellow('may be replaced')); case ResourceImpact.WILL_REPLACE: - return colors.italic(colors.bold(colors.red('replace'))); + return chalk.italic(chalk.bold(chalk.red('replace'))); case ResourceImpact.WILL_DESTROY: - return colors.italic(colors.bold(colors.red('destroy'))); + return chalk.italic(chalk.bold(chalk.red('destroy'))); case ResourceImpact.WILL_ORPHAN: - return colors.italic(colors.yellow('orphan')); + return chalk.italic(chalk.yellow('orphan')); case ResourceImpact.WILL_UPDATE: case ResourceImpact.WILL_CREATE: case ResourceImpact.NO_CHANGE: @@ -249,13 +249,13 @@ class Formatter { this.print('%s %s %s', linePrefix, i === 0 ? '└─' : ' ', diff[i]); } } else { - this.print('%s ├─ %s %s', linePrefix, REMOVAL, this.formatValue(oldObject, colors.red)); - this.print('%s └─ %s %s', linePrefix, ADDITION, this.formatValue(newObject, colors.green)); + this.print('%s ├─ %s %s', linePrefix, REMOVAL, this.formatValue(oldObject, chalk.red)); + this.print('%s └─ %s %s', linePrefix, ADDITION, this.formatValue(newObject, chalk.green)); } } else if (oldObject !== undefined /* && newObject === undefined */) { - this.print('%s └─ %s', linePrefix, this.formatValue(oldObject, colors.red)); + this.print('%s └─ %s', linePrefix, this.formatValue(oldObject, chalk.red)); } else /* if (oldObject === undefined && newObject !== undefined) */ { - this.print('%s └─ %s', linePrefix, this.formatValue(newObject, colors.green)); + this.print('%s └─ %s', linePrefix, this.formatValue(newObject, chalk.green)); } return; } @@ -268,12 +268,12 @@ class Formatter { const newValue = newObject[key]; const treePrefix = key === lastKey ? '└' : '├'; if (oldValue !== undefined && newValue !== undefined) { - this.print('%s %s─ %s %s:', linePrefix, treePrefix, this.changeTag(oldValue, newValue), colors.blue(`.${key}`)); + this.print('%s %s─ %s %s:', linePrefix, treePrefix, this.changeTag(oldValue, newValue), chalk.blue(`.${key}`)); this.formatObjectDiff(oldValue, newValue, `${linePrefix} ${key === lastKey ? ' ' : '│'}`); } else if (oldValue !== undefined /* && newValue === undefined */) { - this.print('%s %s─ %s Removed: %s', linePrefix, treePrefix, REMOVAL, colors.blue(`.${key}`)); + this.print('%s %s─ %s Removed: %s', linePrefix, treePrefix, REMOVAL, chalk.blue(`.${key}`)); } else /* if (oldValue === undefined && newValue !== undefined */ { - this.print('%s %s─ %s Added: %s', linePrefix, treePrefix, ADDITION, colors.blue(`.${key}`)); + this.print('%s %s─ %s Added: %s', linePrefix, treePrefix, ADDITION, chalk.blue(`.${key}`)); } } } @@ -322,7 +322,7 @@ class Formatter { const normalized = this.normalizedLogicalIdPath(logicalId); if (normalized) { - return `${normalized} ${colors.gray(logicalId)}`; + return `${normalized} ${chalk.gray(logicalId)}`; } return logicalId; @@ -430,7 +430,7 @@ function _diffStrings(oldStr: string, newStr: string, context: number): string[] const patch: Patch = structuredPatch(null, null, oldStr, newStr, null, null, { context }); const result = new Array(); for (const hunk of patch.hunks) { - result.push(colors.magenta(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`)); + result.push(chalk.magenta(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`)); const baseIndent = _findIndent(hunk.lines); for (const line of hunk.lines) { // Don't care about termination newline. @@ -442,10 +442,10 @@ function _diffStrings(oldStr: string, newStr: string, context: number): string[] result.push(`${CONTEXT} ${text}`); break; case '+': - result.push(colors.bold(`${ADDITION} ${colors.green(text)}`)); + result.push(chalk.bold(`${ADDITION} ${chalk.green(text)}`)); break; case '-': - result.push(colors.bold(`${REMOVAL} ${colors.red(text)}`)); + result.push(chalk.bold(`${REMOVAL} ${chalk.red(text)}`)); break; default: throw new Error(`Unexpected diff marker: ${marker} (full line: ${line})`); diff --git a/packages/@aws-cdk/cloudformation-diff/lib/iam/iam-changes.ts b/packages/@aws-cdk/cloudformation-diff/lib/iam/iam-changes.ts index 291ab9803f4c9..f1460ff48c027 100644 --- a/packages/@aws-cdk/cloudformation-diff/lib/iam/iam-changes.ts +++ b/packages/@aws-cdk/cloudformation-diff/lib/iam/iam-changes.ts @@ -1,5 +1,5 @@ import * as cfnspec from '@aws-cdk/cfnspec'; -import * as colors from 'colors/safe'; +import * as chalk from 'chalk'; import { PropertyChange, PropertyMap, ResourceChange } from '../diff/types'; import { DiffableCollection } from '../diffable'; import { renderIntrinsics } from '../render-intrinsics'; @@ -77,18 +77,18 @@ export class IamChanges { renderedStatement.action, renderedStatement.principal, renderedStatement.condition, - ].map(s => colors.green(s))); + ].map(s => chalk.green(s))); } for (const statement of this.statements.removals) { const renderedStatement = statement.render(); ret.push([ - colors.red('-'), + chalk.red('-'), renderedStatement.resource, renderedStatement.effect, renderedStatement.action, renderedStatement.principal, renderedStatement.condition, - ].map(s => colors.red(s))); + ].map(s => chalk.red(s))); } // Sort by 2nd column @@ -108,14 +108,14 @@ export class IamChanges { '+', att.identityArn, att.managedPolicyArn, - ].map(s => colors.green(s))); + ].map(s => chalk.green(s))); } for (const att of this.managedPolicies.removals) { ret.push([ '-', att.identityArn, att.managedPolicyArn, - ].map(s => colors.red(s))); + ].map(s => chalk.red(s))); } // Sort by 2nd column diff --git a/packages/@aws-cdk/cloudformation-diff/lib/iam/statement.ts b/packages/@aws-cdk/cloudformation-diff/lib/iam/statement.ts index ea89ad4e597ee..7f83a5561bc76 100644 --- a/packages/@aws-cdk/cloudformation-diff/lib/iam/statement.ts +++ b/packages/@aws-cdk/cloudformation-diff/lib/iam/statement.ts @@ -1,6 +1,9 @@ -import * as deepEqual from 'fast-deep-equal'; import { deepRemoveUndefined } from '../util'; +// namespace object imports won't work in the bundle for function exports +// eslint-disable-next-line @typescript-eslint/no-require-imports +const deepEqual = require('fast-deep-equal'); + export class Statement { /** * Statement ID diff --git a/packages/@aws-cdk/cloudformation-diff/lib/network/security-group-changes.ts b/packages/@aws-cdk/cloudformation-diff/lib/network/security-group-changes.ts index 54122f593ae49..02e2134f99cd4 100644 --- a/packages/@aws-cdk/cloudformation-diff/lib/network/security-group-changes.ts +++ b/packages/@aws-cdk/cloudformation-diff/lib/network/security-group-changes.ts @@ -1,4 +1,4 @@ -import * as colors from 'colors/safe'; +import * as chalk from 'chalk'; import { PropertyChange, ResourceChange } from '../diff/types'; import { DiffableCollection } from '../diffable'; import { renderIntrinsics } from '../render-intrinsics'; @@ -66,7 +66,7 @@ export class SecurityGroupChanges { inOut, rule.describeProtocol(), rule.describePeer(), - ].map(s => plusMin === '+' ? colors.green(s) : colors.red(s)); + ].map(s => plusMin === '+' ? chalk.green(s) : chalk.red(s)); // First generate all lines, sort later ret.push(...this.ingress.additions.map(renderRule('+', inWord))); diff --git a/packages/@aws-cdk/cloudformation-diff/package.json b/packages/@aws-cdk/cloudformation-diff/package.json index 53bea429a4b8c..a46cf4a5539a8 100644 --- a/packages/@aws-cdk/cloudformation-diff/package.json +++ b/packages/@aws-cdk/cloudformation-diff/package.json @@ -25,20 +25,20 @@ "dependencies": { "@aws-cdk/cfnspec": "0.0.0", "@types/node": "^10.17.60", - "colors": "^1.4.0", + "chalk": "^4", "diff": "^5.0.0", "fast-deep-equal": "^3.1.3", "string-width": "^4.2.3", - "table": "^6.7.5" + "table": "^6.8.0" }, "devDependencies": { "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "@types/string-width": "^4.0.1", - "fast-check": "^2.20.0", - "jest": "^27.4.5", - "ts-jest": "^27.1.2" + "fast-check": "^2.22.0", + "jest": "^27.5.1", + "ts-jest": "^27.1.3" }, "repository": { "url": "https://github.com/aws/aws-cdk.git", diff --git a/packages/@aws-cdk/cloudformation-include/build.js b/packages/@aws-cdk/cloudformation-include/build.js index 454a10a5014d5..ab4d071b6cf21 100644 --- a/packages/@aws-cdk/cloudformation-include/build.js +++ b/packages/@aws-cdk/cloudformation-include/build.js @@ -1,7 +1,6 @@ /** * This build file has two purposes: - * 1. It adds a dependency on each @aws-cdk/aws-xyz package with L1s to this package, - * similarly to how deps.js does for decdk. + * 1. It adds a dependency on each @aws-cdk/aws-xyz package with L1s to this package. * 2. It generates the file cfn-types-2-classes.json that contains a mapping * between the CloudFormation type and the fully-qualified name of the L1 class, * used in the logic of the CfnInclude class. diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index 9aa012be11c87..8e24b2f22e400 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -150,6 +150,7 @@ "@aws-cdk/aws-finspace": "0.0.0", "@aws-cdk/aws-fis": "0.0.0", "@aws-cdk/aws-fms": "0.0.0", + "@aws-cdk/aws-forecast": "0.0.0", "@aws-cdk/aws-frauddetector": "0.0.0", "@aws-cdk/aws-fsx": "0.0.0", "@aws-cdk/aws-gamelift": "0.0.0", @@ -163,6 +164,7 @@ "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-imagebuilder": "0.0.0", "@aws-cdk/aws-inspector": "0.0.0", + "@aws-cdk/aws-inspectorv2": "0.0.0", "@aws-cdk/aws-iot": "0.0.0", "@aws-cdk/aws-iot1click": "0.0.0", "@aws-cdk/aws-iotanalytics": "0.0.0", @@ -173,10 +175,13 @@ "@aws-cdk/aws-iotthingsgraph": "0.0.0", "@aws-cdk/aws-iotwireless": "0.0.0", "@aws-cdk/aws-ivs": "0.0.0", + "@aws-cdk/aws-kafkaconnect": "0.0.0", "@aws-cdk/aws-kendra": "0.0.0", "@aws-cdk/aws-kinesis": "0.0.0", "@aws-cdk/aws-kinesisanalytics": "0.0.0", + "@aws-cdk/aws-kinesisanalyticsv2": "0.0.0", "@aws-cdk/aws-kinesisfirehose": "0.0.0", + "@aws-cdk/aws-kinesisvideo": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-lakeformation": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", @@ -334,6 +339,7 @@ "@aws-cdk/aws-finspace": "0.0.0", "@aws-cdk/aws-fis": "0.0.0", "@aws-cdk/aws-fms": "0.0.0", + "@aws-cdk/aws-forecast": "0.0.0", "@aws-cdk/aws-frauddetector": "0.0.0", "@aws-cdk/aws-fsx": "0.0.0", "@aws-cdk/aws-gamelift": "0.0.0", @@ -347,6 +353,7 @@ "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-imagebuilder": "0.0.0", "@aws-cdk/aws-inspector": "0.0.0", + "@aws-cdk/aws-inspectorv2": "0.0.0", "@aws-cdk/aws-iot": "0.0.0", "@aws-cdk/aws-iot1click": "0.0.0", "@aws-cdk/aws-iotanalytics": "0.0.0", @@ -357,10 +364,13 @@ "@aws-cdk/aws-iotthingsgraph": "0.0.0", "@aws-cdk/aws-iotwireless": "0.0.0", "@aws-cdk/aws-ivs": "0.0.0", + "@aws-cdk/aws-kafkaconnect": "0.0.0", "@aws-cdk/aws-kendra": "0.0.0", "@aws-cdk/aws-kinesis": "0.0.0", "@aws-cdk/aws-kinesisanalytics": "0.0.0", + "@aws-cdk/aws-kinesisanalyticsv2": "0.0.0", "@aws-cdk/aws-kinesisfirehose": "0.0.0", + "@aws-cdk/aws-kinesisvideo": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-lakeformation": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", @@ -440,13 +450,13 @@ "constructs": "^3.3.69" }, "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5", - "ts-jest": "^27.1.2" + "@types/jest": "^27.4.1", + "jest": "^27.5.1", + "ts-jest": "^27.1.3" }, "bundledDependencies": [ "yaml" diff --git a/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts index 326b80a3585f8..fb2f4697f610e 100644 --- a/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts @@ -1,15 +1,15 @@ import * as path from 'path'; -import { SynthUtils } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; import * as core from '@aws-cdk/core'; import * as constructs from 'constructs'; import * as inc from '../lib'; describe('CDK Include', () => { + let app: core.App; let stack: core.Stack; beforeEach(() => { - stack = new core.Stack(); + app = new core.App(); + stack = new core.Stack(app); }); test('throws a validation exception for a template with a missing required top-level resource property', () => { @@ -22,7 +22,7 @@ describe('CDK Include', () => { includeTestTemplate(stack, 'bucket-with-cors-rules-not-an-array.json'); expect(() => { - SynthUtils.synthesize(stack); + app.synth(); }).toThrow(/corsRules: "CorsRules!" should be a list/); }); @@ -30,7 +30,7 @@ describe('CDK Include', () => { includeTestTemplate(stack, 'bucket-with-cors-rules-null-element.json'); expect(() => { - SynthUtils.synthesize(stack); + app.synth(); }).toThrow(/allowedMethods: required but missing/); }); @@ -38,7 +38,7 @@ describe('CDK Include', () => { includeTestTemplate(stack, 'bucket-with-invalid-cors-rule.json'); expect(() => { - SynthUtils.synthesize(stack); + app.synth(); }).toThrow(/allowedOrigins: required but missing/); }); @@ -130,7 +130,7 @@ describe('CDK Include', () => { includeTestTemplate(stack, 'alphabetical-string-passed-to-number.json'); expect(() => { - SynthUtils.synthesize(stack); + app.synth(); }).toThrow(/"abc" should be a number/); }); diff --git a/packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts b/packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts index 2a323657f089e..4e73005db1e54 100644 --- a/packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts @@ -1,6 +1,5 @@ import * as path from 'path'; -import { ABSENT, ResourcePart } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as s3 from '@aws-cdk/aws-s3'; import * as core from '@aws-cdk/core'; import * as inc from '../lib'; @@ -27,7 +26,7 @@ describe('CDK Include for nested stacks', () => { }); const childStack = parentTemplate.getNestedStack('ChildStack'); - expect(childStack.stack).toMatchTemplate( + Template.fromStack(childStack.stack).templateMatches( loadTestFileToJsObject('grandchild-import-stack.json'), ); }); @@ -47,11 +46,11 @@ describe('CDK Include for nested stacks', () => { const childStack = parentTemplate.getNestedStack('ChildStack'); const anotherChildStack = parentTemplate.getNestedStack('AnotherChildStack'); - expect(childStack.stack).toMatchTemplate( + Template.fromStack(childStack.stack).templateMatches( loadTestFileToJsObject('grandchild-import-stack.json'), ); - expect(anotherChildStack.stack).toMatchTemplate( + Template.fromStack(anotherChildStack.stack).templateMatches( loadTestFileToJsObject('grandchild-import-stack.json'), ); }); @@ -73,11 +72,11 @@ describe('CDK Include for nested stacks', () => { const childStack = parentTemplate.getNestedStack('ChildStack'); const grandChildStack = childStack.includedTemplate.getNestedStack('GrandChildStack'); - expect(childStack.stack).toMatchTemplate( + Template.fromStack(childStack.stack).templateMatches( loadTestFileToJsObject('child-import-stack.expected.json'), ); - expect(grandChildStack.stack).toMatchTemplate( + Template.fromStack(grandChildStack.stack).templateMatches( loadTestFileToJsObject('grandchild-import-stack.json'), ); }); @@ -188,7 +187,7 @@ describe('CDK Include for nested stacks', () => { bucket.bucketName = 'modified-bucket-name'; - expect(childTemplate.stack).toHaveResource('AWS::S3::Bucket', { BucketName: 'modified-bucket-name' }); + Template.fromStack(childTemplate.stack).hasResourceProperties('AWS::S3::Bucket', { BucketName: 'modified-bucket-name' }); }); test('can use a condition', () => { @@ -218,7 +217,7 @@ describe('CDK Include for nested stacks', () => { const assetParam = 'AssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3BucketEAA24F0C'; const assetParamKey = 'AssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3VersionKey1194CAB2'; - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Parameters": { [assetParam]: { "Type": "String", @@ -284,7 +283,7 @@ describe('CDK Include for nested stacks', () => { templateFile: testTemplateFilePath('parent-two-children.json'), }); - expect(stack).toMatchTemplate(loadTestFileToJsObject('parent-two-children.json')); + Template.fromStack(stack).templateMatches(loadTestFileToJsObject('parent-two-children.json')); }); test('getNestedStack() throws an exception when getting a resource that does not exist in the template', () => { @@ -344,7 +343,7 @@ describe('CDK Include for nested stacks', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CloudFormation::Stack', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFormation::Stack', { "Parameters": { "Param1": { "Ref": "Param", @@ -370,7 +369,7 @@ describe('CDK Include for nested stacks', () => { const parameter = parentTemplate.getParameter('Param'); parameter.overrideLogicalId('DifferentParameter'); - expect(stack).toHaveResourceLike('AWS::CloudFormation::Stack', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFormation::Stack', { "Parameters": { "Param1": { "Ref": "DifferentParameter", @@ -417,7 +416,7 @@ describe('CDK Include for nested stacks', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CloudFormation::Stack', { + Template.fromStack(stack).hasResource('AWS::CloudFormation::Stack', { "Metadata": { "Property1": "Value1", }, @@ -426,7 +425,7 @@ describe('CDK Include for nested stacks', () => { "AnotherChildStack", ], "UpdateReplacePolicy": "Retain", - }, ResourcePart.CompleteDefinition); + }); }); test('correctly parses NotificationsARNs, Timeout', () => { @@ -442,11 +441,11 @@ describe('CDK Include for nested stacks', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CloudFormation::Stack', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFormation::Stack', { "NotificationARNs": ["arn1"], "TimeoutInMinutes": 5, }); - expect(stack).toHaveResourceLike('AWS::CloudFormation::Stack', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFormation::Stack', { "NotificationARNs": { "Ref": "ArrayParam" }, "TimeoutInMinutes": { "Fn::Select": [0, { @@ -466,7 +465,7 @@ describe('CDK Include for nested stacks', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CloudFormation::Stack', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudFormation::Stack', { "Parameters": { "Number": "60", }, @@ -481,7 +480,7 @@ describe('CDK Include for nested stacks', () => { templateFile: testTemplateFilePath('child-no-bucket.json'), }); - expect(includedChild.stack).toMatchTemplate( + Template.fromStack(includedChild.stack).templateMatches( loadTestFileToJsObject('child-no-bucket.json'), ); expect(includedChild.includedTemplate.getResource('GrandChildStack')).toBeDefined(); @@ -536,7 +535,7 @@ describe('CDK Include for nested stacks', () => { }); test('correctly creates parameters in the parent stack, and passes them to the child stack', () => { - expect(assetStack).toMatchTemplate({ + Template.fromStack(assetStack).templateMatches({ "Parameters": { [parentBucketParam]: { "Type": "String", @@ -616,7 +615,7 @@ describe('CDK Include for nested stacks', () => { }); test('correctly creates parameters in the child stack, and passes them to the grandchild stack', () => { - expect(child.stack).toMatchTemplate({ + Template.fromStack(child.stack).templateMatches({ "Parameters": { "MyBucketParameter": { "Type": "String", @@ -676,7 +675,7 @@ describe('CDK Include for nested stacks', () => { }); test('leaves grandchild stack unmodified', () => { - expect(grandChild.stack).toMatchTemplate( + Template.fromStack(grandChild.stack).templateMatches( loadTestFileToJsObject('grandchild-import-stack.json'), ); }); @@ -703,7 +702,7 @@ describe('CDK Include for nested stacks', () => { }); test('correctly removes the parameter from the child stack', () => { - expect(childStack).toMatchTemplate({ + Template.fromStack(childStack).templateMatches({ "Parameters": { "SecondParameter": { "Type": "String", @@ -733,9 +732,9 @@ describe('CDK Include for nested stacks', () => { }); test('correctly removes the parameter from the parent stack', () => { - expect(parentStack).toHaveResourceLike('AWS::CloudFormation::Stack', { + Template.fromStack(parentStack).hasResourceProperties('AWS::CloudFormation::Stack', { "Parameters": { - "FirstParameter": ABSENT, + "FirstParameter": Match.absent(), "SecondParameter": "second-value", }, }); diff --git a/packages/@aws-cdk/cloudformation-include/test/serverless-transform.test.ts b/packages/@aws-cdk/cloudformation-include/test/serverless-transform.test.ts index ce0d044bb4ed3..706afb615ec72 100644 --- a/packages/@aws-cdk/cloudformation-include/test/serverless-transform.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/serverless-transform.test.ts @@ -1,5 +1,5 @@ import * as path from 'path'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as core from '@aws-cdk/core'; import * as constructs from 'constructs'; import * as inc from '../lib'; @@ -18,7 +18,7 @@ describe('CDK Include for templates with SAM transform', () => { test('can ingest a template with only a minimal SAM function using S3Location for CodeUri, and output it unchanged', () => { includeTestTemplate(stack, 'only-minimal-sam-function-codeuri-as-s3location.yaml'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('only-minimal-sam-function-codeuri-as-s3location.yaml'), ); }); @@ -26,7 +26,7 @@ describe('CDK Include for templates with SAM transform', () => { test('can ingest a template with only a SAM function using an array with DDB CRUD for Policies, and output it unchanged', () => { includeTestTemplate(stack, 'only-sam-function-policies-array-ddb-crud.yaml'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('only-sam-function-policies-array-ddb-crud.yaml'), ); }); @@ -34,7 +34,7 @@ describe('CDK Include for templates with SAM transform', () => { test('can ingest a template with only a minimal SAM function using a parameter for CodeUri, and output it unchanged', () => { includeTestTemplate(stack, 'only-minimal-sam-function-codeuri-as-param.yaml'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('only-minimal-sam-function-codeuri-as-param.yaml'), ); }); @@ -42,7 +42,7 @@ describe('CDK Include for templates with SAM transform', () => { test('can ingest a template with only a minimal SAM function using a parameter for CodeUri Bucket property, and output it unchanged', () => { includeTestTemplate(stack, 'only-minimal-sam-function-codeuri-bucket-as-param.yaml'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('only-minimal-sam-function-codeuri-bucket-as-param.yaml'), ); }); @@ -50,7 +50,7 @@ describe('CDK Include for templates with SAM transform', () => { test('can ingest a template with only a SAM function using an array with DDB CRUD for Policies with an Fn::If expression, and output it unchanged', () => { includeTestTemplate(stack, 'only-sam-function-policies-array-ddb-crud-if.yaml'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('only-sam-function-policies-array-ddb-crud-if.yaml'), ); }); @@ -58,7 +58,7 @@ describe('CDK Include for templates with SAM transform', () => { test('can ingest a template with a a union-type property provided as an object, and output it unchanged', () => { includeTestTemplate(stack, 'api-endpoint-config-object.yaml'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('api-endpoint-config-object.yaml'), ); }); @@ -66,7 +66,7 @@ describe('CDK Include for templates with SAM transform', () => { test('can ingest a template with a a union-type property provided as a string, and output it unchanged', () => { includeTestTemplate(stack, 'api-endpoint-config-string.yaml'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('api-endpoint-config-string.yaml'), ); }); @@ -74,7 +74,7 @@ describe('CDK Include for templates with SAM transform', () => { test('can ingest a template with a a union-type property provided as an empty string, and output it unchanged', () => { includeTestTemplate(stack, 'api-endpoint-config-string-empty.yaml'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('api-endpoint-config-string-empty.yaml'), ); }); diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/fn-sub-map-dotted-attributes.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/fn-sub-map-dotted-attributes.json index c53229d2844d8..7daef9f79f9a4 100644 --- a/packages/@aws-cdk/cloudformation-include/test/test-templates/fn-sub-map-dotted-attributes.json +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/fn-sub-map-dotted-attributes.json @@ -4,7 +4,14 @@ "Type": "AWS::S3::Bucket", "Properties": { "BucketName": { - "Fn::Sub": "${ELB.SourceSecurityGroup.GroupName}" + "Fn::Sub": [ + "${ELB.SourceSecurityGroup.GroupName}-${LoadBalancerName}", + { + "LoadBalancerName": { + "Ref": "ELB" + } + } + ] } } }, @@ -14,7 +21,7 @@ "AvailabilityZones": [ "us-east-1a" ], - "CrossZone": "true", + "CrossZone": true, "Listeners": [{ "LoadBalancerPort": "80", "InstancePort": "80", diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/resource-attribute-update-policy.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/resource-attribute-update-policy.json index fb1f6f2aab1b2..e1440a46193be 100644 --- a/packages/@aws-cdk/cloudformation-include/test/test-templates/resource-attribute-update-policy.json +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/resource-attribute-update-policy.json @@ -53,7 +53,7 @@ "BeforeAllowTrafficHook" : "Lambda2", "DeploymentGroupName" : { "Ref": "CodeDeployDg" } }, - "EnableVersionUpgrade": "true", + "EnableVersionUpgrade": true, "UseOnlineResharding": false } } diff --git a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts index 0741b7f9d23df..65cd7e981cc81 100644 --- a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts @@ -1,6 +1,5 @@ import * as path from 'path'; -import { ResourcePart } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; import * as ssm from '@aws-cdk/aws-ssm'; @@ -22,7 +21,7 @@ describe('CDK Include', () => { test('can ingest a template with only an empty S3 Bucket, and output it unchanged', () => { includeTestTemplate(stack, 'only-empty-bucket.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('only-empty-bucket.json'), ); }); @@ -41,7 +40,7 @@ describe('CDK Include', () => { const cfnBucket = cfnTemplate.getResource('Bucket') as s3.CfnBucket; cfnBucket.bucketName = 'my-bucket-name'; - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Resources": { "Bucket": { "Type": "AWS::S3::Bucket", @@ -58,7 +57,7 @@ describe('CDK Include', () => { const cfnBucket = cfnTemplate.getResource('Bucket') as s3.CfnBucket; expect((cfnBucket.corsConfiguration as any).corsRules).toHaveLength(1); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('only-bucket-complex-props.json'), ); }); @@ -75,7 +74,7 @@ describe('CDK Include', () => { resources: [cfnBucket.attrArn], })); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { "PolicyDocument": { "Statement": [ { @@ -95,7 +94,7 @@ describe('CDK Include', () => { test('can ingest a template with a Bucket Ref-erencing a KMS Key, and output it unchanged', () => { includeTestTemplate(stack, 'bucket-with-encryption-key.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('bucket-with-encryption-key.json'), ); }); @@ -103,25 +102,39 @@ describe('CDK Include', () => { test('accepts strings for properties with type number', () => { includeTestTemplate(stack, 'string-for-number.json'); - expect(stack).toMatchTemplate( - loadTestFileToJsObject('string-for-number.json'), - ); + Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { + "CorsConfiguration": { + "CorsRules": [ + { + "MaxAge": 10, + }, + ], + }, + }); }); test('accepts numbers for properties with type string', () => { includeTestTemplate(stack, 'number-for-string.json'); - expect(stack).toMatchTemplate( - loadTestFileToJsObject('number-for-string.json'), - ); + Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { + "WebsiteConfiguration": { + "RoutingRules": [ + { + "RedirectRule": { + "HttpRedirectCode": "403", + }, + }, + ], + }, + }); }); test('accepts booleans for properties with type string', () => { includeTestTemplate(stack, 'boolean-for-string.json'); - expect(stack).toMatchTemplate( - loadTestFileToJsObject('boolean-for-string.json'), - ); + Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { + "AccessControl": "true", + }); }); test('correctly changes the logical IDs, including references, if imported with preserveLogicalIds=false', () => { @@ -135,7 +148,7 @@ describe('CDK Include', () => { const cfnBucket = cfnTemplate.getResource('Bucket') as s3.CfnBucket; cfnBucket.bucketName = 'my-bucket-name'; - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Resources": { "MyScopeKey7673692F": { "Type": "AWS::KMS::Key", @@ -202,7 +215,7 @@ describe('CDK Include', () => { test('can ingest a template with an Fn::If expression for simple values, and output it unchanged', () => { includeTestTemplate(stack, 'if-simple-property.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('if-simple-property.json'), ); }); @@ -210,7 +223,7 @@ describe('CDK Include', () => { test('can ingest a template with an Fn::If expression for complex values, and output it unchanged', () => { includeTestTemplate(stack, 'if-complex-property.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('if-complex-property.json'), ); }); @@ -218,7 +231,7 @@ describe('CDK Include', () => { test('can ingest a UserData script, and output it unchanged', () => { includeTestTemplate(stack, 'user-data.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('user-data.json'), ); }); @@ -226,7 +239,7 @@ describe('CDK Include', () => { test('can correctly ingest a resource with a property of type: Map of Lists of primitive types', () => { const cfnTemplate = includeTestTemplate(stack, 'ssm-association.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('ssm-association.json'), ); const association = cfnTemplate.getResource('Association') as ssm.CfnAssociation; @@ -236,7 +249,7 @@ describe('CDK Include', () => { test('can ingest a template with intrinsic functions and conditions, and output it unchanged', () => { includeTestTemplate(stack, 'functions-and-conditions.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('functions-and-conditions.json'), ); }); @@ -244,7 +257,7 @@ describe('CDK Include', () => { test('can ingest a JSON template with string-form Fn::GetAtt, and output it unchanged', () => { includeTestTemplate(stack, 'get-att-string-form.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('get-att-string-form.json'), ); }); @@ -252,7 +265,7 @@ describe('CDK Include', () => { test('can ingest a template with Fn::Sub in string form with escaped and unescaped references and output it unchanged', () => { includeTestTemplate(stack, 'fn-sub-string.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('fn-sub-string.json'), ); }); @@ -260,15 +273,15 @@ describe('CDK Include', () => { test('can parse the string argument Fn::Sub with escaped references that contain whitespace', () => { includeTestTemplate(stack, 'fn-sub-escaping.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('fn-sub-escaping.json'), ); }); - test('can ingest a template with Fn::Sub in map form and output it unchanged', () => { + test('can ingest a template with Fn::Sub using dotted attributes in map form and output it unchanged', () => { includeTestTemplate(stack, 'fn-sub-map-dotted-attributes.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('fn-sub-map-dotted-attributes.json'), ); }); @@ -276,7 +289,7 @@ describe('CDK Include', () => { test('preserves an empty map passed to Fn::Sub', () => { includeTestTemplate(stack, 'fn-sub-map-empty.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('fn-sub-map-empty.json'), ); }); @@ -284,7 +297,7 @@ describe('CDK Include', () => { test('can ingest a template with Fn::Sub shadowing a logical ID from the template and output it unchanged', () => { includeTestTemplate(stack, 'fn-sub-shadow.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('fn-sub-shadow.json'), ); }); @@ -292,7 +305,7 @@ describe('CDK Include', () => { test('can ingest a template with Fn::Sub attribute expression shadowing a logical ID from the template, and output it unchanged', () => { includeTestTemplate(stack, 'fn-sub-shadow-attribute.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('fn-sub-shadow-attribute.json'), ); }); @@ -302,7 +315,7 @@ describe('CDK Include', () => { cfnTemplate.getResource('AnotherBucket').overrideLogicalId('NewBucket'); - expect(stack).toHaveResourceLike('AWS::S3::Bucket', { + Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { "BucketName": { "Fn::Sub": [ "${AnotherBucket}", @@ -319,7 +332,7 @@ describe('CDK Include', () => { cfnTemplate.getResource('Bucket').overrideLogicalId('NewBucket'); - expect(stack).toHaveResourceLike('AWS::S3::Bucket', { + Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { "BucketName": { "Fn::Sub": "${NewBucket}-${!Bucket}-${NewBucket.DomainName}", }, @@ -329,7 +342,7 @@ describe('CDK Include', () => { test('can ingest a template with Fn::Sub with brace edge cases and output it unchanged', () => { includeTestTemplate(stack, 'fn-sub-brace-edges.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('fn-sub-brace-edges.json'), ); }); @@ -342,7 +355,7 @@ describe('CDK Include', () => { }, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Parameters": { "AnotherParam": { "Type": "String", @@ -375,7 +388,7 @@ describe('CDK Include', () => { test('can ingest a template with a Ref expression for an array value, and output it unchanged', () => { includeTestTemplate(stack, 'ref-array-property.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('ref-array-property.json'), ); }); @@ -383,7 +396,7 @@ describe('CDK Include', () => { test('renders non-Resources sections unchanged', () => { includeTestTemplate(stack, 'only-empty-bucket-with-parameters.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('only-empty-bucket-with-parameters.json'), ); }); @@ -394,14 +407,14 @@ describe('CDK Include', () => { expect(cfnBucket2.node.dependencies).toHaveLength(1); // we always render dependsOn as an array, even if it's a single string - expect(stack).toHaveResourceLike('AWS::S3::Bucket', { + Template.fromStack(stack).hasResource('AWS::S3::Bucket', { "Properties": { "BucketName": "bucket2", }, "DependsOn": [ "Bucket1", ], - }, ResourcePart.CompleteDefinition); + }); }); test('resolves DependsOn with an array of String values to the actual L1 class instances', () => { @@ -409,7 +422,7 @@ describe('CDK Include', () => { const cfnBucket2 = cfnTemplate.getResource('Bucket2'); expect(cfnBucket2.node.dependencies).toHaveLength(2); - expect(stack).toHaveResourceLike('AWS::S3::Bucket', { + Template.fromStack(stack).hasResource('AWS::S3::Bucket', { "Properties": { "BucketName": "bucket2", }, @@ -417,7 +430,7 @@ describe('CDK Include', () => { "Bucket0", "Bucket1", ], - }, ResourcePart.CompleteDefinition); + }); }); test('correctly parses Conditions and the Condition resource attribute', () => { @@ -426,7 +439,7 @@ describe('CDK Include', () => { const cfnBucket = cfnTemplate.getResource('Bucket'); expect(cfnBucket.cfnOptions.condition).toBe(alwaysFalseCondition); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('resource-attribute-condition.json'), ); }); @@ -434,7 +447,7 @@ describe('CDK Include', () => { test('allows Conditions to reference Mappings', () => { includeTestTemplate(stack, 'condition-using-mapping.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('condition-using-mapping.json'), ); }); @@ -444,7 +457,7 @@ describe('CDK Include', () => { const alwaysFalse = cfnTemplate.getCondition('AlwaysFalse'); alwaysFalse.overrideLogicalId('TotallyFalse'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Parameters": { "Param": { "Type": "String", @@ -481,7 +494,7 @@ describe('CDK Include', () => { }); const originalTemplate = loadTestFileToJsObject('bucket-with-parameters.json'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Resources": { ...originalTemplate.Resources, "NewBucket": { @@ -526,7 +539,7 @@ describe('CDK Include', () => { numberParam.type = "NewType"; const originalTemplate = loadTestFileToJsObject('bucket-with-parameters.json'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Resources": { ...originalTemplate.Resources, }, @@ -559,7 +572,7 @@ describe('CDK Include', () => { alwaysFalseCondition.expression = core.Fn.conditionEquals(1, 2); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Conditions": { "AlwaysFalseCond": { "Fn::Equals": [1, 2], @@ -580,7 +593,7 @@ describe('CDK Include', () => { expect(cfnBucket.cfnOptions.creationPolicy).toBeDefined(); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('resource-attribute-creation-policy.json'), ); }); @@ -590,8 +603,7 @@ describe('CDK Include', () => { const cfnBucket = cfnTemplate.getResource('Bucket'); expect(cfnBucket.cfnOptions.updatePolicy).toBeDefined(); - - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('resource-attribute-update-policy.json'), ); }); @@ -612,7 +624,7 @@ describe('CDK Include', () => { resources: [cfnBucket.attrArn], })); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ ...loadTestFileToJsObject('only-empty-bucket.json'), "Outputs": { "ExportsOutputFnGetAttBucketArn436138FE": { @@ -626,7 +638,7 @@ describe('CDK Include', () => { }, }); - expect(otherStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(otherStack).hasResourceProperties('AWS::IAM::Policy', { "PolicyDocument": { "Statement": [ { @@ -646,7 +658,7 @@ describe('CDK Include', () => { cfnKey.overrideLogicalId('TotallyDifferentKey'); const originalTemplate = loadTestFileToJsObject('bucket-with-encryption-key.json'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Resources": { "Bucket": { "Type": "AWS::S3::Bucket", @@ -679,7 +691,7 @@ describe('CDK Include', () => { test('can include a template with a custom resource that uses attributes', () => { const cfnTemplate = includeTestTemplate(stack, 'custom-resource-with-attributes.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('custom-resource-with-attributes.json'), ); @@ -706,7 +718,7 @@ describe('CDK Include', () => { const originalTemplate = loadTestFileToJsObject('outputs-with-references.json'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Conditions": { ...originalTemplate.Conditions, "MyCondition": { @@ -747,7 +759,7 @@ describe('CDK Include', () => { expect(output.value).toBeDefined(); expect(output.exportName).toBeDefined(); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('outputs-with-references.json'), ); }); @@ -766,7 +778,7 @@ describe('CDK Include', () => { someMapping.setValue('region', 'key2', 'value2'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Mappings": { "SomeMapping": { "region": { @@ -803,7 +815,7 @@ describe('CDK Include', () => { test('can ingest a template that uses Fn::FindInMap with the first argument being a dynamic reference', () => { includeTestTemplate(stack, 'find-in-map-with-dynamic-mapping.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('find-in-map-with-dynamic-mapping.json'), ); }); @@ -814,7 +826,7 @@ describe('CDK Include', () => { someMapping.overrideLogicalId('DifferentMapping'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Mappings": { "DifferentMapping": { "region": { @@ -842,7 +854,7 @@ describe('CDK Include', () => { test('can ingest a template that uses Fn::FindInMap for the value of a boolean property', () => { includeTestTemplate(stack, 'find-in-map-for-boolean-property.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('find-in-map-for-boolean-property.json'), ); }); @@ -853,7 +865,7 @@ describe('CDK Include', () => { expect(rule).toBeDefined(); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('only-parameters-and-rule.json'), ); }); @@ -881,7 +893,7 @@ describe('CDK Include', () => { const hook = cfnTemplate.getHook('EcsBlueGreenCodeDeployHook'); expect(hook).toBeDefined(); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('hook-code-deploy-blue-green-ecs.json'), ); }); @@ -901,7 +913,7 @@ describe('CDK Include', () => { }, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Transform": { "Name": "AWS::Include", "Parameters": { @@ -948,7 +960,7 @@ describe('CDK Include', () => { }, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Resources": { "Bucket": { "Type": "AWS::S3::Bucket", @@ -973,7 +985,7 @@ describe('CDK Include', () => { }, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Resources": { "Bucket": { "Type": "AWS::S3::Bucket", @@ -994,7 +1006,7 @@ describe('CDK Include', () => { }, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Resources": { "Bucket": { "Type": "AWS::S3::Bucket", @@ -1020,7 +1032,7 @@ describe('CDK Include', () => { }, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Resources": { "Bucket": { "Type": "AWS::S3::Bucket", @@ -1065,7 +1077,7 @@ describe('CDK Include', () => { test('can ingest a template that contains properties not in the current CFN spec, and output it unchanged', () => { includeTestTemplate(stack, 'properties-not-in-cfn-spec.json'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('properties-not-in-cfn-spec.json'), ); }); diff --git a/packages/@aws-cdk/cloudformation-include/test/yaml-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/yaml-templates.test.ts index 06beafcdeb62c..4c693bd1026dc 100644 --- a/packages/@aws-cdk/cloudformation-include/test/yaml-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/yaml-templates.test.ts @@ -1,5 +1,5 @@ import * as path from 'path'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as core from '@aws-cdk/core'; import * as constructs from 'constructs'; @@ -19,7 +19,7 @@ describe('CDK Include', () => { test('can ingest a template with all long-form CloudFormation functions and output it unchanged', () => { includeTestTemplate(stack, 'long-form-vpc.yaml'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('long-form-vpc.yaml'), ); }); @@ -27,7 +27,7 @@ describe('CDK Include', () => { test('can ingest a template with year-month-date parsed as string instead of Date', () => { includeTestTemplate(stack, 'year-month-date-as-strings.yaml'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "Role": { @@ -54,7 +54,7 @@ describe('CDK Include', () => { test('can ingest a template with the short form Base64 function', () => { includeTestTemplate(stack, 'short-form-base64.yaml'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Resources": { "Base64Bucket": { "Type": "AWS::S3::Bucket", @@ -71,7 +71,7 @@ describe('CDK Include', () => { test('can ingest a template with the short form !Cidr function', () => { includeTestTemplate(stack, 'short-form-cidr.yaml'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Resources": { "CidrVpc1": { "Type": "AWS::EC2::VPC", @@ -104,7 +104,7 @@ describe('CDK Include', () => { test('can ingest a template with the short form !FindInMap function, in both hyphen and bracket notation', () => { includeTestTemplate(stack, 'short-form-find-in-map.yaml'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Mappings": { "RegionMap": { "region-1": { @@ -145,7 +145,7 @@ describe('CDK Include', () => { test('can ingest a template with the short form !GetAtt function', () => { includeTestTemplate(stack, 'short-form-get-att.yaml'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Resources": { "ELB": { "Type": "AWS::ElasticLoadBalancing::LoadBalancer", @@ -187,7 +187,7 @@ describe('CDK Include', () => { test('can ingest a template with short form Select, GetAZs, and Ref functions', () => { includeTestTemplate(stack, 'short-form-select.yaml'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Resources": { "Subnet1": { "Type": "AWS::EC2::Subnet", @@ -220,7 +220,7 @@ describe('CDK Include', () => { test('can ingest a template with the short form !ImportValue function', () => { includeTestTemplate(stack, 'short-form-import-value.yaml'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Resources": { "Bucket1": { "Type": "AWS::S3::Bucket", @@ -237,7 +237,7 @@ describe('CDK Include', () => { test('can ingest a template with the short form !Join function', () => { includeTestTemplate(stack, 'short-form-join.yaml'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Resources": { "Bucket": { "Type": "AWS::S3::Bucket", @@ -257,7 +257,7 @@ describe('CDK Include', () => { test('can ingest a template with the short form !Split function that uses both brackets and hyphens', () => { includeTestTemplate(stack, 'short-form-split.yaml'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Resources": { "Bucket1": { "Type": "AWS::S3::Bucket", @@ -287,7 +287,7 @@ describe('CDK Include', () => { // Note that this yaml template fails validation. It is unclear how to invoke !Transform. includeTestTemplate(stack, 'invalid/short-form-transform.yaml'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Resources": { "Bucket": { "Type": "AWS::S3::Bucket", @@ -310,7 +310,7 @@ describe('CDK Include', () => { test('can ingest a template with the short form conditionals', () => { includeTestTemplate(stack, 'short-form-conditionals.yaml'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Conditions": { "AlwaysTrueCond": { "Fn::And": [ @@ -348,7 +348,7 @@ describe('CDK Include', () => { test('can ingest a template with the short form Conditions', () => { includeTestTemplate(stack, 'short-form-conditions.yaml'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ "Conditions": { "AlwaysTrueCond": { "Fn::Not": [ @@ -393,7 +393,7 @@ describe('CDK Include', () => { test('can ingest a yaml with long-form functions and output it unchanged', () => { includeTestTemplate(stack, 'long-form-subnet.yaml'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('long-form-subnet.yaml'), ); }); @@ -401,7 +401,7 @@ describe('CDK Include', () => { test('can ingest a YAML template with Fn::Sub in string form and output it unchanged', () => { includeTestTemplate(stack, 'short-form-fnsub-string.yaml'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('short-form-fnsub-string.yaml'), ); }); @@ -409,7 +409,7 @@ describe('CDK Include', () => { test('can ingest a YAML template with Fn::Sub in map form and output it unchanged', () => { includeTestTemplate(stack, 'short-form-sub-map.yaml'); - expect(stack).toMatchTemplate( + Template.fromStack(stack).templateMatches( loadTestFileToJsObject('short-form-sub-map.yaml'), ); }); diff --git a/packages/@aws-cdk/core/README.md b/packages/@aws-cdk/core/README.md index d2b1e2c5e5222..041eccef39873 100644 --- a/packages/@aws-cdk/core/README.md +++ b/packages/@aws-cdk/core/README.md @@ -57,6 +57,45 @@ organize their deployments with. If you want to vend a reusable construct, define it as a subclasses of `Construct`: the consumers of your construct will decide where to place it in their own stacks. +## Stack Synthesizers + +Each Stack has a *synthesizer*, an object that determines how and where +the Stack should be synthesized and deployed. The synthesizer controls +aspects like: + +- How does the stack reference assets? (Either through CloudFormation + parameters the CLI supplies, or because the Stack knows a predefined + location where assets will be uploaded). +- What roles are used to deploy the stack? These can be bootstrapped + roles, roles created in some other way, or just the CLI's current + credentials. + +The following synthesizers are available: + +- `DefaultStackSynthesizer`: recommended. Uses predefined asset locations and + roles created by the modern bootstrap template. Access control is done by + controlling who can assume the deploy role. This is the default stack + synthesizer in CDKv2. +- `LegacyStackSynthesizer`: Uses CloudFormation parameters to communicate + asset locations, and the CLI's current permissions to deploy stacks. The + is the default stack synthesizer in CDKv1. +- `CliCredentialsStackSynthesizer`: Uses predefined asset locations, and the + CLI's current permissions. + +Each of these synthesizers takes configuration arguments. To configure +a stack with a synthesizer, pass it as one of its properties: + +```ts +new MyStack(app, 'MyStack', { + synthesizer: new DefaultStackSynthesizer({ + fileAssetsBucketName: 'my-orgs-asset-bucket', + }), +}); +``` + +For more information on bootstrapping accounts and customizing synthesis, +see [Bootstrapping in the CDK Developer Guide](https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html). + ## Nested Stacks [Nested stacks](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-nested-stacks.html) are stacks created as part of other stacks. You create a nested stack within another stack by using the `NestedStack` construct. diff --git a/packages/@aws-cdk/core/lib/annotations.ts b/packages/@aws-cdk/core/lib/annotations.ts index f46c830c25757..03fb7a99bd80a 100644 --- a/packages/@aws-cdk/core/lib/annotations.ts +++ b/packages/@aws-cdk/core/lib/annotations.ts @@ -44,7 +44,7 @@ export class Annotations { /** * Adds an { "error": } metadata entry to this construct. - * The toolkit will fail synthesis when errors are reported. + * The toolkit will fail deployment of any stack that has errors reported against it. * @param message The error message. */ public addError(message: string) { diff --git a/packages/@aws-cdk/core/lib/arn.ts b/packages/@aws-cdk/core/lib/arn.ts index a04f03baf466f..2f6cb68e2dc95 100644 --- a/packages/@aws-cdk/core/lib/arn.ts +++ b/packages/@aws-cdk/core/lib/arn.ts @@ -417,8 +417,6 @@ function parseArnShape(arn: string): 'token' | string[] { // Tokens won't contain ":", so this won't break them. const components = arn.split(':'); - // const [/* arn */, partition, service, /* region */ , /* account */ , resource] = components; - const partition = components.length > 1 ? components[1] : undefined; if (!partition) { throw new Error('The `partition` component (2nd component) of an ARN is required: ' + arn); diff --git a/packages/@aws-cdk/core/lib/assets.ts b/packages/@aws-cdk/core/lib/assets.ts index 3732cc4fc19b8..7466ded8e6d60 100644 --- a/packages/@aws-cdk/core/lib/assets.ts +++ b/packages/@aws-cdk/core/lib/assets.ts @@ -203,6 +203,15 @@ export interface DockerImageAssetSource { * @deprecated repository name should be specified at the environment-level and not at the image level */ readonly repositoryName?: string; + + /** + * Networking mode for the RUN commands during build. _Requires Docker Engine API v1.25+_. + * + * Specify this property to build images on a specific networking mode. + * + * @default - no networking mode specified + */ + readonly networkMode?: string; } /** @@ -247,6 +256,9 @@ export interface FileAssetLocation { /** * The HTTP URL of this asset on Amazon S3. * + * This value suitable for inclusion in a CloudFormation template, and + * may be an encoded token. + * * Example value: `https://s3-us-east-1.amazonaws.com/mybucket/myobject` */ readonly httpUrl: string; @@ -254,6 +266,9 @@ export interface FileAssetLocation { /** * The S3 URL of this asset on Amazon S3. * + * This value suitable for inclusion in a CloudFormation template, and + * may be an encoded token. + * * Example value: `s3://mybucket/myobject` */ readonly s3ObjectUrl: string; @@ -276,6 +291,16 @@ export interface FileAssetLocation { * key via the bucket and no additional parameters have to be granted anymore. */ readonly kmsKeyArn?: string; + + /** + * Like `s3ObjectUrl`, but not suitable for CloudFormation consumption + * + * If there are placeholders in the S3 URL, they will be returned unreplaced + * and un-evaluated. + * + * @default - This feature cannot be used + */ + readonly s3ObjectUrlWithPlaceholders?: string; } /** diff --git a/packages/@aws-cdk/core/lib/cfn-mapping.ts b/packages/@aws-cdk/core/lib/cfn-mapping.ts index 18b670a4ff927..d8a5f6a972f5d 100644 --- a/packages/@aws-cdk/core/lib/cfn-mapping.ts +++ b/packages/@aws-cdk/core/lib/cfn-mapping.ts @@ -43,7 +43,7 @@ export class CfnMapping extends CfnRefElement { constructor(scope: Construct, id: string, props: CfnMappingProps = {}) { super(scope, id); - this.mapping = props.mapping ?? { }; + this.mapping = props.mapping ? this.validateMapping(props.mapping) : {}; this.lazy = props.lazy; } @@ -51,6 +51,8 @@ export class CfnMapping extends CfnRefElement { * Sets a value in the map based on the two keys. */ public setValue(key1: string, key2: string, value: any) { + this.validateAlphanumeric(key2); + if (!(key1 in this.mapping)) { this.mapping[key1] = { }; } @@ -108,4 +110,16 @@ export class CfnMapping extends CfnRefElement { } this.lazyInformed = true; } + + private validateMapping(mapping: Mapping): Mapping { + Object.keys(mapping).forEach((m) => Object.keys(mapping[m]).forEach(this.validateAlphanumeric)); + return mapping; + } + + private validateAlphanumeric(value: any) { + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html + if (value.match(/[^a-zA-Z0-9]/g)) { + throw new Error(`Attribute name '${value}' must contain only alphanumeric characters.`); + } + } } diff --git a/packages/@aws-cdk/core/lib/cfn-resource.ts b/packages/@aws-cdk/core/lib/cfn-resource.ts index f1b4d76a10563..d1f6c3d65ffbb 100644 --- a/packages/@aws-cdk/core/lib/cfn-resource.ts +++ b/packages/@aws-cdk/core/lib/cfn-resource.ts @@ -185,6 +185,12 @@ export class CfnResource extends CfnRefElement { * } * ``` * + * The `value` argument to `addOverride` will not be processed or translated + * in any way. Pass raw JSON values in here with the correct capitalization + * for CloudFormation. If you pass CDK classes or structs, they will be + * rendered with lowercased key names, and CloudFormation will reject the + * template. + * * @param path - The path of the property, you can use dot notation to * override values in complex types. Any intermdediate keys * will be created as needed. diff --git a/packages/@aws-cdk/core/lib/context-provider.ts b/packages/@aws-cdk/core/lib/context-provider.ts index b8d6c16abac75..a74623f087802 100644 --- a/packages/@aws-cdk/core/lib/context-provider.ts +++ b/packages/@aws-cdk/core/lib/context-provider.ts @@ -17,6 +17,13 @@ export interface GetContextKeyOptions { * Provider-specific properties. */ readonly props?: { [key: string]: any }; + + /** + * Whether to include the stack's account and region automatically. + * + * @default true + */ + readonly includeEnvironment?: boolean; } /** @@ -60,11 +67,9 @@ export class ContextProvider { public static getKey(scope: Construct, options: GetContextKeyOptions): GetContextKeyResult { const stack = Stack.of(scope); - const props = { - account: stack.account, - region: stack.region, - ...options.props || {}, - }; + const props = options.includeEnvironment ?? true + ? { account: stack.account, region: stack.region, ...options.props } + : (options.props ?? {}); if (Object.values(props).find(x => Token.isUnresolved(x))) { throw new Error( diff --git a/packages/@aws-cdk/core/lib/private/region-lookup.ts b/packages/@aws-cdk/core/lib/private/region-lookup.ts index 208ff7eee30be..226df28d7f444 100644 --- a/packages/@aws-cdk/core/lib/private/region-lookup.ts +++ b/packages/@aws-cdk/core/lib/private/region-lookup.ts @@ -31,7 +31,7 @@ export function deployTimeLookup(stack: Stack, factName: string, lookupMap: Reco : [factName, 'value'] as const; const mapId = `${ucfirst(factClass)}Map`; - const factKey = factParam.replace(/[^a-zA-Z0-9]/g, '_'); + const factKey = factParam.replace(/[^a-zA-Z0-9]/g, 'x'); let mapping = stack.node.tryFindChild(mapId) as CfnMapping | undefined; if (!mapping) { diff --git a/packages/@aws-cdk/core/lib/secret-value.ts b/packages/@aws-cdk/core/lib/secret-value.ts index 84e668a1b4306..270a70a519065 100644 --- a/packages/@aws-cdk/core/lib/secret-value.ts +++ b/packages/@aws-cdk/core/lib/secret-value.ts @@ -71,8 +71,9 @@ export class SecretValue extends Intrinsic { * latest version of the parameter. */ public static ssmSecure(parameterName: string, version?: string): SecretValue { - const parts = [parameterName, version ?? '']; - return this.cfnDynamicReference(new CfnDynamicReference(CfnDynamicReferenceService.SSM_SECURE, parts.join(':'))); + return this.cfnDynamicReference( + new CfnDynamicReference(CfnDynamicReferenceService.SSM_SECURE, + version ? `${parameterName}:${version}` : parameterName)); } /** diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/_asset-manifest-builder.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/_asset-manifest-builder.ts new file mode 100644 index 0000000000000..b1e121399cf4a --- /dev/null +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/_asset-manifest-builder.ts @@ -0,0 +1,219 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import { FileAssetSource, FileAssetLocation, FileAssetPackaging, DockerImageAssetSource, DockerImageAssetLocation } from '../assets'; +import { Fn } from '../cfn-fn'; +import { ISynthesisSession } from '../construct-compat'; +import { Stack } from '../stack'; +import { resolvedOr } from './_shared'; + +/** + * Build an manifest from assets added to a stack synthesizer + */ +export class AssetManifestBuilder { + private readonly files: NonNullable = {}; + private readonly dockerImages: NonNullable = {}; + + public addFileAssetDefault( + asset: FileAssetSource, + stack: Stack, + bucketName: string, + bucketPrefix: string, + role?: RoleOptions, + ): FileAssetLocation { + validateFileAssetSource(asset); + + const extension = + asset.fileName != undefined ? path.extname(asset.fileName) : ''; + const objectKey = + bucketPrefix + + asset.sourceHash + + (asset.packaging === FileAssetPackaging.ZIP_DIRECTORY + ? '.zip' + : extension); + + // Add to manifest + this.files[asset.sourceHash] = { + source: { + path: asset.fileName, + executable: asset.executable, + packaging: asset.packaging, + }, + destinations: { + [this.manifestEnvName(stack)]: { + bucketName: bucketName, + objectKey, + region: resolvedOr(stack.region, undefined), + assumeRoleArn: role?.assumeRoleArn, + assumeRoleExternalId: role?.assumeRoleExternalId, + }, + }, + }; + + const { region, urlSuffix } = stackLocationOrInstrinsics(stack); + const httpUrl = cfnify( + `https://s3.${region}.${urlSuffix}/${bucketName}/${objectKey}`, + ); + const s3ObjectUrlWithPlaceholders = `s3://${bucketName}/${objectKey}`; + + // Return CFN expression + // + // 's3ObjectUrlWithPlaceholders' is intended for the CLI. The CLI ultimately needs a + // 'https://s3.REGION.amazonaws.com[.cn]/name/hash' URL to give to CloudFormation. + // However, there's no way for us to actually know the URL_SUFFIX in the framework, so + // we can't construct that URL. Instead, we record the 's3://.../...' form, and the CLI + // transforms it to the correct 'https://.../' URL before calling CloudFormation. + return { + bucketName: cfnify(bucketName), + objectKey, + httpUrl, + s3ObjectUrl: cfnify(s3ObjectUrlWithPlaceholders), + s3ObjectUrlWithPlaceholders, + s3Url: httpUrl, + }; + } + + public addDockerImageAssetDefault( + asset: DockerImageAssetSource, + stack: Stack, + repositoryName: string, + dockerTagPrefix: string, + role?: RoleOptions, + ): DockerImageAssetLocation { + validateDockerImageAssetSource(asset); + const imageTag = dockerTagPrefix + asset.sourceHash; + + // Add to manifest + this.dockerImages[asset.sourceHash] = { + source: { + executable: asset.executable, + directory: asset.directoryName, + dockerBuildArgs: asset.dockerBuildArgs, + dockerBuildTarget: asset.dockerBuildTarget, + dockerFile: asset.dockerFile, + networkMode: asset.networkMode, + }, + destinations: { + [this.manifestEnvName(stack)]: { + repositoryName: repositoryName, + imageTag, + region: resolvedOr(stack.region, undefined), + assumeRoleArn: role?.assumeRoleArn, + assumeRoleExternalId: role?.assumeRoleExternalId, + }, + }, + }; + + const { account, region, urlSuffix } = stackLocationOrInstrinsics(stack); + + // Return CFN expression + return { + repositoryName: cfnify(repositoryName), + imageUri: cfnify( + `${account}.dkr.ecr.${region}.${urlSuffix}/${repositoryName}:${imageTag}`, + ), + }; + } + + /** + * Write the manifest to disk, and add it to the synthesis session + * + * Reutrn the artifact Id + */ + public writeManifest( + stack: Stack, + session: ISynthesisSession, + additionalProps: Partial = {}, + ): string { + const artifactId = `${stack.artifactId}.assets`; + const manifestFile = `${artifactId}.json`; + const outPath = path.join(session.assembly.outdir, manifestFile); + + const manifest: cxschema.AssetManifest = { + version: cxschema.Manifest.version(), + files: this.files, + dockerImages: this.dockerImages, + }; + + fs.writeFileSync(outPath, JSON.stringify(manifest, undefined, 2)); + + session.assembly.addArtifact(artifactId, { + type: cxschema.ArtifactType.ASSET_MANIFEST, + properties: { + file: manifestFile, + ...additionalProps, + }, + }); + + return artifactId; + } + + private manifestEnvName(stack: Stack): string { + return [ + resolvedOr(stack.account, 'current_account'), + resolvedOr(stack.region, 'current_region'), + ].join('-'); + } +} + +export interface RoleOptions { + readonly assumeRoleArn?: string; + readonly assumeRoleExternalId?: string; +} + +function validateFileAssetSource(asset: FileAssetSource) { + if (!!asset.executable === !!asset.fileName) { + throw new Error(`Exactly one of 'fileName' or 'executable' is required, got: ${JSON.stringify(asset)}`); + } + + if (!!asset.packaging !== !!asset.fileName) { + throw new Error(`'packaging' is expected in combination with 'fileName', got: ${JSON.stringify(asset)}`); + } +} + +function validateDockerImageAssetSource(asset: DockerImageAssetSource) { + if (!!asset.executable === !!asset.directoryName) { + throw new Error(`Exactly one of 'directoryName' or 'executable' is required, got: ${JSON.stringify(asset)}`); + } + + check('dockerBuildArgs'); + check('dockerBuildTarget'); + check('dockerFile'); + + function check(key: K) { + if (asset[key] && !asset.directoryName) { + throw new Error(`'${key}' is only allowed in combination with 'directoryName', got: ${JSON.stringify(asset)}`); + } + } +} + +/** + * Return the stack locations if they're concrete, or the original CFN intrisics otherwise + * + * We need to return these instead of the tokenized versions of the strings, + * since we must accept those same ${AWS::AccountId}/${AWS::Region} placeholders + * in bucket names and role names (in order to allow environment-agnostic stacks). + * + * We'll wrap a single {Fn::Sub} around the final string in order to replace everything, + * but we can't have the token system render part of the string to {Fn::Join} because + * the CFN specification doesn't allow the {Fn::Sub} template string to be an arbitrary + * expression--it must be a string literal. + */ +function stackLocationOrInstrinsics(stack: Stack) { + return { + account: resolvedOr(stack.account, '${AWS::AccountId}'), + region: resolvedOr(stack.region, '${AWS::Region}'), + urlSuffix: resolvedOr(stack.urlSuffix, '${AWS::URLSuffix}'), + }; +} + +/** + * If the string still contains placeholders, wrap it in a Fn::Sub so they will be substituted at CFN deployment time + * + * (This happens to work because the placeholders we picked map directly onto CFN + * placeholders. If they didn't we'd have to do a transformation here). + */ +function cfnify(s: string): string { + return s.indexOf('${') > -1 ? Fn.sub(s) : s; +} \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/_shared.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/_shared.ts index a9b9e0d7acb5d..91251259c111d 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/_shared.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/_shared.ts @@ -1,7 +1,12 @@ import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as cxapi from '@aws-cdk/cx-api'; +import { FileAssetSource, FileAssetPackaging } from '../assets'; import { ConstructNode, IConstruct, ISynthesisSession } from '../construct-compat'; import { Stack } from '../stack'; +import { Token } from '../token'; /** * Shared logic of writing stack artifact to the Cloud Assembly @@ -122,4 +127,60 @@ export function assertBound(x: A | undefined): asserts x is NonNullable { function nonEmptyDict(xs: Record) { return Object.keys(xs).length > 0 ? xs : undefined; +} + +/** + * A "replace-all" function that doesn't require us escaping a literal string to a regex + */ +function replaceAll(s: string, search: string, replace: string) { + return s.split(search).join(replace); +} + +export class StringSpecializer { + constructor(private readonly stack: Stack, private readonly qualifier: string) { + } + + /** + * Function to replace placeholders in the input string as much as possible + * + * We replace: + * - ${Qualifier}: always + * - ${AWS::AccountId}, ${AWS::Region}: only if we have the actual values available + * - ${AWS::Partition}: never, since we never have the actual partition value. + */ + public specialize(s: string): string { + s = replaceAll(s, '${Qualifier}', this.qualifier); + return cxapi.EnvironmentPlaceholders.replace(s, { + region: resolvedOr(this.stack.region, cxapi.EnvironmentPlaceholders.CURRENT_REGION), + accountId: resolvedOr(this.stack.account, cxapi.EnvironmentPlaceholders.CURRENT_ACCOUNT), + partition: cxapi.EnvironmentPlaceholders.CURRENT_PARTITION, + }); + } + + /** + * Specialize only the qualifier + */ + public qualifierOnly(s: string): string { + return replaceAll(s, '${Qualifier}', this.qualifier); + } +} + +/** + * Return the given value if resolved or fall back to a default + */ +export function resolvedOr(x: string, def: A): string | A { + return Token.isUnresolved(x) ? def : x; +} + +export function stackTemplateFileAsset(stack: Stack, session: ISynthesisSession): FileAssetSource { + const templatePath = path.join(session.assembly.outdir, stack.templateFile); + const template = fs.readFileSync(templatePath, { encoding: 'utf-8' }); + + const sourceHash = contentHash(template); + + return { + fileName: stack.templateFile, + packaging: FileAssetPackaging.FILE, + sourceHash, + }; } \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts index 1a9a2ab8ee0cc..d1526edb79891 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts @@ -24,11 +24,19 @@ export interface BootstraplessSynthesizerProps { } /** - * A special synthesizer that behaves similarly to DefaultStackSynthesizer, - * but doesn't require bootstrapping the environment it operates in. - * Because of that, stacks using it cannot have assets inside of them. + * Synthesizer that reuses bootstrap roles from a different region + * + * A special synthesizer that behaves similarly to `DefaultStackSynthesizer`, + * but doesn't require bootstrapping the environment it operates in. Instead, + * it will re-use the Roles that were created for a different region (which + * is possible because IAM is a global service). + * + * However, it will not assume asset buckets or repositories have been created, + * and therefore does not support assets. + * * Used by the CodePipeline construct for the support stacks needed for - * cross-region replication S3 buckets. + * cross-region replication S3 buckets. App builders do not need to use this + * synthesizer directly. */ export class BootstraplessSynthesizer extends DefaultStackSynthesizer { constructor(props: BootstraplessSynthesizerProps) { diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/cli-credentials-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/cli-credentials-synthesizer.ts new file mode 100644 index 0000000000000..0f268297999b3 --- /dev/null +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/cli-credentials-synthesizer.ts @@ -0,0 +1,174 @@ +import * as cxapi from '@aws-cdk/cx-api'; +import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource } from '../assets'; +import { ISynthesisSession } from '../construct-compat'; +import { Stack } from '../stack'; +import { Token } from '../token'; +import { AssetManifestBuilder } from './_asset-manifest-builder'; +import { assertBound, StringSpecializer, stackTemplateFileAsset } from './_shared'; +import { BOOTSTRAP_QUALIFIER_CONTEXT, DefaultStackSynthesizer } from './default-synthesizer'; +import { StackSynthesizer } from './stack-synthesizer'; + +/** + * Properties for the CliCredentialsStackSynthesizer + */ +export interface CliCredentialsStackSynthesizerProps { + /** + * Name of the S3 bucket to hold file assets + * + * You must supply this if you have given a non-standard name to the staging bucket. + * + * The placeholders `${Qualifier}`, `${AWS::AccountId}` and `${AWS::Region}` will + * be replaced with the values of qualifier and the stack's account and region, + * respectively. + * + * @default DefaultStackSynthesizer.DEFAULT_FILE_ASSETS_BUCKET_NAME + */ + readonly fileAssetsBucketName?: string; + + /** + * Name of the ECR repository to hold Docker Image assets + * + * You must supply this if you have given a non-standard name to the ECR repository. + * + * The placeholders `${Qualifier}`, `${AWS::AccountId}` and `${AWS::Region}` will + * be replaced with the values of qualifier and the stack's account and region, + * respectively. + * + * @default DefaultStackSynthesizer.DEFAULT_IMAGE_ASSETS_REPOSITORY_NAME + */ + readonly imageAssetsRepositoryName?: string; + + /** + * Qualifier to disambiguate multiple environments in the same account + * + * You can use this and leave the other naming properties empty if you have deployed + * the bootstrap environment with standard names but only differnet qualifiers. + * + * @default - Value of context key '@aws-cdk/core:bootstrapQualifier' if set, otherwise `DefaultStackSynthesizer.DEFAULT_QUALIFIER` + */ + readonly qualifier?: string; + + /** + * bucketPrefix to use while storing S3 Assets + * + * @default - DefaultStackSynthesizer.DEFAULT_FILE_ASSET_PREFIX + */ + readonly bucketPrefix?: string; + + /** + * A prefix to use while tagging and uploading Docker images to ECR. + * + * This does not add any separators - the source hash will be appended to + * this string directly. + * + * @default - DefaultStackSynthesizer.DEFAULT_DOCKER_ASSET_PREFIX + */ + readonly dockerTagPrefix?: string; +} + +/** + * A synthesizer that uses conventional asset locations, but not conventional deployment roles + * + * Instead of assuming the bootstrapped deployment roles, all stack operations will be performed + * using the CLI's current credentials. + * + * - This synthesizer does not support deploying to accounts to which the CLI does not have + * credentials. It also does not support deploying using **CDK Pipelines**. For either of those + * features, use `DefaultStackSynthesizer`. + * - This synthesizer requires an S3 bucket and ECR repository with well-known names. To + * not depend on those, use `LegacyStackSynthesizer`. + * + * Be aware that your CLI credentials must be valid for the duration of the + * entire deployment. If you are using session credentials, make sure the + * session lifetime is long enough. + * + * By default, expects the environment to have been bootstrapped with just the staging resources + * of the Bootstrap Stack V2 (also known as "modern bootstrap stack"). You can override + * the default names using the synthesizer's construction properties. + */ +export class CliCredentialsStackSynthesizer extends StackSynthesizer { + private stack?: Stack; + private qualifier?: string; + private bucketName?: string; + private repositoryName?: string; + private bucketPrefix?: string; + private dockerTagPrefix?: string; + + private readonly assetManifest = new AssetManifestBuilder(); + + constructor(private readonly props: CliCredentialsStackSynthesizerProps = {}) { + super(); + + for (const key in props) { + if (props.hasOwnProperty(key)) { + validateNoToken(key as keyof CliCredentialsStackSynthesizerProps); + } + } + + function validateNoToken(key: A) { + const prop = props[key]; + if (typeof prop === 'string' && Token.isUnresolved(prop)) { + throw new Error(`DefaultSynthesizer property '${key}' cannot contain tokens; only the following placeholder strings are allowed: ` + [ + '${Qualifier}', + cxapi.EnvironmentPlaceholders.CURRENT_REGION, + cxapi.EnvironmentPlaceholders.CURRENT_ACCOUNT, + cxapi.EnvironmentPlaceholders.CURRENT_PARTITION, + ].join(', ')); + } + } + } + + public bind(stack: Stack): void { + if (this.stack !== undefined) { + throw new Error('A StackSynthesizer can only be used for one Stack: create a new instance to use with a different Stack'); + } + + this.stack = stack; + + const qualifier = this.props.qualifier ?? stack.node.tryGetContext(BOOTSTRAP_QUALIFIER_CONTEXT) ?? DefaultStackSynthesizer.DEFAULT_QUALIFIER; + this.qualifier = qualifier; + + const spec = new StringSpecializer(stack, qualifier); + + /* eslint-disable max-len */ + this.bucketName = spec.specialize(this.props.fileAssetsBucketName ?? DefaultStackSynthesizer.DEFAULT_FILE_ASSETS_BUCKET_NAME); + this.repositoryName = spec.specialize(this.props.imageAssetsRepositoryName ?? DefaultStackSynthesizer.DEFAULT_IMAGE_ASSETS_REPOSITORY_NAME); + this.bucketPrefix = spec.specialize(this.props.bucketPrefix ?? DefaultStackSynthesizer.DEFAULT_FILE_ASSET_PREFIX); + this.dockerTagPrefix = spec.specialize(this.props.dockerTagPrefix ?? DefaultStackSynthesizer.DEFAULT_DOCKER_ASSET_PREFIX); + /* eslint-enable max-len */ + } + + public addFileAsset(asset: FileAssetSource): FileAssetLocation { + assertBound(this.stack); + assertBound(this.bucketName); + assertBound(this.bucketPrefix); + + return this.assetManifest.addFileAssetDefault(asset, this.stack, this.bucketName, this.bucketPrefix); + } + + public addDockerImageAsset(asset: DockerImageAssetSource): DockerImageAssetLocation { + assertBound(this.stack); + assertBound(this.repositoryName); + assertBound(this.dockerTagPrefix); + + return this.assetManifest.addDockerImageAssetDefault(asset, this.stack, this.repositoryName, this.dockerTagPrefix); + } + + /** + * Synthesize the associated stack to the session + */ + public synthesize(session: ISynthesisSession): void { + assertBound(this.stack); + assertBound(this.qualifier); + + this.synthesizeStackTemplate(this.stack, session); + + const templateAsset = this.addFileAsset(stackTemplateFileAsset(this.stack, session)); + const assetManifestId = this.assetManifest.writeManifest(this.stack, session); + + this.emitStackArtifact(this.stack, session, { + stackTemplateAssetObjectUrl: templateAsset.s3ObjectUrlWithPlaceholders, + additionalDependencies: [assetManifestId], + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts index d8e1f8818abc4..8f8e79ac467b9 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts @@ -1,15 +1,13 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; -import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetPackaging, FileAssetSource } from '../assets'; +import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource } from '../assets'; import { Fn } from '../cfn-fn'; import { CfnParameter } from '../cfn-parameter'; import { CfnRule } from '../cfn-rule'; import { ISynthesisSession } from '../construct-compat'; import { Stack } from '../stack'; import { Token } from '../token'; -import { assertBound, contentHash } from './_shared'; +import { AssetManifestBuilder } from './_asset-manifest-builder'; +import { assertBound, StringSpecializer, stackTemplateFileAsset } from './_shared'; import { StackSynthesizer } from './stack-synthesizer'; export const BOOTSTRAP_QUALIFIER_CONTEXT = '@aws-cdk/core:bootstrapQualifier'; @@ -21,6 +19,12 @@ export const BOOTSTRAP_QUALIFIER_CONTEXT = '@aws-cdk/core:bootstrapQualifier'; */ const MIN_BOOTSTRAP_STACK_VERSION = 6; +/** + * The minimum bootstrap stack version required + * to use the lookup role. + */ +const MIN_LOOKUP_ROLE_BOOTSTRAP_STACK_VERSION = 8; + /** * Configuration properties for DefaultStackSynthesizer */ @@ -91,6 +95,25 @@ export interface DefaultStackSynthesizerProps { */ readonly lookupRoleArn?: string; + /** + * External ID to use when assuming lookup role + * + * @default - No external ID + */ + readonly lookupRoleExternalId?: string; + + /** + * Use the bootstrapped lookup role for (read-only) stack operations + * + * Use the lookup role when performing a `cdk diff`. If set to `false`, the + * `deploy role` credentials will be used to perform a `cdk diff`. + * + * Requires bootstrap stack version 8. + * + * @default true + */ + readonly useLookupRoleForStackOperations?: boolean; + /** * External ID to use when assuming role for image asset publishing * @@ -193,13 +216,19 @@ export interface DefaultStackSynthesizerProps { } /** - * Uses conventionally named roles and reify asset storage locations + * Uses conventionally named roles and asset storage locations * - * This synthesizer is the only StackSynthesizer that generates - * an asset manifest, and is required to deploy CDK applications using the - * `@aws-cdk/app-delivery` CI/CD library. + * This synthesizer: * - * Requires the environment to have been bootstrapped with Bootstrap Stack V2. + * - Supports cross-account deployments (the CLI can have credentials to one + * account, and you can still deploy to another account by assuming roles with + * well-known names in the other account). + * - Supports the **CDK Pipelines** library. + * + * Requires the environment to have been bootstrapped with Bootstrap Stack V2 + * (also known as "modern bootstrap stack"). The synthesizer adds a version + * check to the template, to make sure the bootstrap stack is recent enough + * to support all features expected by this synthesizer. */ export class DefaultStackSynthesizer extends StackSynthesizer { /** @@ -269,16 +298,17 @@ export class DefaultStackSynthesizer extends StackSynthesizer { private fileAssetPublishingRoleArn?: string; private imageAssetPublishingRoleArn?: string; private lookupRoleArn?: string; + private useLookupRoleForStackOperations: boolean; private qualifier?: string; private bucketPrefix?: string; private dockerTagPrefix?: string; private bootstrapStackVersionSsmParameter?: string; - private readonly files: NonNullable = {}; - private readonly dockerImages: NonNullable = {}; + private assetManifest = new AssetManifestBuilder(); constructor(private readonly props: DefaultStackSynthesizerProps = {}) { super(); + this.useLookupRoleForStackOperations = props.useLookupRoleForStackOperations ?? true; for (const key in props) { if (props.hasOwnProperty(key)) { @@ -309,116 +339,45 @@ export class DefaultStackSynthesizer extends StackSynthesizer { const qualifier = this.props.qualifier ?? stack.node.tryGetContext(BOOTSTRAP_QUALIFIER_CONTEXT) ?? DefaultStackSynthesizer.DEFAULT_QUALIFIER; this.qualifier = qualifier; - // Function to replace placeholders in the input string as much as possible - // - // We replace: - // - ${Qualifier}: always - // - ${AWS::AccountId}, ${AWS::Region}: only if we have the actual values available - // - ${AWS::Partition}: never, since we never have the actual partition value. - const specialize = (s: string) => { - s = replaceAll(s, '${Qualifier}', qualifier); - return cxapi.EnvironmentPlaceholders.replace(s, { - region: resolvedOr(stack.region, cxapi.EnvironmentPlaceholders.CURRENT_REGION), - accountId: resolvedOr(stack.account, cxapi.EnvironmentPlaceholders.CURRENT_ACCOUNT), - partition: cxapi.EnvironmentPlaceholders.CURRENT_PARTITION, - }); - }; + const spec = new StringSpecializer(stack, qualifier); /* eslint-disable max-len */ - this.bucketName = specialize(this.props.fileAssetsBucketName ?? DefaultStackSynthesizer.DEFAULT_FILE_ASSETS_BUCKET_NAME); - this.repositoryName = specialize(this.props.imageAssetsRepositoryName ?? DefaultStackSynthesizer.DEFAULT_IMAGE_ASSETS_REPOSITORY_NAME); - this._deployRoleArn = specialize(this.props.deployRoleArn ?? DefaultStackSynthesizer.DEFAULT_DEPLOY_ROLE_ARN); - this._cloudFormationExecutionRoleArn = specialize(this.props.cloudFormationExecutionRole ?? DefaultStackSynthesizer.DEFAULT_CLOUDFORMATION_ROLE_ARN); - this.fileAssetPublishingRoleArn = specialize(this.props.fileAssetPublishingRoleArn ?? DefaultStackSynthesizer.DEFAULT_FILE_ASSET_PUBLISHING_ROLE_ARN); - this.imageAssetPublishingRoleArn = specialize(this.props.imageAssetPublishingRoleArn ?? DefaultStackSynthesizer.DEFAULT_IMAGE_ASSET_PUBLISHING_ROLE_ARN); - this.lookupRoleArn = specialize(this.props.lookupRoleArn ?? DefaultStackSynthesizer.DEFAULT_LOOKUP_ROLE_ARN); - this.bucketPrefix = specialize(this.props.bucketPrefix ?? DefaultStackSynthesizer.DEFAULT_FILE_ASSET_PREFIX); - this.dockerTagPrefix = specialize(this.props.dockerTagPrefix ?? DefaultStackSynthesizer.DEFAULT_DOCKER_ASSET_PREFIX); - this.bootstrapStackVersionSsmParameter = replaceAll( - this.props.bootstrapStackVersionSsmParameter ?? DefaultStackSynthesizer.DEFAULT_BOOTSTRAP_STACK_VERSION_SSM_PARAMETER, - '${Qualifier}', - qualifier, - ); + this.bucketName = spec.specialize(this.props.fileAssetsBucketName ?? DefaultStackSynthesizer.DEFAULT_FILE_ASSETS_BUCKET_NAME); + this.repositoryName = spec.specialize(this.props.imageAssetsRepositoryName ?? DefaultStackSynthesizer.DEFAULT_IMAGE_ASSETS_REPOSITORY_NAME); + this._deployRoleArn = spec.specialize(this.props.deployRoleArn ?? DefaultStackSynthesizer.DEFAULT_DEPLOY_ROLE_ARN); + this._cloudFormationExecutionRoleArn = spec.specialize(this.props.cloudFormationExecutionRole ?? DefaultStackSynthesizer.DEFAULT_CLOUDFORMATION_ROLE_ARN); + this.fileAssetPublishingRoleArn = spec.specialize(this.props.fileAssetPublishingRoleArn ?? DefaultStackSynthesizer.DEFAULT_FILE_ASSET_PUBLISHING_ROLE_ARN); + this.imageAssetPublishingRoleArn = spec.specialize(this.props.imageAssetPublishingRoleArn ?? DefaultStackSynthesizer.DEFAULT_IMAGE_ASSET_PUBLISHING_ROLE_ARN); + this.lookupRoleArn = spec.specialize(this.props.lookupRoleArn ?? DefaultStackSynthesizer.DEFAULT_LOOKUP_ROLE_ARN); + this.bucketPrefix = spec.specialize(this.props.bucketPrefix ?? DefaultStackSynthesizer.DEFAULT_FILE_ASSET_PREFIX); + this.dockerTagPrefix = spec.specialize(this.props.dockerTagPrefix ?? DefaultStackSynthesizer.DEFAULT_DOCKER_ASSET_PREFIX); + this.bootstrapStackVersionSsmParameter = spec.qualifierOnly(this.props.bootstrapStackVersionSsmParameter ?? DefaultStackSynthesizer.DEFAULT_BOOTSTRAP_STACK_VERSION_SSM_PARAMETER); /* eslint-enable max-len */ } public addFileAsset(asset: FileAssetSource): FileAssetLocation { assertBound(this.stack); assertBound(this.bucketName); - validateFileAssetSource(asset); - - const extension = asset.fileName != undefined ? path.extname(asset.fileName) : ''; - const objectKey = this.bucketPrefix + asset.sourceHash + (asset.packaging === FileAssetPackaging.ZIP_DIRECTORY ? '.zip' : extension); + assertBound(this.bucketPrefix); - // Add to manifest - this.files[asset.sourceHash] = { - source: { - path: asset.fileName, - executable: asset.executable, - packaging: asset.packaging, - }, - destinations: { - [this.manifestEnvName]: { - bucketName: this.bucketName, - objectKey, - region: resolvedOr(this.stack.region, undefined), - assumeRoleArn: this.fileAssetPublishingRoleArn, - assumeRoleExternalId: this.props.fileAssetPublishingExternalId, - }, - }, - }; - - const { region, urlSuffix } = stackLocationOrInstrinsics(this.stack); - const httpUrl = cfnify(`https://s3.${region}.${urlSuffix}/${this.bucketName}/${objectKey}`); - const s3ObjectUrl = cfnify(`s3://${this.bucketName}/${objectKey}`); - - // Return CFN expression - return { - bucketName: cfnify(this.bucketName), - objectKey, - httpUrl, - s3ObjectUrl, - s3Url: httpUrl, - }; + return this.assetManifest.addFileAssetDefault(asset, this.stack, this.bucketName, this.bucketPrefix, { + assumeRoleArn: this.fileAssetPublishingRoleArn, + assumeRoleExternalId: this.props.fileAssetPublishingExternalId, + }); } public addDockerImageAsset(asset: DockerImageAssetSource): DockerImageAssetLocation { assertBound(this.stack); assertBound(this.repositoryName); - validateDockerImageAssetSource(asset); - - const imageTag = this.dockerTagPrefix + asset.sourceHash; - - // Add to manifest - this.dockerImages[asset.sourceHash] = { - source: { - executable: asset.executable, - directory: asset.directoryName, - dockerBuildArgs: asset.dockerBuildArgs, - dockerBuildTarget: asset.dockerBuildTarget, - dockerFile: asset.dockerFile, - }, - destinations: { - [this.manifestEnvName]: { - repositoryName: this.repositoryName, - imageTag, - region: resolvedOr(this.stack.region, undefined), - assumeRoleArn: this.imageAssetPublishingRoleArn, - assumeRoleExternalId: this.props.imageAssetPublishingExternalId, - }, - }, - }; + assertBound(this.dockerTagPrefix); - const { account, region, urlSuffix } = stackLocationOrInstrinsics(this.stack); - - // Return CFN expression - return { - repositoryName: cfnify(this.repositoryName), - imageUri: cfnify(`${account}.dkr.ecr.${region}.${urlSuffix}/${this.repositoryName}:${imageTag}`), - }; + return this.assetManifest.addDockerImageAssetDefault(asset, this.stack, this.repositoryName, this.dockerTagPrefix, { + assumeRoleArn: this.imageAssetPublishingRoleArn, + assumeRoleExternalId: this.props.imageAssetPublishingExternalId, + }); } - protected synthesizeStackTemplate(stack: Stack, session: ISynthesisSession): void { + protected synthesizeStackTemplate(stack: Stack, session: ISynthesisSession) { stack._synthesizeTemplate(session, this.lookupRoleArn); } @@ -440,19 +399,27 @@ export class DefaultStackSynthesizer extends StackSynthesizer { this.synthesizeStackTemplate(this.stack, session); - // Add the stack's template to the artifact manifest - const templateManifestUrl = this.addStackTemplateToAssetManifest(session); + const templateAsset = this.addFileAsset(stackTemplateFileAsset(this.stack, session)); - const artifactId = this.writeAssetManifest(session); + const assetManifestId = this.assetManifest.writeManifest(this.stack, session, { + requiresBootstrapStackVersion: MIN_BOOTSTRAP_STACK_VERSION, + bootstrapStackVersionSsmParameter: this.bootstrapStackVersionSsmParameter, + }); this.emitStackArtifact(this.stack, session, { assumeRoleExternalId: this.props.deployRoleExternalId, assumeRoleArn: this._deployRoleArn, cloudFormationExecutionRoleArn: this._cloudFormationExecutionRoleArn, - stackTemplateAssetObjectUrl: templateManifestUrl, + stackTemplateAssetObjectUrl: templateAsset.s3ObjectUrlWithPlaceholders, requiresBootstrapStackVersion: MIN_BOOTSTRAP_STACK_VERSION, bootstrapStackVersionSsmParameter: this.bootstrapStackVersionSsmParameter, - additionalDependencies: [artifactId], + additionalDependencies: [assetManifestId], + lookupRole: this.useLookupRoleForStackOperations && this.lookupRoleArn ? { + arn: this.lookupRoleArn, + assumeRoleExternalId: this.props.lookupRoleExternalId, + requiresBootstrapStackVersion: MIN_LOOKUP_ROLE_BOOTSTRAP_STACK_VERSION, + bootstrapStackVersionSsmParameter: this.bootstrapStackVersionSsmParameter, + } : undefined, }); } @@ -479,126 +446,6 @@ export class DefaultStackSynthesizer extends StackSynthesizer { protected get stack(): Stack | undefined { return this._stack; } - - /** - * Add the stack's template as one of the manifest assets - * - * This will make it get uploaded to S3 automatically by S3-assets. Return - * the manifest URL. - * - * (We can't return the location returned from `addFileAsset`, as that - * contains CloudFormation intrinsics which can't go into the manifest). - */ - private addStackTemplateToAssetManifest(session: ISynthesisSession) { - assertBound(this.stack); - - const templatePath = path.join(session.assembly.outdir, this.stack.templateFile); - const template = fs.readFileSync(templatePath, { encoding: 'utf-8' }); - - const sourceHash = contentHash(template); - - this.addFileAsset({ - fileName: this.stack.templateFile, - packaging: FileAssetPackaging.FILE, - sourceHash, - }); - - // We should technically return an 'https://s3.REGION.amazonaws.com[.cn]/name/hash' URL here, - // because that is what CloudFormation expects to see. - // - // However, there's no way for us to actually know the UrlSuffix a priori, so we can't construct it here. - // - // Instead, we'll have a protocol with the CLI that we put an 's3://.../...' URL here, and the CLI - // is going to resolve it to the correct 'https://.../' URL before it gives it to CloudFormation. - // - // ALSO: it would be great to reuse the return value of `addFileAsset()` here, except those contain - // CloudFormation REFERENCES to locations, not actual locations (can contain `{ Ref: AWS::Region }` and - // `{ Ref: SomeParameter }` etc). We therefore have to duplicate some logic here :(. - const extension = path.extname(this.stack.templateFile); - return `s3://${this.bucketName}/${this.bucketPrefix}${sourceHash}${extension}`; - } - - /** - * Write an asset manifest to the Cloud Assembly, return the artifact IDs written - */ - private writeAssetManifest(session: ISynthesisSession): string { - assertBound(this.stack); - - const artifactId = `${this.stack.artifactId}.assets`; - const manifestFile = `${artifactId}.json`; - const outPath = path.join(session.assembly.outdir, manifestFile); - - const manifest: cxschema.AssetManifest = { - version: cxschema.Manifest.version(), - files: this.files, - dockerImages: this.dockerImages, - }; - - fs.writeFileSync(outPath, JSON.stringify(manifest, undefined, 2)); - session.assembly.addArtifact(artifactId, { - type: cxschema.ArtifactType.ASSET_MANIFEST, - properties: { - file: manifestFile, - requiresBootstrapStackVersion: MIN_BOOTSTRAP_STACK_VERSION, - bootstrapStackVersionSsmParameter: this.bootstrapStackVersionSsmParameter, - }, - }); - - return artifactId; - } - - private get manifestEnvName(): string { - assertBound(this.stack); - - return [ - resolvedOr(this.stack.account, 'current_account'), - resolvedOr(this.stack.region, 'current_region'), - ].join('-'); - } -} - -/** - * Return the given value if resolved or fall back to a default - */ -function resolvedOr(x: string, def: A): string | A { - return Token.isUnresolved(x) ? def : x; -} - -/** - * A "replace-all" function that doesn't require us escaping a literal string to a regex - */ -function replaceAll(s: string, search: string, replace: string) { - return s.split(search).join(replace); -} - -/** - * If the string still contains placeholders, wrap it in a Fn::Sub so they will be substituted at CFN deployment time - * - * (This happens to work because the placeholders we picked map directly onto CFN - * placeholders. If they didn't we'd have to do a transformation here). - */ -function cfnify(s: string): string { - return s.indexOf('${') > -1 ? Fn.sub(s) : s; -} - -/** - * Return the stack locations if they're concrete, or the original CFN intrisics otherwise - * - * We need to return these instead of the tokenized versions of the strings, - * since we must accept those same ${AWS::AccountId}/${AWS::Region} placeholders - * in bucket names and role names (in order to allow environment-agnostic stacks). - * - * We'll wrap a single {Fn::Sub} around the final string in order to replace everything, - * but we can't have the token system render part of the string to {Fn::Join} because - * the CFN specification doesn't allow the {Fn::Sub} template string to be an arbitrary - * expression--it must be a string literal. - */ -function stackLocationOrInstrinsics(stack: Stack) { - return { - account: resolvedOr(stack.account, '${AWS::AccountId}'), - region: resolvedOr(stack.region, '${AWS::Region}'), - urlSuffix: resolvedOr(stack.urlSuffix, '${AWS::URLSuffix}'), - }; } /** @@ -641,29 +488,3 @@ function range(startIncl: number, endExcl: number) { return ret; } - -function validateFileAssetSource(asset: FileAssetSource) { - if (!!asset.executable === !!asset.fileName) { - throw new Error(`Exactly one of 'fileName' or 'executable' is required, got: ${JSON.stringify(asset)}`); - } - - if (!!asset.packaging !== !!asset.fileName) { - throw new Error(`'packaging' is expected in combination with 'fileName', got: ${JSON.stringify(asset)}`); - } -} - -function validateDockerImageAssetSource(asset: DockerImageAssetSource) { - if (!!asset.executable === !!asset.directoryName) { - throw new Error(`Exactly one of 'directoryName' or 'executable' is required, got: ${JSON.stringify(asset)}`); - } - - check('dockerBuildArgs'); - check('dockerBuildTarget'); - check('dockerFile'); - - function check(key: K) { - if (asset[key] && !asset.directoryName) { - throw new Error(`'${key}' is only allowed in combination with 'directoryName', got: ${JSON.stringify(asset)}`); - } - } -} diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/index.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/index.ts index db5f8e4d3f656..2a7a4060dcf53 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/index.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/index.ts @@ -4,3 +4,4 @@ export * from './legacy'; export * from './bootstrapless-synthesizer'; export * from './nested'; export * from './stack-synthesizer'; +export * from './cli-credentials-synthesizer'; diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/legacy.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/legacy.ts index 53eafad29a317..204f3b8ba6827 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/legacy.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/legacy.ts @@ -24,10 +24,22 @@ const ASSETS_ECR_REPOSITORY_NAME = 'aws-cdk/assets'; const ASSETS_ECR_REPOSITORY_NAME_OVERRIDE_CONTEXT_KEY = 'assets-ecr-repository-name'; /** - * Use the original deployment environment + * Use the CDK classic way of referencing assets * - * This deployment environment is restricted in cross-environment deployments, - * CI/CD deployments, and will use up CloudFormation parameters in your template. + * This synthesizer will generate CloudFormation parameters for every referenced + * asset, and use the CLI's current credentials to deploy the stack. + * + * - It does not support cross-account deployment (the CLI must have credentials + * to the account you are trying to deploy to). + * - It cannot be used with **CDK Pipelines**. To deploy using CDK Pipelines, + * you must use the `DefaultStackSynthesizer`. + * - Each asset will take up a CloudFormation Parameter in your template. Keep in + * mind that there is a maximum of 200 parameters in a CloudFormation template. + * To use determinstic asset locations instead, use `CliCredentialsStackSynthesizer`. + * + * Be aware that your CLI credentials must be valid for the duration of the + * entire deployment. If you are using session credentials, make sure the + * session lifetime is long enough. * * This is the only StackSynthesizer that supports customizing asset behavior * by overriding `Stack.addFileAsset()` and `Stack.addDockerImageAsset()`. @@ -136,6 +148,7 @@ export class LegacyStackSynthesizer extends StackSynthesizer { buildArgs: asset.dockerBuildArgs, target: asset.dockerBuildTarget, file: asset.dockerFile, + networkMode: asset.networkMode, }; this.stack.node.addMetadata(cxschema.ArtifactMetadataEntryType.ASSET, metadata); diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/nested.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/nested.ts index 8eb05d34cba9e..ff5f1def0652f 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/nested.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/nested.ts @@ -6,9 +6,12 @@ import { StackSynthesizer } from './stack-synthesizer'; import { IStackSynthesizer } from './types'; /** - * Deployment environment for a nested stack + * Synthesizer for a nested stack * - * Interoperates with the StackSynthesizer of the parent stack. + * Forwards all calls to the parent stack's synthesizer. + * + * This synthesizer is automatically used for `NestedStack` constructs. + * App builder do not need to use this class directly. */ export class NestedStackSynthesizer extends StackSynthesizer { private stack?: Stack; diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/stack-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/stack-synthesizer.ts index 3b283eaae24ce..75e4b46bf015f 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/stack-synthesizer.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/stack-synthesizer.ts @@ -1,3 +1,4 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource } from '../assets'; import { ISynthesisSession } from '../construct-compat'; import { Stack } from '../stack'; @@ -46,7 +47,6 @@ export abstract class StackSynthesizer implements IStackSynthesizer { stack._synthesizeTemplate(session); } - /** * Write the stack artifact to the session * @@ -100,6 +100,13 @@ export interface SynthesizeStackArtifactOptions { */ readonly cloudFormationExecutionRoleArn?: string; + /** + * The role to use to look up values from the target AWS account + * + * @default - None + */ + readonly lookupRole?: cxschema.BootstrapRole; + /** * If the stack template has already been included in the asset manifest, its asset URL * diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index ffcd1f5c9e3fe..852c03fa918df 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -178,15 +178,15 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/aws-lambda": "^8.10.89", + "@types/aws-lambda": "^8.10.92", "@types/fs-extra": "^8.1.2", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "@types/lodash": "^4.14.178", "@types/minimatch": "^3.0.5", "@types/node": "^10.17.60", "@types/sinon": "^9.0.11", - "fast-check": "^2.20.0", - "jest": "^27.4.5", + "fast-check": "^2.22.0", + "jest": "^27.5.1", "lodash": "^4.17.21", "sinon": "^9.2.4", "ts-mock-imports": "^1.3.8" @@ -199,7 +199,7 @@ "constructs": "^3.3.69", "fs-extra": "^9.1.0", "ignore": "^5.2.0", - "minimatch": "^3.0.4" + "minimatch": "^3.1.2" }, "bundledDependencies": [ "fs-extra", diff --git a/packages/@aws-cdk/core/rosetta/default.ts-fixture b/packages/@aws-cdk/core/rosetta/default.ts-fixture index 26a25736acb17..e0da717734235 100644 --- a/packages/@aws-cdk/core/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/core/rosetta/default.ts-fixture @@ -23,6 +23,7 @@ import { CustomResource, CustomResourceProvider, CustomResourceProviderRuntime, + DefaultStackSynthesizer, DependableTrait, Duration, Fn, diff --git a/packages/@aws-cdk/core/test/context.test.ts b/packages/@aws-cdk/core/test/context.test.ts index b052e74e8f1b7..7df1ce97cebac 100644 --- a/packages/@aws-cdk/core/test/context.test.ts +++ b/packages/@aws-cdk/core/test/context.test.ts @@ -166,6 +166,11 @@ describe('context', () => { }); + + test('can skip account/region from attach to context', () => { + const stack = new Stack(undefined, 'TestStack', { env: { account: '12345', region: 'us-east-1' } }); + expect(ContextProvider.getKey(stack, { provider: 'asdf', includeEnvironment: false }).key).toEqual('asdf:'); + }); }); /** diff --git a/packages/@aws-cdk/core/test/feature-flags.test.ts b/packages/@aws-cdk/core/test/feature-flags.test.ts index 09b79b7c26123..cdffe64e04d2f 100644 --- a/packages/@aws-cdk/core/test/feature-flags.test.ts +++ b/packages/@aws-cdk/core/test/feature-flags.test.ts @@ -23,7 +23,7 @@ describe('feature flags', () => { test('invalid flag', () => { const stack = new Stack(); - expect(FeatureFlags.of(stack).isEnabled('non-existent-flag')).toEqual(undefined); + expect(FeatureFlags.of(stack).isEnabled('non-existent-flag')).toEqual(false); }); }); diff --git a/packages/@aws-cdk/core/test/mappings.test.ts b/packages/@aws-cdk/core/test/mappings.test.ts index 7d1964f60bfe8..5d67aa0163e68 100644 --- a/packages/@aws-cdk/core/test/mappings.test.ts +++ b/packages/@aws-cdk/core/test/mappings.test.ts @@ -58,8 +58,6 @@ describe('mappings', () => { }, }, }); - - }); test('allow using unresolved tokens in find-in-map', () => { @@ -67,26 +65,25 @@ describe('mappings', () => { const mapping = new CfnMapping(stack, 'mapping', { mapping: { - instanceCount: { - 'us-east-1': 12, + 'us-east-1': { + instanceCount: 12, }, }, }); - const v1 = mapping.findInMap('instanceCount', Aws.REGION); - const v2 = Fn.findInMap(mapping.logicalId, 'instanceCount', Aws.REGION); + const v1 = mapping.findInMap(Aws.REGION, 'instanceCount'); + const v2 = Fn.findInMap(mapping.logicalId, Aws.REGION, 'instanceCount'); - const expected = { 'Fn::FindInMap': ['mapping', 'instanceCount', { Ref: 'AWS::Region' }] }; + const expected = { 'Fn::FindInMap': ['mapping', { Ref: 'AWS::Region' }, 'instanceCount'] }; expect(stack.resolve(v1)).toEqual(expected); expect(stack.resolve(v2)).toEqual(expected); expect(toCloudFormation(stack).Mappings).toEqual({ mapping: { - instanceCount: { - 'us-east-1': 12, + 'us-east-1': { + instanceCount: 12, }, }, }); - }); test('no validation if first key is token and second is a static string', () => { @@ -114,7 +111,6 @@ describe('mappings', () => { }, }, }); - }); test('validate first key if it is a string and second is a token', () => { @@ -122,26 +118,36 @@ describe('mappings', () => { const stack = new Stack(); const mapping = new CfnMapping(stack, 'mapping', { mapping: { - size: { - 'us-east-1': 12, + 'us-east-1': { + size: 12, }, }, }); // WHEN - const v = mapping.findInMap('size', Aws.REGION); + const v = mapping.findInMap(Aws.REGION, 'size'); // THEN - expect(() => mapping.findInMap('not-found', Aws.REGION)).toThrow(/Mapping doesn't contain top-level key 'not-found'/); - expect(stack.resolve(v)).toEqual({ 'Fn::FindInMap': ['mapping', 'size', { Ref: 'AWS::Region' }] }); + expect(() => mapping.findInMap('not-found', 'size')).toThrow(/Mapping doesn't contain top-level key 'not-found'/); + expect(stack.resolve(v)).toEqual({ 'Fn::FindInMap': ['mapping', { Ref: 'AWS::Region' }, 'size'] }); expect(toCloudFormation(stack).Mappings).toEqual({ mapping: { - size: { - 'us-east-1': 12, + 'us-east-1': { + size: 12, }, }, }); + }); + test('throws if mapping attribute name not alphanumeric', () => { + const stack = new Stack(); + expect(() => new CfnMapping(stack, 'mapping', { + mapping: { + size: { + 'us-east-1': 12, + }, + }, + })).toThrowError(/Attribute name 'us-east-1' must contain only alphanumeric characters./); }); }); diff --git a/packages/@aws-cdk/core/test/secret-value.test.ts b/packages/@aws-cdk/core/test/secret-value.test.ts index a987cfaff0c87..4a9b7bbe56093 100644 --- a/packages/@aws-cdk/core/test/secret-value.test.ts +++ b/packages/@aws-cdk/core/test/secret-value.test.ts @@ -127,7 +127,7 @@ describe('secret value', () => { const v = SecretValue.ssmSecure('param-name'); // THEN - expect(stack.resolve(v)).toEqual('{{resolve:ssm-secure:param-name:}}'); + expect(stack.resolve(v)).toEqual('{{resolve:ssm-secure:param-name}}'); }); test('cfnDynamicReference', () => { diff --git a/packages/@aws-cdk/core/test/stack-synthesis/clicreds-synthesis.test.ts b/packages/@aws-cdk/core/test/stack-synthesis/clicreds-synthesis.test.ts new file mode 100644 index 0000000000000..3fab5ef7e0b5f --- /dev/null +++ b/packages/@aws-cdk/core/test/stack-synthesis/clicreds-synthesis.test.ts @@ -0,0 +1,257 @@ +import * as fs from 'fs'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as cxapi from '@aws-cdk/cx-api'; +import { App, Aws, CfnResource, CliCredentialsStackSynthesizer, FileAssetPackaging, Stack } from '../../lib'; +import { evaluateCFN } from '../evaluate-cfn'; + +const CFN_CONTEXT = { + 'AWS::Region': 'the_region', + 'AWS::AccountId': 'the_account', + 'AWS::URLSuffix': 'domain.aws', +}; + +let app: App; +let stack: Stack; +describe('CLI creds synthesis', () => { + beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + synthesizer: new CliCredentialsStackSynthesizer(), + }); + }); + + test('stack template is in asset manifest', () => { + // GIVEN + new CfnResource(stack, 'Resource', { + type: 'Some::Resource', + }); + + // WHEN + const asm = app.synth(); + + // THEN -- the S3 url is advertised on the stack artifact + const stackArtifact = asm.getStackArtifact('Stack'); + + const templateObjectKey = last(stackArtifact.stackTemplateAssetObjectUrl?.split('/')); + + expect(stackArtifact.stackTemplateAssetObjectUrl).toEqual(`s3://cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}/${templateObjectKey}`); + + // THEN - the template is in the asset manifest + const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; + expect(manifestArtifact).toBeDefined(); + const manifest: cxschema.AssetManifest = JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' })); + + const firstFile = (manifest.files ? manifest.files[Object.keys(manifest.files)[0]] : undefined) ?? {}; + + expect(firstFile).toEqual({ + source: { path: 'Stack.template.json', packaging: 'file' }, + destinations: { + 'current_account-current_region': { + bucketName: 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + objectKey: templateObjectKey, + }, + }, + }); + }); + + test('add file asset', () => { + // WHEN + const location = stack.synthesizer.addFileAsset({ + fileName: __filename, + packaging: FileAssetPackaging.FILE, + sourceHash: 'abcdef', + }); + + // THEN - we have a fixed asset location with region placeholders + expect(evalCFN(location.bucketName)).toEqual('cdk-hnb659fds-assets-the_account-the_region'); + expect(evalCFN(location.s3Url)).toEqual('https://s3.the_region.domain.aws/cdk-hnb659fds-assets-the_account-the_region/abcdef.js'); + + // THEN - object key contains source hash somewhere + expect(location.objectKey.indexOf('abcdef')).toBeGreaterThan(-1); + }); + + test('add docker image asset', () => { + // WHEN + const location = stack.synthesizer.addDockerImageAsset({ + directoryName: '.', + sourceHash: 'abcdef', + }); + + // THEN - we have a fixed asset location with region placeholders + expect(evalCFN(location.repositoryName)).toEqual('cdk-hnb659fds-container-assets-the_account-the_region'); + expect(evalCFN(location.imageUri)).toEqual('the_account.dkr.ecr.the_region.domain.aws/cdk-hnb659fds-container-assets-the_account-the_region:abcdef'); + + + }); + + test('synthesis', () => { + // GIVEN + stack.synthesizer.addFileAsset({ + fileName: __filename, + packaging: FileAssetPackaging.FILE, + sourceHash: 'abcdef', + }); + stack.synthesizer.addDockerImageAsset({ + directoryName: '.', + sourceHash: 'abcdef', + }); + + // WHEN + const asm = app.synth(); + + // THEN - we have an asset manifest with both assets and the stack template in there + const manifestArtifact = getAssetManifest(asm); + const manifest = readAssetManifest(manifestArtifact); + + expect(Object.keys(manifest.files || {}).length).toEqual(2); + expect(Object.keys(manifest.dockerImages || {}).length).toEqual(1); + }); + + test('customize publishing resources', () => { + // GIVEN + const myapp = new App(); + + // WHEN + const mystack = new Stack(myapp, 'mystack', { + synthesizer: new CliCredentialsStackSynthesizer({ + fileAssetsBucketName: 'file-asset-bucket', + imageAssetsRepositoryName: 'image-ecr-repository', + }), + }); + + mystack.synthesizer.addFileAsset({ + fileName: __filename, + packaging: FileAssetPackaging.FILE, + sourceHash: 'file-asset-hash', + }); + + mystack.synthesizer.addDockerImageAsset({ + directoryName: '.', + sourceHash: 'docker-asset-hash', + }); + + // THEN + const asm = myapp.synth(); + const manifest = readAssetManifest(getAssetManifest(asm)); + + expect(manifest.files?.['file-asset-hash']?.destinations?.['current_account-current_region']).toEqual({ + bucketName: 'file-asset-bucket', + objectKey: 'file-asset-hash.js', + }); + + expect(manifest.dockerImages?.['docker-asset-hash']?.destinations?.['current_account-current_region']).toEqual({ + repositoryName: 'image-ecr-repository', + imageTag: 'docker-asset-hash', + }); + }); + + test('synthesis with bucketPrefix', () => { + // GIVEN + const myapp = new App(); + + // WHEN + const mystack = new Stack(myapp, 'mystack-bucketPrefix', { + synthesizer: new CliCredentialsStackSynthesizer({ + fileAssetsBucketName: 'file-asset-bucket', + bucketPrefix: '000000000000/', + }), + }); + + mystack.synthesizer.addFileAsset({ + fileName: __filename, + packaging: FileAssetPackaging.FILE, + sourceHash: 'file-asset-hash-with-prefix', + }); + + // WHEN + const asm = myapp.synth(); + + // THEN -- the S3 url is advertised on the stack artifact + const stackArtifact = asm.getStackArtifact('mystack-bucketPrefix'); + + // THEN - we have an asset manifest with both assets and the stack template in there + const manifest = readAssetManifest(getAssetManifest(asm)); + + // THEN + expect(manifest.files?.['file-asset-hash-with-prefix']?.destinations?.['current_account-current_region']).toEqual({ + bucketName: 'file-asset-bucket', + objectKey: '000000000000/file-asset-hash-with-prefix.js', + }); + + const templateHash = last(stackArtifact.stackTemplateAssetObjectUrl?.split('/')); + + expect(stackArtifact.stackTemplateAssetObjectUrl).toEqual(`s3://file-asset-bucket/000000000000/${templateHash}`); + }); + + test('synthesis with dockerPrefix', () => { + // GIVEN + const myapp = new App(); + + // WHEN + const mystack = new Stack(myapp, 'mystack-dockerPrefix', { + synthesizer: new CliCredentialsStackSynthesizer({ + dockerTagPrefix: 'test-prefix-', + }), + }); + + mystack.synthesizer.addDockerImageAsset({ + directoryName: 'some-folder', + sourceHash: 'docker-asset-hash', + }); + + const asm = myapp.synth(); + + // THEN + const manifest = readAssetManifest(getAssetManifest(asm)); + const imageTag = manifest.dockerImages?.['docker-asset-hash']?.destinations?.['current_account-current_region'].imageTag; + expect(imageTag).toEqual('test-prefix-docker-asset-hash'); + }); + + test('cannot use same synthesizer for multiple stacks', () => { + // GIVEN + const synthesizer = new CliCredentialsStackSynthesizer(); + + // WHEN + new Stack(app, 'Stack2', { synthesizer }); + expect(() => { + new Stack(app, 'Stack3', { synthesizer }); + }).toThrow(/A StackSynthesizer can only be used for one Stack/); + + }); +}); + +test('get an exception when using tokens for parameters', () => { + expect(() => { + // GIVEN + new CliCredentialsStackSynthesizer({ + fileAssetsBucketName: `my-bucket-${Aws.REGION}`, + }); + }).toThrow(/cannot contain tokens/); +}); + +/** + * Evaluate a possibly string-containing value the same way CFN would do + * + * (Be invariant to the specific Fn::Sub or Fn::Join we would output) + */ +function evalCFN(value: any) { + return evaluateCFN(stack.resolve(value), CFN_CONTEXT); +} + +function isAssetManifest(x: cxapi.CloudArtifact): x is cxapi.AssetManifestArtifact { + return x instanceof cxapi.AssetManifestArtifact; +} + +function getAssetManifest(asm: cxapi.CloudAssembly): cxapi.AssetManifestArtifact { + const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; + if (!manifestArtifact) { throw new Error('no asset manifest in assembly'); } + return manifestArtifact; +} + +function readAssetManifest(manifestArtifact: cxapi.AssetManifestArtifact): cxschema.AssetManifest { + return JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' })); +} + +function last(xs?: A[]): A | undefined { + return xs ? xs[xs.length - 1] : undefined; +} diff --git a/packages/@aws-cdk/custom-resources/README.md b/packages/@aws-cdk/custom-resources/README.md index 890255ee7bb12..cb30aada65d7f 100644 --- a/packages/@aws-cdk/custom-resources/README.md +++ b/packages/@aws-cdk/custom-resources/README.md @@ -31,14 +31,9 @@ with a `CustomResource` and a user-provided AWS Lambda function which implements the actual handler. ```ts -import { CustomResource } from '@aws-cdk/core'; -import * as logs from '@aws-cdk/aws-logs'; -import * as iam from '@aws-cdk/aws-iam'; -import * as cr from '@aws-cdk/custom-resources'; - -const onEvent = new lambda.Function(this, 'MyHandler', { /* ... */ }); - -const myRole = new iam.Role(this, 'MyRole', { /* ... */ }); +declare const onEvent: lambda.Function; +declare const isComplete: lambda.Function; +declare const myRole: iam.Role; const myProvider = new cr.Provider(this, 'MyProvider', { onEventHandler: onEvent, @@ -275,10 +270,12 @@ to all buckets: ```ts new lambda.Function(this, 'OnEventHandler', { - // ... + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromInline('my code'), initialPolicy: [ - new iam.PolicyStatement({ actions: [ 's3:GetObject*' ], resources: [ '*' ] }) - ] + new iam.PolicyStatement({ actions: [ 's3:GetObject*' ], resources: [ '*' ] }), + ], }); ``` @@ -309,12 +306,15 @@ The following example will create the file `folder/file1.txt` inside `myBucket` with the contents `hello!`. -```ts -new S3File(this, 'MyFile', { +```plaintext +// This example exists only for TypeScript + +declare const myBucket: s3.Bucket; +new cr.S3File(this, 'MyFile', { bucket: myBucket, objectKey: 'folder/file1.txt', // optional content: 'hello!', - public: true // optional + public: true, // optional }); ``` @@ -334,11 +334,14 @@ Checks that the textual contents of an S3 object matches a certain value. The ch The following example defines an `S3Assert` resource which waits until `myfile.txt` in `myBucket` exists and includes the contents `foo bar`: -```ts -new S3Assert(this, 'AssertMyFile', { +```plaintext +// This example exists only for TypeScript + +declare const myBucket: s3.Bucket; +new cr.S3Assert(this, 'AssertMyFile', { bucket: myBucket, objectKey: 'myfile.txt', - expectedContent: 'foo bar' + expectedContent: 'foo bar', }); ``` @@ -356,7 +359,9 @@ stacks it may be useful to manually set a name for the Provider Function Lambda have a predefined service token ARN. ```ts - +declare const onEvent: lambda.Function; +declare const isComplete: lambda.Function; +declare const myRole: iam.Role; const myProvider = new cr.Provider(this, 'MyProvider', { onEventHandler: onEvent, isCompleteHandler: isComplete, @@ -409,26 +414,30 @@ resources. Chained API calls can be achieved by creating dependencies: ```ts -const awsCustom1 = new AwsCustomResource(this, 'API1', { +const awsCustom1 = new cr.AwsCustomResource(this, 'API1', { onCreate: { service: '...', action: '...', - physicalResourceId: PhysicalResourceId.of('...') + physicalResourceId: cr.PhysicalResourceId.of('...'), }, - policy: AwsCustomResourcePolicy.fromSdkCalls({resources: AwsCustomResourcePolicy.ANY_RESOURCE}) + policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ + resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE, + }), }); -const awsCustom2 = new AwsCustomResource(this, 'API2', { +const awsCustom2 = new cr.AwsCustomResource(this, 'API2', { onCreate: { service: '...', - action: '...' + action: '...', parameters: { - text: awsCustom1.getResponseField('Items.0.text') + text: awsCustom1.getResponseField('Items.0.text'), }, - physicalResourceId: PhysicalResourceId.of('...') + physicalResourceId: cr.PhysicalResourceId.of('...'), }, - policy: AwsCustomResourcePolicy.fromSdkCalls({resources: AwsCustomResourcePolicy.ANY_RESOURCE}) -}) + policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ + resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE, + }), +}); ``` ### Physical Resource Id Parameter @@ -436,24 +445,26 @@ const awsCustom2 = new AwsCustomResource(this, 'API2', { Some AWS APIs may require passing the physical resource id in as a parameter for doing updates and deletes. You can pass it by using `PhysicalResourceIdReference`. ```ts -const awsCustom = new AwsCustomResource(this, '...', { +const awsCustom = new cr.AwsCustomResource(this, 'aws-custom', { onCreate: { service: '...', - action: '...' + action: '...', parameters: { - text: '...' + text: '...', }, - physicalResourceId: PhysicalResourceId.of('...') + physicalResourceId: cr.PhysicalResourceId.of('...'), }, onUpdate: { service: '...', - action: '...'. + action: '...', parameters: { text: '...', - resourceId: new PhysicalResourceIdReference() - } + resourceId: new cr.PhysicalResourceIdReference(), + }, }, - policy: AwsCustomResourcePolicy.fromSdkCalls({resources: AwsCustomResourcePolicy.ANY_RESOURCE}) + policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ + resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE, + }), }) ``` @@ -476,13 +487,16 @@ Use the `role`, `timeout`, `logRetention` and `functionName` properties to custo the Lambda function implementing the custom resource: ```ts -new AwsCustomResource(this, 'Customized', { - // other props here +declare const myRole: iam.Role; +new cr.AwsCustomResource(this, 'Customized', { role: myRole, // must be assumable by the `lambda.amazonaws.com` service principal - timeout: cdk.Duration.minutes(10) // defaults to 2 minutes - logRetention: logs.RetentionDays.ONE_WEEK // defaults to never delete logs + timeout: Duration.minutes(10), // defaults to 2 minutes + logRetention: logs.RetentionDays.ONE_WEEK, // defaults to never delete logs functionName: 'my-custom-name', // defaults to a CloudFormation generated name -}) + policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ + resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE, + }), +}); ``` ### Restricting the output of the Custom Resource @@ -492,17 +506,19 @@ objects. If your API call returns an object that exceeds this limit, you can res the data returned by the custom resource to specific paths in the API response: ```ts -new AwsCustomResource(stack, 'ListObjects', { +new cr.AwsCustomResource(this, 'ListObjects', { onCreate: { service: 's3', action: 'listObjectsV2', parameters: { Bucket: 'my-bucket', }, - physicalResourceId: PhysicalResourceId.of('id'), + physicalResourceId: cr.PhysicalResourceId.of('id'), outputPaths: ['Contents.0.Key', 'Contents.1.Key'], // Output only the two first keys }, - policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: AwsCustomResourcePolicy.ANY_RESOURCE }), + policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ + resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE, + }), }); ``` @@ -514,49 +530,56 @@ path in `PhysicalResourceId.fromResponse()`. #### Verify a domain with SES ```ts -const verifyDomainIdentity = new AwsCustomResource(this, 'VerifyDomainIdentity', { +import * as route53 from '@aws-cdk/aws-route53'; + +const verifyDomainIdentity = new cr.AwsCustomResource(this, 'VerifyDomainIdentity', { onCreate: { service: 'SES', action: 'verifyDomainIdentity', parameters: { - Domain: 'example.com' + Domain: 'example.com', }, - physicalResourceId: PhysicalResourceId.fromResponse('VerificationToken') // Use the token returned by the call as physical id + physicalResourceId: cr.PhysicalResourceId.fromResponse('VerificationToken'), // Use the token returned by the call as physical id }, - policy: AwsCustomResourcePolicy.fromSdkCalls({resources: AwsCustomResourcePolicy.ANY_RESOURCE}) + policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ + resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE, + }), }); +declare const zone: route53.HostedZone; new route53.TxtRecord(this, 'SESVerificationRecord', { zone, recordName: `_amazonses.example.com`, - values: [verifyDomainIdentity.getResponseField('VerificationToken')] + values: [verifyDomainIdentity.getResponseField('VerificationToken')], }); ``` #### Get the latest version of a secure SSM parameter ```ts -const getParameter = new AwsCustomResource(this, 'GetParameter', { +const getParameter = new cr.AwsCustomResource(this, 'GetParameter', { onUpdate: { // will also be called for a CREATE event service: 'SSM', action: 'getParameter', parameters: { Name: 'my-parameter', - WithDecryption: true + WithDecryption: true, }, - physicalResourceId: PhysicalResourceId.of(Date.now().toString()) // Update physical id to always fetch the latest version + physicalResourceId: cr.PhysicalResourceId.of(Date.now().toString()), // Update physical id to always fetch the latest version }, - policy: AwsCustomResourcePolicy.fromSdkCalls({resources: AwsCustomResourcePolicy.ANY_RESOURCE}) + policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ + resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE, + }), }); // Use the value in another construct with -getParameter.getResponseField('Parameter.Value') +getParameter.getResponseField('Parameter.Value'); ``` #### Associate a PrivateHostedZone with VPC shared from another account ```ts -const getParameter = new AwsCustomResource(this, 'AssociateVPCWithHostedZone', { +const getParameter = new cr.AwsCustomResource(this, 'AssociateVPCWithHostedZone', { onCreate: { assumedRoleArn: 'arn:aws:iam::OTHERACCOUNT:role/CrossAccount/ManageHostedZoneConnections', service: 'Route53', @@ -564,16 +587,17 @@ const getParameter = new AwsCustomResource(this, 'AssociateVPCWithHostedZone', { parameters: { HostedZoneId: 'hz-123', VPC: { - VPCId: 'vpc-123', - VPCRegion: 'region-for-vpc' - } + VPCId: 'vpc-123', + VPCRegion: 'region-for-vpc', + }, }, - physicalResourceId: PhysicalResourceId.of('${vpcStack.SharedVpc.VpcId}-${vpcStack.Region}-${PrivateHostedZone.HostedZoneId}') + physicalResourceId: cr.PhysicalResourceId.of('${vpcStack.SharedVpc.VpcId}-${vpcStack.Region}-${PrivateHostedZone.HostedZoneId}'), }, //Will ignore any resource and use the assumedRoleArn as resource and 'sts:AssumeRole' for service:action - policy: AwsCustomResourcePolicy.fromSdkCalls({resources: AwsCustomResourcePolicy.ANY_RESOURCE}) + policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ + resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE, + }), }); - ``` --- diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index e14d766ee5367..af0a8eaa7d508 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -72,7 +79,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-ssm": "0.0.0", @@ -80,14 +87,14 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/aws-lambda": "^8.10.89", + "@types/aws-lambda": "^8.10.92", "@types/fs-extra": "^8.1.2", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "@types/sinon": "^9.0.11", "aws-sdk": "^2.848.0", - "aws-sdk-mock": "^5.5.0", + "aws-sdk-mock": "5.6.0", "fs-extra": "^9.1.0", - "nock": "^13.2.1", + "nock": "^13.2.4", "sinon": "^9.2.4" }, "dependencies": { diff --git a/packages/@aws-cdk/custom-resources/rosetta/default.ts-fixture b/packages/@aws-cdk/custom-resources/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..b80888ebeedd0 --- /dev/null +++ b/packages/@aws-cdk/custom-resources/rosetta/default.ts-fixture @@ -0,0 +1,16 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { CustomResource, Duration, Stack } from '@aws-cdk/core'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cr from '@aws-cdk/custom-resources'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as logs from '@aws-cdk/aws-logs'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts index f072c0f926875..c3df398f6f5dd 100644 --- a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts +++ b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as logs from '@aws-cdk/aws-logs'; import * as cdk from '@aws-cdk/core'; @@ -33,7 +33,7 @@ test('aws sdk js custom resource with onCreate and onDelete', () => { }); // THEN - expect(stack).toHaveResource('Custom::LogRetentionPolicy', { + Template.fromStack(stack).hasResourceProperties('Custom::LogRetentionPolicy', { 'Create': JSON.stringify({ 'service': 'CloudWatchLogs', 'action': 'putRetentionPolicy', @@ -55,7 +55,7 @@ test('aws sdk js custom resource with onCreate and onDelete', () => { 'InstallLatestAwsSdk': true, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -95,7 +95,7 @@ test('onCreate defaults to onUpdate', () => { }); // THEN - expect(stack).toHaveResource('Custom::S3PutObject', { + Template.fromStack(stack).hasResourceProperties('Custom::S3PutObject', { 'Create': JSON.stringify({ 'service': 's3', 'action': 'putObject', @@ -148,7 +148,7 @@ test('with custom policyStatements', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -207,7 +207,7 @@ test('booleans are encoded in the stringified parameters object', () => { }); // THEN - expect(stack).toHaveResource('Custom::ServiceAction', { + Template.fromStack(stack).hasResourceProperties('Custom::ServiceAction', { 'Create': JSON.stringify({ 'service': 'service', 'action': 'action', @@ -263,7 +263,7 @@ test('encodes physical resource id reference', () => { }); // THEN - expect(stack).toHaveResource('Custom::ServiceAction', { + Template.fromStack(stack).hasResourceProperties('Custom::ServiceAction', { 'Create': JSON.stringify({ 'service': 'service', 'action': 'action', @@ -296,7 +296,7 @@ test('timeout defaults to 2 minutes', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Timeout: 120, }); }); @@ -317,7 +317,7 @@ test('can specify timeout', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Timeout: 900, }); }); @@ -340,7 +340,7 @@ test('implements IGrantable', () => { // WHEN role.grantPassRole(customResource.grantPrincipal); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -376,11 +376,11 @@ test('can use existing role', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Role: 'arn:aws:iam::123456789012:role/CoolRole', }); - expect(stack).not.toHaveResource('AWS::IAM::Role'); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 0); }); test('getData', () => { @@ -524,7 +524,7 @@ test('getDataString', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWS', { + Template.fromStack(stack).hasResourceProperties('Custom::AWS', { Create: { 'Fn::Join': [ '', @@ -559,7 +559,7 @@ test('can specify log retention', () => { }); // THEN - expect(stack).toHaveResource('Custom::LogRetention', { + Template.fromStack(stack).hasResourceProperties('Custom::LogRetention', { LogGroupName: { 'Fn::Join': [ '', @@ -591,7 +591,7 @@ test('disable AWS SDK installation', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWS', { + Template.fromStack(stack).hasResourceProperties('Custom::AWS', { 'InstallLatestAwsSdk': false, }); }); @@ -612,7 +612,7 @@ test('can specify function name', () => { }); // THEN - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { FunctionName: 'my-cool-function', }); }); @@ -640,7 +640,7 @@ test('separate policies per custom resource', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -652,7 +652,7 @@ test('separate policies per custom resource', () => { Version: '2012-10-17', }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -690,7 +690,7 @@ test('tokens can be used as dictionary keys', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWS', { + Template.fromStack(stack).hasResourceProperties('Custom::AWS', { Create: { 'Fn::Join': [ '', @@ -730,7 +730,7 @@ test('assumedRoleArn adds statement for sts:assumeRole', () => { // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { diff --git a/packages/@aws-cdk/custom-resources/test/provider-framework/provider.test.ts b/packages/@aws-cdk/custom-resources/test/provider-framework/provider.test.ts index 724c76425a1ee..ac09be73e0f89 100644 --- a/packages/@aws-cdk/custom-resources/test/provider-framework/provider.test.ts +++ b/packages/@aws-cdk/custom-resources/test/provider-framework/provider.test.ts @@ -1,4 +1,5 @@ import * as path from 'path'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; @@ -7,8 +8,6 @@ import { Duration, Stack } from '@aws-cdk/core'; import * as cr from '../../lib'; import * as util from '../../lib/provider-framework/util'; -import '@aws-cdk/assert-internal/jest'; - test('security groups are applied to all framework functions', () => { // GIVEN @@ -34,7 +33,7 @@ test('security groups are applied to all framework functions', () => { securityGroups: [securityGroup], }); - expect(stack).toHaveResourceLike('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Handler: 'framework.onEvent', VpcConfig: { SecurityGroupIds: [ @@ -48,7 +47,7 @@ test('security groups are applied to all framework functions', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Handler: 'framework.isComplete', VpcConfig: { SecurityGroupIds: [ @@ -62,7 +61,7 @@ test('security groups are applied to all framework functions', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Handler: 'framework.onTimeout', VpcConfig: { SecurityGroupIds: [ @@ -101,7 +100,7 @@ test('vpc is applied to all framework functions', () => { vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE }, }); - expect(stack).toHaveResourceLike('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Handler: 'framework.onEvent', VpcConfig: { SubnetIds: [ @@ -111,7 +110,7 @@ test('vpc is applied to all framework functions', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Handler: 'framework.isComplete', VpcConfig: { SubnetIds: [ @@ -121,7 +120,7 @@ test('vpc is applied to all framework functions', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Handler: 'framework.onTimeout', VpcConfig: { SubnetIds: [ @@ -149,23 +148,23 @@ test('minimal setup', () => { // THEN // framework "onEvent" handler - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Handler: 'framework.onEvent', Environment: { Variables: { USER_ON_EVENT_FUNCTION_ARN: { 'Fn::GetAtt': ['MyHandler6B74D312', 'Arn'] } } }, Timeout: 900, }); // user "onEvent" handler - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Handler: 'index.onEvent', }); // no framework "is complete" handler or state machine - expect(stack).not.toHaveResource('AWS::StepFunctions::StateMachine'); - expect(stack).not.toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).resourceCountIs('AWS::StepFunctions::StateMachine', 0); + expect(Template.fromStack(stack).findResources('AWS::Lambda::Function', { Handler: 'framework.isComplete', Timeout: 900, - }); + })).toEqual({}); }); test('if isComplete is specified, the isComplete framework handler is also included', () => { @@ -193,7 +192,7 @@ test('if isComplete is specified, the isComplete framework handler is also inclu }, }; - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Handler: 'framework.onEvent', Timeout: 900, Environment: { @@ -204,19 +203,19 @@ test('if isComplete is specified, the isComplete framework handler is also inclu }, }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Handler: 'framework.isComplete', Timeout: 900, Environment: expectedEnv, }); - expect(stack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Handler: 'framework.onTimeout', Timeout: 900, Environment: expectedEnv, }); - expect(stack).toHaveResource('AWS::StepFunctions::StateMachine', { + Template.fromStack(stack).hasResourceProperties('AWS::StepFunctions::StateMachine', { DefinitionString: { 'Fn::Join': [ '', @@ -310,7 +309,7 @@ describe('log retention', () => { }); // THEN - expect(stack).toHaveResource('Custom::LogRetention', { + Template.fromStack(stack).hasResourceProperties('Custom::LogRetention', { LogGroupName: { 'Fn::Join': [ '', @@ -340,7 +339,7 @@ describe('log retention', () => { }); // THEN - expect(stack).not.toHaveResource('Custom::LogRetention'); + Template.fromStack(stack).resourceCountIs('Custom::LogRetention', 0); }); }); @@ -363,7 +362,7 @@ describe('role', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Role: { 'Fn::GetAtt': [ 'MyRoleF48FFE04', @@ -387,7 +386,7 @@ describe('role', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Role: { 'Fn::GetAtt': [ 'MyProviderframeworkonEventServiceRole8761E48D', @@ -415,7 +414,7 @@ describe('name', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::Lambda::Function', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { FunctionName: providerFunctionName, }); }); diff --git a/packages/@aws-cdk/custom-resources/test/provider-framework/waiter-state-machine.test.ts b/packages/@aws-cdk/custom-resources/test/provider-framework/waiter-state-machine.test.ts index f9b683417dc94..7548f4e151041 100644 --- a/packages/@aws-cdk/custom-resources/test/provider-framework/waiter-state-machine.test.ts +++ b/packages/@aws-cdk/custom-resources/test/provider-framework/waiter-state-machine.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Code, Function as lambdaFn, Runtime } from '@aws-cdk/aws-lambda'; import { Duration, Stack } from '@aws-cdk/core'; import { Node } from 'constructs'; @@ -35,7 +35,7 @@ describe('state machine', () => { // THEN const roleId = 'statemachineRole52044F93'; - expect(stack).toHaveResourceLike('AWS::StepFunctions::StateMachine', { + Template.fromStack(stack).hasResourceProperties('AWS::StepFunctions::StateMachine', { DefinitionString: { 'Fn::Join': [ '', @@ -54,7 +54,7 @@ describe('state machine', () => { 'Fn::GetAtt': [roleId, 'Arn'], }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -77,7 +77,7 @@ describe('state machine', () => { Version: '2012-10-17', }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { diff --git a/packages/@aws-cdk/cx-api/lib/artifacts/cloudformation-artifact.ts b/packages/@aws-cdk/cx-api/lib/artifacts/cloudformation-artifact.ts index 225f256e85f5f..3e6f395b5c99d 100644 --- a/packages/@aws-cdk/cx-api/lib/artifacts/cloudformation-artifact.ts +++ b/packages/@aws-cdk/cx-api/lib/artifacts/cloudformation-artifact.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { CloudArtifact } from '../cloud-artifact'; -import { CloudAssembly } from '../cloud-assembly'; +import type { CloudAssembly } from '../cloud-assembly'; import { Environment, EnvironmentUtils } from '../environment'; export class CloudFormationStackArtifact extends CloudArtifact { @@ -75,6 +75,13 @@ export class CloudFormationStackArtifact extends CloudArtifact { */ public readonly cloudFormationExecutionRoleArn?: string; + /** + * The role to use to look up values from the target AWS account + * + * @default - No role is assumed (current credentials are used) + */ + public readonly lookupRole?: cxschema.BootstrapRole; + /** * If the stack template has already been included in the asset manifest, its asset URL * @@ -135,6 +142,7 @@ export class CloudFormationStackArtifact extends CloudArtifact { this.bootstrapStackVersionSsmParameter = properties.bootstrapStackVersionSsmParameter; this.terminationProtection = properties.terminationProtection; this.validateOnSynth = properties.validateOnSynth; + this.lookupRole = properties.lookupRole; this.stackName = properties.stackName || artifactId; this.assets = this.findMetadataByType(cxschema.ArtifactMetadataEntryType.ASSET).map(e => e.data as cxschema.AssetMetadataEntry); diff --git a/packages/@aws-cdk/cx-api/lib/artifacts/nested-cloud-assembly-artifact-aug.ts b/packages/@aws-cdk/cx-api/lib/artifacts/nested-cloud-assembly-artifact-aug.ts new file mode 100644 index 0000000000000..cb6c54a806ee5 --- /dev/null +++ b/packages/@aws-cdk/cx-api/lib/artifacts/nested-cloud-assembly-artifact-aug.ts @@ -0,0 +1,18 @@ +import { CloudAssembly } from '../cloud-assembly'; +import { NestedCloudAssemblyArtifact } from './nested-cloud-assembly-artifact'; + +const cacheSym = Symbol(); + +/** + * The nested Assembly + * + * Declared in a different file to break circular dep between CloudAssembly and NestedCloudAssemblyArtifact + */ +Object.defineProperty(NestedCloudAssemblyArtifact.prototype, 'nestedAssembly', { + get() { + if (!this[cacheSym]) { + this[cacheSym] = new CloudAssembly(this.fullPath); + } + return this[cacheSym]; + }, +}); diff --git a/packages/@aws-cdk/cx-api/lib/artifacts/nested-cloud-assembly-artifact.ts b/packages/@aws-cdk/cx-api/lib/artifacts/nested-cloud-assembly-artifact.ts index 3aa57577851b0..61684316949d4 100644 --- a/packages/@aws-cdk/cx-api/lib/artifacts/nested-cloud-assembly-artifact.ts +++ b/packages/@aws-cdk/cx-api/lib/artifacts/nested-cloud-assembly-artifact.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { CloudArtifact } from '../cloud-artifact'; -import { CloudAssembly } from '../cloud-assembly'; +import type { CloudAssembly } from '../cloud-assembly'; /** * Asset manifest is a description of a set of assets which need to be built and published @@ -17,11 +17,6 @@ export class NestedCloudAssemblyArtifact extends CloudArtifact { */ public readonly displayName: string; - /** - * Cache for the inner assembly loading - */ - private _nestedAssembly?: CloudAssembly; - constructor(assembly: CloudAssembly, name: string, artifact: cxschema.ArtifactManifest) { super(assembly, name, artifact); @@ -36,14 +31,13 @@ export class NestedCloudAssemblyArtifact extends CloudArtifact { public get fullPath(): string { return path.join(this.assembly.directory, this.directoryName); } +} +export interface NestedCloudAssemblyArtifact { /** * The nested Assembly */ - public get nestedAssembly(): CloudAssembly { - if (!this._nestedAssembly) { - this._nestedAssembly = new CloudAssembly(this.fullPath); - } - return this._nestedAssembly; - } -} + readonly nestedAssembly: CloudAssembly; + + // Declared in a different file +} \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/lib/cloud-artifact-aug.ts b/packages/@aws-cdk/cx-api/lib/cloud-artifact-aug.ts new file mode 100644 index 0000000000000..7afadb73a5c54 --- /dev/null +++ b/packages/@aws-cdk/cx-api/lib/cloud-artifact-aug.ts @@ -0,0 +1,32 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import { AssetManifestArtifact } from './artifacts/asset-manifest-artifact'; +import { CloudFormationStackArtifact } from './artifacts/cloudformation-artifact'; +import { NestedCloudAssemblyArtifact } from './artifacts/nested-cloud-assembly-artifact'; +import { TreeCloudArtifact } from './artifacts/tree-cloud-artifact'; +import { CloudArtifact } from './cloud-artifact'; +import { CloudAssembly } from './cloud-assembly'; + +/** + * Add the 'fromManifest' factory function + * + * It is defined in a separate file to avoid circular dependencies between 'cloud-artifact.ts' + * and all of its subclass files. + */ +CloudArtifact.fromManifest = function fromManifest( + assembly: CloudAssembly, + id: string, + artifact: cxschema.ArtifactManifest, +): CloudArtifact | undefined { + switch (artifact.type) { + case cxschema.ArtifactType.AWS_CLOUDFORMATION_STACK: + return new CloudFormationStackArtifact(assembly, id, artifact); + case cxschema.ArtifactType.CDK_TREE: + return new TreeCloudArtifact(assembly, id, artifact); + case cxschema.ArtifactType.ASSET_MANIFEST: + return new AssetManifestArtifact(assembly, id, artifact); + case cxschema.ArtifactType.NESTED_CLOUD_ASSEMBLY: + return new NestedCloudAssemblyArtifact(assembly, id, artifact); + default: + return undefined; + } +}; \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/lib/cloud-artifact.ts b/packages/@aws-cdk/cx-api/lib/cloud-artifact.ts index 7f4e5a899983a..528104c678199 100644 --- a/packages/@aws-cdk/cx-api/lib/cloud-artifact.ts +++ b/packages/@aws-cdk/cx-api/lib/cloud-artifact.ts @@ -1,5 +1,5 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; -import { CloudAssembly } from './cloud-assembly'; +import type { CloudAssembly } from './cloud-assembly'; import { MetadataEntryResult, SynthesisMessage, SynthesisMessageLevel } from './metadata'; /** @@ -36,24 +36,16 @@ export interface AwsCloudFormationStackProperties { export class CloudArtifact { /** * Returns a subclass of `CloudArtifact` based on the artifact type defined in the artifact manifest. + * * @param assembly The cloud assembly from which to load the artifact * @param id The artifact ID * @param artifact The artifact manifest * @returns the `CloudArtifact` that matches the artifact type or `undefined` if it's an artifact type that is unrecognized by this module. */ public static fromManifest(assembly: CloudAssembly, id: string, artifact: cxschema.ArtifactManifest): CloudArtifact | undefined { - switch (artifact.type) { - case cxschema.ArtifactType.AWS_CLOUDFORMATION_STACK: - return new CloudFormationStackArtifact(assembly, id, artifact); - case cxschema.ArtifactType.CDK_TREE: - return new TreeCloudArtifact(assembly, id, artifact); - case cxschema.ArtifactType.ASSET_MANIFEST: - return new AssetManifestArtifact(assembly, id, artifact); - case cxschema.ArtifactType.NESTED_CLOUD_ASSEMBLY: - return new NestedCloudAssemblyArtifact(assembly, id, artifact); - default: - return undefined; - } + // Implementation is defined in a separate file to break cyclic dependencies + void(assembly), void(id), void(artifact); + throw new Error('Implementation not overridden yet'); } /** @@ -152,9 +144,3 @@ export class CloudArtifact { return this.manifest.displayName ?? this.id; } } - -// needs to be defined at the end to avoid a cyclic dependency -import { AssetManifestArtifact } from './artifacts/asset-manifest-artifact'; -import { CloudFormationStackArtifact } from './artifacts/cloudformation-artifact'; -import { NestedCloudAssemblyArtifact } from './artifacts/nested-cloud-assembly-artifact'; -import { TreeCloudArtifact } from './artifacts/tree-cloud-artifact'; \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/lib/features.ts b/packages/@aws-cdk/cx-api/lib/features.ts index 74f4ff63082a6..9278717ef1c15 100644 --- a/packages/@aws-cdk/cx-api/lib/features.ts +++ b/packages/@aws-cdk/cx-api/lib/features.ts @@ -3,8 +3,18 @@ // implemented behind a flag in order to preserve backwards compatibility for // existing apps. When a new app is initialized through `cdk init`, the CLI will // automatically add enable these features by adding them to the generated -// `cdk.json` file. In the next major release of the CDK, these feature flags -// will be removed and will become the default behavior. +// `cdk.json` file. +// +// Some of these flags only affect the behavior of the construct library -- +// these will be removed in the next major release and the behavior they are +// gating will become the only behavior. +// +// Other flags also affect the generated CloudFormation templates, in a way +// that prevents seamless upgrading. In the next major version, their +// behavior will become the default, but the flag still exists so users can +// switch it *off* in order to revert to the old behavior. These flags +// are marked with with the [PERMANENT] tag below. +// // See https://github.com/aws/aws-cdk-rfcs/blob/master/text/0055-feature-flags.md // -------------------------------------------------------------------------------- @@ -31,6 +41,8 @@ export const ENABLE_DIFF_NO_FAIL = ENABLE_DIFF_NO_FAIL_CONTEXT; /** * Switch to new stack synthesis method which enable CI/CD + * + * [PERMANENT] */ export const NEW_STYLE_STACK_SYNTHESIS_CONTEXT = '@aws-cdk/core:newStyleStackSynthesis'; @@ -41,6 +53,8 @@ export const NEW_STYLE_STACK_SYNTHESIS_CONTEXT = '@aws-cdk/core:newStyleStackSyn * ensure uniqueness, and makes the export names robust against refactoring * the location of the stack in the construct tree (specifically, moving the Stack * into a Stage). + * + * [PERMANENT] */ export const STACK_RELATIVE_EXPORTS_CONTEXT = '@aws-cdk/core:stackRelativeExports'; @@ -116,6 +130,8 @@ export const ECS_REMOVE_DEFAULT_DESIRED_COUNT = '@aws-cdk/aws-ecs-patterns:remov * * This feature flag make correct the ServerlessCluster.clusterArn when * clusterIdentifier contains a Upper case letters. + * + * [PERMANENT] */ export const RDS_LOWERCASE_DB_IDENTIFIER = '@aws-cdk/aws-rds:lowercaseDbIdentifier'; @@ -132,6 +148,8 @@ export const RDS_LOWERCASE_DB_IDENTIFIER = '@aws-cdk/aws-rds:lowercaseDbIdentifi * * In effect, there is no way to get out of this mess in a backwards compatible way, while supporting existing stacks. * This flag changes the logical id layout of UsagePlanKey to not be sensitive to order. + * + * [PERMANENT] */ export const APIGATEWAY_USAGEPLANKEY_ORDERINSENSITIVE_ID = '@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId'; @@ -150,6 +168,8 @@ export const EFS_DEFAULT_ENCRYPTION_AT_REST = '@aws-cdk/aws-efs:defaultEncryptio * not constitute creating a new Version. * * See 'currentVersion' section in the aws-lambda module's README for more details. + * + * [PERMANENT] */ export const LAMBDA_RECOGNIZE_VERSION_PROPS = '@aws-cdk/aws-lambda:recognizeVersionProps'; @@ -157,6 +177,8 @@ export const LAMBDA_RECOGNIZE_VERSION_PROPS = '@aws-cdk/aws-lambda:recognizeVers * Enable this feature flag to have cloudfront distributions use the security policy TLSv1.2_2021 by default. * * The security policy can also be configured explicitly using the `minimumProtocolVersion` property. + * + * [PERMANENT] */ export const CLOUDFRONT_DEFAULT_SECURITY_POLICY_TLS_V1_2_2021 = '@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021'; @@ -167,19 +189,38 @@ export const CLOUDFRONT_DEFAULT_SECURITY_POLICY_TLS_V1_2_2021 = '@aws-cdk/aws-cl * of unnecessary regions included in stacks without a known region. * * The type of this value should be a list of strings. + * + * [PERMANENT] */ export const TARGET_PARTITIONS = '@aws-cdk/core:target-partitions'; /** - * This map includes context keys and values for feature flags that enable - * capabilities "from the future", which we could not introduce as the default - * behavior due to backwards compatibility for existing projects. + * Enable this feature flag to configure default logging behavior for the ECS Service Extensions. This will enable the + * `awslogs` log driver for the application container of the service to send the container logs to CloudWatch Logs. * - * New projects generated through `cdk init` will include these flags in their - * generated `cdk.json` file. + * This is a feature flag as the new behavior provides a better default experience for the users. * - * When we release the next major version of the CDK, we will flip the logic of - * these features and clean up the `cdk.json` generated by `cdk init`. + * [PERMANENT] + */ +export const ECS_SERVICE_EXTENSIONS_ENABLE_DEFAULT_LOG_DRIVER = '@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver'; + +/** + * Enable this feature flag to have Launch Templates generated by the `InstanceRequireImdsv2Aspect` use unique names. + * + * Previously, the generated Launch Template names were only unique within a stack because they were based only on the + * `Instance` construct ID. If another stack that has an `Instance` with the same construct ID is deployed in the same + * account and region, the deployments would always fail as the generated Launch Template names were the same. + * + * The new implementation addresses this issue by generating the Launch Template name with the `Names.uniqueId` method. + */ +export const EC2_UNIQUE_IMDSV2_LAUNCH_TEMPLATE_NAME = '@aws-cdk/aws-ec2:uniqueImdsv2TemplateName'; + +/** + * Flag values that should apply for new projects + * + * Add a flag in here (typically with the value `true`), to enable + * backwards-breaking behavior changes only for new projects. New projects + * generated through `cdk init` will include these flags in their generated * * Tests must cover the default (disabled) case and the future (enabled) case. */ @@ -197,9 +238,8 @@ export const FUTURE_FLAGS: { [key: string]: boolean } = { [EFS_DEFAULT_ENCRYPTION_AT_REST]: true, [LAMBDA_RECOGNIZE_VERSION_PROPS]: true, [CLOUDFRONT_DEFAULT_SECURITY_POLICY_TLS_V1_2_2021]: true, - - // We will advertise this flag when the feature is complete - // [NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: 'true', + [ECS_SERVICE_EXTENSIONS_ENABLE_DEFAULT_LOG_DRIVER]: true, + [EC2_UNIQUE_IMDSV2_LAUNCH_TEMPLATE_NAME]: true, }; /** @@ -217,26 +257,17 @@ export const FUTURE_FLAGS_EXPIRED: string[] = [ ]; /** - * The set of defaults that should be applied if the feature flag is not - * explicitly configured. + * The default values of each of these flags. + * + * This is the effective value of the flag, unless it's overriden via + * context. + * + * Adding new flags here is only allowed during the pre-release period of a new + * major version! */ const FUTURE_FLAGS_DEFAULTS: { [key: string]: boolean } = { - [APIGATEWAY_USAGEPLANKEY_ORDERINSENSITIVE_ID]: false, - [ENABLE_STACK_NAME_DUPLICATES_CONTEXT]: false, - [ENABLE_DIFF_NO_FAIL_CONTEXT]: false, - [STACK_RELATIVE_EXPORTS_CONTEXT]: false, - [NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false, - [DOCKER_IGNORE_SUPPORT]: false, - [SECRETS_MANAGER_PARSE_OWNED_SECRET_NAME]: false, - [KMS_DEFAULT_KEY_POLICIES]: false, - [S3_GRANT_WRITE_WITHOUT_ACL]: false, - [ECS_REMOVE_DEFAULT_DESIRED_COUNT]: false, - [RDS_LOWERCASE_DB_IDENTIFIER]: false, - [EFS_DEFAULT_ENCRYPTION_AT_REST]: false, - [LAMBDA_RECOGNIZE_VERSION_PROPS]: false, - [CLOUDFRONT_DEFAULT_SECURITY_POLICY_TLS_V1_2_2021]: false, }; -export function futureFlagDefault(flag: string): boolean | undefined { - return FUTURE_FLAGS_DEFAULTS[flag]; +export function futureFlagDefault(flag: string): boolean { + return FUTURE_FLAGS_DEFAULTS[flag] ?? false; } diff --git a/packages/@aws-cdk/cx-api/lib/index.ts b/packages/@aws-cdk/cx-api/lib/index.ts index 0b0d7b519e5d7..f5db330871753 100644 --- a/packages/@aws-cdk/cx-api/lib/index.ts +++ b/packages/@aws-cdk/cx-api/lib/index.ts @@ -7,10 +7,12 @@ export * from './context/endpoint-service-availability-zones'; export * from './context/security-group'; export * from './context/key'; export * from './cloud-artifact'; +import './cloud-artifact-aug'; export * from './artifacts/asset-manifest-artifact'; export * from './artifacts/cloudformation-artifact'; export * from './artifacts/tree-cloud-artifact'; export * from './artifacts/nested-cloud-assembly-artifact'; +import './artifacts/nested-cloud-assembly-artifact-aug'; export * from './cloud-assembly'; export * from './assets'; export * from './environment'; diff --git a/packages/@aws-cdk/cx-api/package.json b/packages/@aws-cdk/cx-api/package.json index c5440f10adc33..90941529df1f3 100644 --- a/packages/@aws-cdk/cx-api/package.json +++ b/packages/@aws-cdk/cx-api/package.json @@ -40,7 +40,7 @@ "scripts": { "build": "cdk-build", "watch": "cdk-watch", - "lint": "cdk-lint", + "lint": "cdk-lint && madge --circular --extensions js lib", "test": "cdk-test", "pkglint": "pkglint -f", "package": "cdk-package", @@ -68,10 +68,11 @@ "devDependencies": { "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "@types/mock-fs": "^4.13.1", "@types/semver": "^7.3.9", - "jest": "^27.4.5", + "jest": "^27.5.1", + "madge": "^5.0.1", "mock-fs": "^4.14.0" }, "repository": { diff --git a/packages/@aws-cdk/cx-api/test/features.test.ts b/packages/@aws-cdk/cx-api/test/features.test.ts index afc9c0838d7da..b71927bfeeffd 100644 --- a/packages/@aws-cdk/cx-api/test/features.test.ts +++ b/packages/@aws-cdk/cx-api/test/features.test.ts @@ -7,8 +7,8 @@ test('all future flags have defaults configured', () => { }); }); -test('futureFlagDefault returns undefined if non existent flag was given', () => { - expect(feats.futureFlagDefault('non-existent-flag')).toEqual(undefined); +test('futureFlagDefault returns false if non existent flag was given', () => { + expect(feats.futureFlagDefault('non-existent-flag')).toEqual(false); }); testLegacyBehavior('FUTURE_FLAGS_EXPIRED must be empty in CDKv1', Object, () => { diff --git a/packages/@aws-cdk/example-construct-library/package.json b/packages/@aws-cdk/example-construct-library/package.json index 5ccda022a31b1..0846135bf2e90 100644 --- a/packages/@aws-cdk/example-construct-library/package.json +++ b/packages/@aws-cdk/example-construct-library/package.json @@ -29,7 +29,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -70,8 +77,8 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-cloudwatch": "0.0.0", diff --git a/packages/@aws-cdk/lambda-layer-awscli/README.md b/packages/@aws-cdk/lambda-layer-awscli/README.md index e36677c222de0..c902510290dde 100644 --- a/packages/@aws-cdk/lambda-layer-awscli/README.md +++ b/packages/@aws-cdk/lambda-layer-awscli/README.md @@ -12,6 +12,8 @@ This module exports a single class called `AwsCliLayer` which is a `lambda.Layer` that bundles the AWS CLI. +Any Lambda Function that uses this layer must use a Python 3.x runtime. + Usage: ```ts diff --git a/packages/@aws-cdk/lambda-layer-awscli/layer/Dockerfile b/packages/@aws-cdk/lambda-layer-awscli/layer/Dockerfile index 7cf1287e2023c..ef5c7c91d65c6 100644 --- a/packages/@aws-cdk/lambda-layer-awscli/layer/Dockerfile +++ b/packages/@aws-cdk/lambda-layer-awscli/layer/Dockerfile @@ -1,10 +1,4 @@ -FROM public.ecr.aws/lambda/provided:latest - -# -# versions -# - -ARG AWSCLI_VERSION=1.18.198 +FROM public.ecr.aws/sam/build-python3.7 USER root RUN mkdir -p /opt @@ -21,23 +15,25 @@ RUN yum update -y \ # aws cli # -RUN curl https://s3.amazonaws.com/aws-cli/awscli-bundle-${AWSCLI_VERSION}.zip -o awscli-bundle.zip -RUN unzip awscli-bundle.zip -RUN ./awscli-bundle/install -i /opt/awscli -b /opt/awscli/aws +COPY requirements.txt ./ +RUN python -m pip install -r requirements.txt -t /opt/awscli # organize for self-contained usage -RUN mv /opt/awscli /opt/awscli.tmp -RUN mv /opt/awscli.tmp/lib/python2.7/site-packages /opt/awscli -RUN mv /opt/awscli.tmp/bin /opt/awscli/bin -RUN mv /opt/awscli/bin/aws /opt/awscli +RUN mv /opt/awscli/bin/aws /opt/awscli # cleanup -RUN rm -fr /opt/awscli.tmp RUN rm -rf \ /opt/awscli/pip* \ /opt/awscli/setuptools* \ /opt/awscli/awscli/examples +# +# Test that the CLI works +# + +RUN yum install -y groff +RUN /opt/awscli/aws help + # # create the bundle # @@ -48,4 +44,4 @@ RUN cd /opt \ && ls -alh /layer.zip; WORKDIR / -ENTRYPOINT [ "/bin/bash" ] \ No newline at end of file +ENTRYPOINT [ "/bin/bash" ] diff --git a/packages/@aws-cdk/lambda-layer-awscli/layer/requirements.txt b/packages/@aws-cdk/lambda-layer-awscli/layer/requirements.txt new file mode 100644 index 0000000000000..370f676abcc66 --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-awscli/layer/requirements.txt @@ -0,0 +1 @@ +awscli==1.22.58 diff --git a/packages/@aws-cdk/lambda-layer-awscli/lib/awscli-layer.ts b/packages/@aws-cdk/lambda-layer-awscli/lib/awscli-layer.ts index 525babcac82f3..9bc2584fda3ad 100644 --- a/packages/@aws-cdk/lambda-layer-awscli/lib/awscli-layer.ts +++ b/packages/@aws-cdk/lambda-layer-awscli/lib/awscli-layer.ts @@ -1,7 +1,6 @@ -import * as crypto from 'crypto'; -import * as fs from 'fs'; import * as path from 'path'; import * as lambda from '@aws-cdk/aws-lambda'; +import { FileSystem } from '@aws-cdk/core'; import { Construct } from 'constructs'; /** @@ -11,17 +10,10 @@ export class AwsCliLayer extends lambda.LayerVersion { constructor(scope: Construct, id: string) { super(scope, id, { code: lambda.Code.fromAsset(path.join(__dirname, 'layer.zip'), { - // we hash the Dockerfile (it contains the tools versions) because hashing the zip is non-deterministic - assetHash: hashFile(path.join(__dirname, '..', 'layer', 'Dockerfile')), + // we hash the layer directory (it contains the tools versions and Dockerfile) because hashing the zip is non-deterministic + assetHash: FileSystem.fingerprint(path.join(__dirname, '../layer')), }), description: '/opt/awscli/aws', }); } } - -function hashFile(fileName: string) { - return crypto - .createHash('sha256') - .update(fs.readFileSync(fileName)) - .digest('hex'); -} \ No newline at end of file diff --git a/packages/@aws-cdk/lambda-layer-awscli/package.json b/packages/@aws-cdk/lambda-layer-awscli/package.json index 328cf50f7ecde..7daf454a10df1 100644 --- a/packages/@aws-cdk/lambda-layer-awscli/package.json +++ b/packages/@aws-cdk/lambda-layer-awscli/package.json @@ -73,12 +73,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", + "@aws-cdk/custom-resources": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-lambda": "0.0.0", diff --git a/packages/@aws-cdk/lambda-layer-awscli/test/awscli-layer.test.ts b/packages/@aws-cdk/lambda-layer-awscli/test/awscli-layer.test.ts index 2a5c4b7d80043..561bb87a14ac7 100644 --- a/packages/@aws-cdk/lambda-layer-awscli/test/awscli-layer.test.ts +++ b/packages/@aws-cdk/lambda-layer-awscli/test/awscli-layer.test.ts @@ -1,6 +1,6 @@ import { Stack } from '@aws-cdk/core'; import { AwsCliLayer } from '../lib'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; test('synthesized to a layer version', () => { //GIVEN @@ -10,7 +10,7 @@ test('synthesized to a layer version', () => { new AwsCliLayer(stack, 'MyLayer'); // THEN - expect(stack).toHaveResource('AWS::Lambda::LayerVersion', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::LayerVersion', { Description: '/opt/awscli/aws', }); }); diff --git a/packages/@aws-cdk/lambda-layer-awscli/test/integ.awscli-layer.expected.json b/packages/@aws-cdk/lambda-layer-awscli/test/integ.awscli-layer.expected.json new file mode 100644 index 0000000000000..d37e67106e0cf --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-awscli/test/integ.awscli-layer.expected.json @@ -0,0 +1,773 @@ +{ + "Resources": { + "AwsCliLayerF44AAF94": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": { + "S3Bucket": { + "Ref": "AssetParametersba23ea22aa357b771a4ebc95be163f8848dafee07daf2333380d3b890472d1f3S3BucketD774C319" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersba23ea22aa357b771a4ebc95be163f8848dafee07daf2333380d3b890472d1f3S3VersionKey9C5C53B3" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersba23ea22aa357b771a4ebc95be163f8848dafee07daf2333380d3b890472d1f3S3VersionKey9C5C53B3" + } + ] + } + ] + } + ] + ] + } + }, + "Description": "/opt/awscli/aws" + } + }, + "Lambdapython36ServiceRole658256F3": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "Lambdapython36B64E8A5D": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters5dff6208ccd5fb196bb0354fd6e47faa8431a789e6125d20386586fef761ed48S3Bucket1DD21439" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5dff6208ccd5fb196bb0354fd6e47faa8431a789e6125d20386586fef761ed48S3VersionKey77F71A63" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5dff6208ccd5fb196bb0354fd6e47faa8431a789e6125d20386586fef761ed48S3VersionKey77F71A63" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "Lambdapython36ServiceRole658256F3", + "Arn" + ] + }, + "Handler": "index.handler", + "Layers": [ + { + "Ref": "AwsCliLayerF44AAF94" + } + ], + "MemorySize": 512, + "Runtime": "python3.6", + "Timeout": 30 + }, + "DependsOn": [ + "Lambdapython36ServiceRole658256F3" + ] + }, + "Providerpython36frameworkonEventServiceRole0B176429": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "Providerpython36frameworkonEventServiceRoleDefaultPolicy414F3608": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Lambdapython36B64E8A5D", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "Providerpython36frameworkonEventServiceRoleDefaultPolicy414F3608", + "Roles": [ + { + "Ref": "Providerpython36frameworkonEventServiceRole0B176429" + } + ] + } + }, + "Providerpython36frameworkonEventA3B6DD44": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters733a1180c316ce99003dfcfd7bd70d8039134b3fbac69643f144aceea90d6b8cS3BucketBA45D90E" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters733a1180c316ce99003dfcfd7bd70d8039134b3fbac69643f144aceea90d6b8cS3VersionKey1021C50F" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters733a1180c316ce99003dfcfd7bd70d8039134b3fbac69643f144aceea90d6b8cS3VersionKey1021C50F" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "Providerpython36frameworkonEventServiceRole0B176429", + "Arn" + ] + }, + "Description": "AWS CDK resource provider framework - onEvent (lambda-layer-awscli-integ-stack/Providerpython3.6)", + "Environment": { + "Variables": { + "USER_ON_EVENT_FUNCTION_ARN": { + "Fn::GetAtt": [ + "Lambdapython36B64E8A5D", + "Arn" + ] + } + } + }, + "Handler": "framework.onEvent", + "Runtime": "nodejs12.x", + "Timeout": 900 + }, + "DependsOn": [ + "Providerpython36frameworkonEventServiceRoleDefaultPolicy414F3608", + "Providerpython36frameworkonEventServiceRole0B176429" + ] + }, + "CustomResourcepython36": { + "Type": "AWS::CloudFormation::CustomResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "Providerpython36frameworkonEventA3B6DD44", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "Lambdapython37ServiceRoleB5A704D4": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "Lambdapython3780349E0A": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters5dff6208ccd5fb196bb0354fd6e47faa8431a789e6125d20386586fef761ed48S3Bucket1DD21439" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5dff6208ccd5fb196bb0354fd6e47faa8431a789e6125d20386586fef761ed48S3VersionKey77F71A63" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5dff6208ccd5fb196bb0354fd6e47faa8431a789e6125d20386586fef761ed48S3VersionKey77F71A63" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "Lambdapython37ServiceRoleB5A704D4", + "Arn" + ] + }, + "Handler": "index.handler", + "Layers": [ + { + "Ref": "AwsCliLayerF44AAF94" + } + ], + "MemorySize": 512, + "Runtime": "python3.7", + "Timeout": 30 + }, + "DependsOn": [ + "Lambdapython37ServiceRoleB5A704D4" + ] + }, + "Providerpython37frameworkonEventServiceRole9EA6B2B0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "Providerpython37frameworkonEventServiceRoleDefaultPolicyA9099DC2": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Lambdapython3780349E0A", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "Providerpython37frameworkonEventServiceRoleDefaultPolicyA9099DC2", + "Roles": [ + { + "Ref": "Providerpython37frameworkonEventServiceRole9EA6B2B0" + } + ] + } + }, + "Providerpython37frameworkonEvent3AA4F69E": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters733a1180c316ce99003dfcfd7bd70d8039134b3fbac69643f144aceea90d6b8cS3BucketBA45D90E" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters733a1180c316ce99003dfcfd7bd70d8039134b3fbac69643f144aceea90d6b8cS3VersionKey1021C50F" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters733a1180c316ce99003dfcfd7bd70d8039134b3fbac69643f144aceea90d6b8cS3VersionKey1021C50F" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "Providerpython37frameworkonEventServiceRole9EA6B2B0", + "Arn" + ] + }, + "Description": "AWS CDK resource provider framework - onEvent (lambda-layer-awscli-integ-stack/Providerpython3.7)", + "Environment": { + "Variables": { + "USER_ON_EVENT_FUNCTION_ARN": { + "Fn::GetAtt": [ + "Lambdapython3780349E0A", + "Arn" + ] + } + } + }, + "Handler": "framework.onEvent", + "Runtime": "nodejs12.x", + "Timeout": 900 + }, + "DependsOn": [ + "Providerpython37frameworkonEventServiceRoleDefaultPolicyA9099DC2", + "Providerpython37frameworkonEventServiceRole9EA6B2B0" + ] + }, + "CustomResourcepython37": { + "Type": "AWS::CloudFormation::CustomResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "Providerpython37frameworkonEvent3AA4F69E", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "Lambdapython39ServiceRoleE2CFED77": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "Lambdapython39426A0480": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters5dff6208ccd5fb196bb0354fd6e47faa8431a789e6125d20386586fef761ed48S3Bucket1DD21439" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5dff6208ccd5fb196bb0354fd6e47faa8431a789e6125d20386586fef761ed48S3VersionKey77F71A63" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5dff6208ccd5fb196bb0354fd6e47faa8431a789e6125d20386586fef761ed48S3VersionKey77F71A63" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "Lambdapython39ServiceRoleE2CFED77", + "Arn" + ] + }, + "Handler": "index.handler", + "Layers": [ + { + "Ref": "AwsCliLayerF44AAF94" + } + ], + "MemorySize": 512, + "Runtime": "python3.9", + "Timeout": 30 + }, + "DependsOn": [ + "Lambdapython39ServiceRoleE2CFED77" + ] + }, + "Providerpython39frameworkonEventServiceRoleA299F5C1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "Providerpython39frameworkonEventServiceRoleDefaultPolicy16A4767C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Lambdapython39426A0480", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "Providerpython39frameworkonEventServiceRoleDefaultPolicy16A4767C", + "Roles": [ + { + "Ref": "Providerpython39frameworkonEventServiceRoleA299F5C1" + } + ] + } + }, + "Providerpython39frameworkonEvent00AFA742": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters733a1180c316ce99003dfcfd7bd70d8039134b3fbac69643f144aceea90d6b8cS3BucketBA45D90E" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters733a1180c316ce99003dfcfd7bd70d8039134b3fbac69643f144aceea90d6b8cS3VersionKey1021C50F" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters733a1180c316ce99003dfcfd7bd70d8039134b3fbac69643f144aceea90d6b8cS3VersionKey1021C50F" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "Providerpython39frameworkonEventServiceRoleA299F5C1", + "Arn" + ] + }, + "Description": "AWS CDK resource provider framework - onEvent (lambda-layer-awscli-integ-stack/Providerpython3.9)", + "Environment": { + "Variables": { + "USER_ON_EVENT_FUNCTION_ARN": { + "Fn::GetAtt": [ + "Lambdapython39426A0480", + "Arn" + ] + } + } + }, + "Handler": "framework.onEvent", + "Runtime": "nodejs12.x", + "Timeout": 900 + }, + "DependsOn": [ + "Providerpython39frameworkonEventServiceRoleDefaultPolicy16A4767C", + "Providerpython39frameworkonEventServiceRoleA299F5C1" + ] + }, + "CustomResourcepython39": { + "Type": "AWS::CloudFormation::CustomResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "Providerpython39frameworkonEvent00AFA742", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + }, + "Parameters": { + "AssetParametersba23ea22aa357b771a4ebc95be163f8848dafee07daf2333380d3b890472d1f3S3BucketD774C319": { + "Type": "String", + "Description": "S3 bucket for asset \"ba23ea22aa357b771a4ebc95be163f8848dafee07daf2333380d3b890472d1f3\"" + }, + "AssetParametersba23ea22aa357b771a4ebc95be163f8848dafee07daf2333380d3b890472d1f3S3VersionKey9C5C53B3": { + "Type": "String", + "Description": "S3 key for asset version \"ba23ea22aa357b771a4ebc95be163f8848dafee07daf2333380d3b890472d1f3\"" + }, + "AssetParametersba23ea22aa357b771a4ebc95be163f8848dafee07daf2333380d3b890472d1f3ArtifactHash4F540915": { + "Type": "String", + "Description": "Artifact hash for asset \"ba23ea22aa357b771a4ebc95be163f8848dafee07daf2333380d3b890472d1f3\"" + }, + "AssetParameters5dff6208ccd5fb196bb0354fd6e47faa8431a789e6125d20386586fef761ed48S3Bucket1DD21439": { + "Type": "String", + "Description": "S3 bucket for asset \"5dff6208ccd5fb196bb0354fd6e47faa8431a789e6125d20386586fef761ed48\"" + }, + "AssetParameters5dff6208ccd5fb196bb0354fd6e47faa8431a789e6125d20386586fef761ed48S3VersionKey77F71A63": { + "Type": "String", + "Description": "S3 key for asset version \"5dff6208ccd5fb196bb0354fd6e47faa8431a789e6125d20386586fef761ed48\"" + }, + "AssetParameters5dff6208ccd5fb196bb0354fd6e47faa8431a789e6125d20386586fef761ed48ArtifactHashA6792482": { + "Type": "String", + "Description": "Artifact hash for asset \"5dff6208ccd5fb196bb0354fd6e47faa8431a789e6125d20386586fef761ed48\"" + }, + "AssetParameters733a1180c316ce99003dfcfd7bd70d8039134b3fbac69643f144aceea90d6b8cS3BucketBA45D90E": { + "Type": "String", + "Description": "S3 bucket for asset \"733a1180c316ce99003dfcfd7bd70d8039134b3fbac69643f144aceea90d6b8c\"" + }, + "AssetParameters733a1180c316ce99003dfcfd7bd70d8039134b3fbac69643f144aceea90d6b8cS3VersionKey1021C50F": { + "Type": "String", + "Description": "S3 key for asset version \"733a1180c316ce99003dfcfd7bd70d8039134b3fbac69643f144aceea90d6b8c\"" + }, + "AssetParameters733a1180c316ce99003dfcfd7bd70d8039134b3fbac69643f144aceea90d6b8cArtifactHash371618FE": { + "Type": "String", + "Description": "Artifact hash for asset \"733a1180c316ce99003dfcfd7bd70d8039134b3fbac69643f144aceea90d6b8c\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/lambda-layer-awscli/test/integ.awscli-layer.ts b/packages/@aws-cdk/lambda-layer-awscli/test/integ.awscli-layer.ts new file mode 100644 index 0000000000000..a34009c289f5c --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-awscli/test/integ.awscli-layer.ts @@ -0,0 +1,40 @@ +import * as path from 'path'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk from '@aws-cdk/core'; +import * as cr from '@aws-cdk/custom-resources'; + +import { AwsCliLayer } from '../lib'; + +/** + * Test verifies that AWS CLI is invoked successfully inside Lambda runtime. + */ + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'lambda-layer-awscli-integ-stack'); +const layer = new AwsCliLayer(stack, 'AwsCliLayer'); + +const runtimes = [ + lambda.Runtime.PYTHON_3_6, + lambda.Runtime.PYTHON_3_7, + lambda.Runtime.PYTHON_3_9, +]; + +for (const runtime of runtimes) { + const provider = new cr.Provider(stack, `Provider${runtime.name}`, { + onEventHandler: new lambda.Function(stack, `Lambda$${runtime.name}`, { + code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), + handler: 'index.handler', + runtime: runtime, + layers: [layer], + memorySize: 512, + timeout: cdk.Duration.seconds(30), + }), + }); + + new cdk.CustomResource(stack, `CustomResource${runtime.name}`, { + serviceToken: provider.serviceToken, + }); +} + +app.synth(); diff --git a/packages/@aws-cdk/lambda-layer-awscli/test/lambda-handler/index.py b/packages/@aws-cdk/lambda-layer-awscli/test/lambda-handler/index.py new file mode 100644 index 0000000000000..dafef9f351903 --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-awscli/test/lambda-handler/index.py @@ -0,0 +1,5 @@ +import subprocess + +def handler(event, context): + subprocess.check_call(["/opt/awscli/aws", "--version"]) + return diff --git a/packages/@aws-cdk/lambda-layer-kubectl/package.json b/packages/@aws-cdk/lambda-layer-kubectl/package.json index f096461625cf2..2a2223166c3ee 100644 --- a/packages/@aws-cdk/lambda-layer-kubectl/package.json +++ b/packages/@aws-cdk/lambda-layer-kubectl/package.json @@ -73,12 +73,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "pkglint": { "attribution": [ diff --git a/packages/@aws-cdk/lambda-layer-kubectl/test/kubectl-layer.test.ts b/packages/@aws-cdk/lambda-layer-kubectl/test/kubectl-layer.test.ts index c7591af7b3c2b..68374424b0085 100644 --- a/packages/@aws-cdk/lambda-layer-kubectl/test/kubectl-layer.test.ts +++ b/packages/@aws-cdk/lambda-layer-kubectl/test/kubectl-layer.test.ts @@ -1,6 +1,6 @@ import { Stack } from '@aws-cdk/core'; import { KubectlLayer } from '../lib'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; test('synthesized to a layer version', () => { //GIVEN @@ -10,7 +10,7 @@ test('synthesized to a layer version', () => { new KubectlLayer(stack, 'MyLayer'); // THEN - expect(stack).toHaveResource('AWS::Lambda::LayerVersion', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::LayerVersion', { Description: '/opt/kubectl/kubectl and /opt/helm/helm', }); }); diff --git a/packages/@aws-cdk/lambda-layer-node-proxy-agent/.no-packagejson-validator b/packages/@aws-cdk/lambda-layer-node-proxy-agent/.no-packagejson-validator new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/@aws-cdk/lambda-layer-node-proxy-agent/README.md b/packages/@aws-cdk/lambda-layer-node-proxy-agent/README.md index 45187394cb8b2..da6471a4950da 100644 --- a/packages/@aws-cdk/lambda-layer-node-proxy-agent/README.md +++ b/packages/@aws-cdk/lambda-layer-node-proxy-agent/README.md @@ -16,8 +16,11 @@ This module exports a single class called `NodeProxyAgentLayer` which is a `lamb Usage: ```ts -const fn = new lambda.Function(...); -fn.addLayers(new NodeProxyAgentLayer(stack, 'NodeProxyAgentLayer')); +import { NodeProxyAgentLayer } from '@aws-cdk/lambda-layer-node-proxy-agent'; +import * as lambda from '@aws-cdk/aws-lambda'; + +declare const fn: lambda.Function; +fn.addLayers(new NodeProxyAgentLayer(this, 'NodeProxyAgentLayer')); ``` -[`proxy-agent`](https://www.npmjs.com/package/proxy-agent) will be installed under `/opt/nodejs/node_modules`. +[`proxy-agent`](https://www.npmjs.com/package/proxy-agent) will be installed under `/nodejs/node_modules`. diff --git a/packages/@aws-cdk/lambda-layer-node-proxy-agent/layer/Dockerfile b/packages/@aws-cdk/lambda-layer-node-proxy-agent/layer/Dockerfile index 2e3f644258652..0166da75f4651 100644 --- a/packages/@aws-cdk/lambda-layer-node-proxy-agent/layer/Dockerfile +++ b/packages/@aws-cdk/lambda-layer-node-proxy-agent/layer/Dockerfile @@ -1,8 +1,6 @@ # base lambda image FROM public.ecr.aws/lambda/nodejs:latest -ARG PROXY_AGENT_VERSION=5.0.0 - USER root RUN mkdir -p /opt WORKDIR /tmp @@ -19,7 +17,8 @@ RUN yum update -y \ # RUN mkdir -p /opt/nodejs -RUN cd /opt/nodejs && npm install proxy-agent@${PROXY_AGENT_VERSION} +COPY package*.json /opt/nodejs/ +RUN cd /opt/nodejs && npm ci && rm package*.json # # create the bundle diff --git a/packages/@aws-cdk/lambda-layer-node-proxy-agent/layer/build.sh b/packages/@aws-cdk/lambda-layer-node-proxy-agent/layer/build.sh index 6a84896b9d991..d45f9d47a77d7 100755 --- a/packages/@aws-cdk/lambda-layer-node-proxy-agent/layer/build.sh +++ b/packages/@aws-cdk/lambda-layer-node-proxy-agent/layer/build.sh @@ -3,7 +3,7 @@ set -euo pipefail cd $(dirname $0) -echo ">> Building AWS Lambda layer inside a docker image..." +echo ">> Building AWS Lambda layer inside a docker image for Proxy Agent..." TAG='aws-lambda-node-proxy-agent' diff --git a/packages/@aws-cdk/lambda-layer-node-proxy-agent/layer/package-lock.json b/packages/@aws-cdk/lambda-layer-node-proxy-agent/layer/package-lock.json new file mode 100644 index 0000000000000..19220af66d40b --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-node-proxy-agent/layer/package-lock.json @@ -0,0 +1,1137 @@ +{ + "name": "proxy-agent-layer", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "proxy-agent-layer", + "version": "0.0.0", + "license": "ISC", + "devDependencies": { + "proxy-agent": "^5.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bytes": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz", + "integrity": "sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/data-uri-to-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/degenerator": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-3.0.1.tgz", + "integrity": "sha512-LFsIFEeLPlKvAKXu7j3ssIG6RT0TbI7/GhsqrI0DnHASEQjXQ0LUSYcjJteGgRGmZbl1TnMSxpNQIAiJ7Du5TQ==", + "dev": true, + "dependencies": { + "ast-types": "^0.13.2", + "escodegen": "^1.8.1", + "esprima": "^4.0.0", + "vm2": "^3.9.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "node_modules/file-uri-to-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz", + "integrity": "sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/ftp": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", + "integrity": "sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=", + "dev": true, + "dependencies": { + "readable-stream": "1.1.x", + "xregexp": "2.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/get-uri": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-3.0.2.tgz", + "integrity": "sha512-+5s0SJbGoyiJTZZ2JTpFPLMPSch72KEqGOTvQsBqg0RBWvwhWUSYZFAtz3TPW0GXJuLBJPts1E241iHg+VRfhg==", + "dev": true, + "dependencies": { + "@tootallnate/once": "1", + "data-uri-to-buffer": "3", + "debug": "4", + "file-uri-to-path": "2", + "fs-extra": "^8.1.0", + "ftp": "^0.3.10" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", + "dev": true + }, + "node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pac-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-5.0.0.tgz", + "integrity": "sha512-CcFG3ZtnxO8McDigozwE3AqAw15zDvGH+OjXO4kzf7IkEKkQ4gxQ+3sdF50WmhQ4P/bVusXcqNE2S3XrNURwzQ==", + "dev": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4", + "get-uri": "3", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "5", + "pac-resolver": "^5.0.0", + "raw-body": "^2.2.0", + "socks-proxy-agent": "5" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/pac-resolver": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-5.0.0.tgz", + "integrity": "sha512-H+/A6KitiHNNW+bxBKREk2MCGSxljfqRX76NjummWEYIat7ldVXRU3dhRIE3iXZ0nvGBk6smv3nntxKkzRL8NA==", + "dev": true, + "dependencies": { + "degenerator": "^3.0.1", + "ip": "^1.1.5", + "netmask": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-5.0.0.tgz", + "integrity": "sha512-gkH7BkvLVkSfX9Dk27W6TyNOWWZWRilRfk1XxGNWOYJ2TuedAv1yFpCaU9QSBmBe716XOTNpYNOzhysyw8xn7g==", + "dev": true, + "dependencies": { + "agent-base": "^6.0.0", + "debug": "4", + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^5.0.0", + "lru-cache": "^5.1.1", + "pac-proxy-agent": "^5.0.0", + "proxy-from-env": "^1.0.0", + "socks-proxy-agent": "^5.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, + "node_modules/raw-body": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz", + "integrity": "sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==", + "dev": true, + "dependencies": { + "bytes": "3.1.1", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.1.tgz", + "integrity": "sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA==", + "dev": true, + "dependencies": { + "ip": "^1.1.5", + "smart-buffer": "^4.1.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz", + "integrity": "sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ==", + "dev": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "4", + "socks": "^2.3.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vm2": { + "version": "3.9.8", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.8.tgz", + "integrity": "sha512-/1PYg/BwdKzMPo8maOZ0heT7DLI0DAFTm7YQaz/Lim9oIaFZsJs3EdtalvXuBfZwczNwsYhju75NW4d6E+4q+w==", + "dev": true, + "dependencies": { + "acorn": "^8.7.0", + "acorn-walk": "^8.2.0" + }, + "bin": { + "vm2": "bin/vm2" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xregexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + }, + "dependencies": { + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true + }, + "acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "dev": true + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "requires": { + "debug": "4" + } + }, + "ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "requires": { + "tslib": "^2.0.1" + } + }, + "bytes": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz", + "integrity": "sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==", + "dev": true + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "data-uri-to-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", + "dev": true + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "degenerator": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-3.0.1.tgz", + "integrity": "sha512-LFsIFEeLPlKvAKXu7j3ssIG6RT0TbI7/GhsqrI0DnHASEQjXQ0LUSYcjJteGgRGmZbl1TnMSxpNQIAiJ7Du5TQ==", + "dev": true, + "requires": { + "ast-types": "^0.13.2", + "escodegen": "^1.8.1", + "esprima": "^4.0.0", + "vm2": "^3.9.3" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "file-uri-to-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz", + "integrity": "sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==", + "dev": true + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "ftp": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", + "integrity": "sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=", + "dev": true, + "requires": { + "readable-stream": "1.1.x", + "xregexp": "2.0.0" + } + }, + "get-uri": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-3.0.2.tgz", + "integrity": "sha512-+5s0SJbGoyiJTZZ2JTpFPLMPSch72KEqGOTvQsBqg0RBWvwhWUSYZFAtz3TPW0GXJuLBJPts1E241iHg+VRfhg==", + "dev": true, + "requires": { + "@tootallnate/once": "1", + "data-uri-to-buffer": "3", + "debug": "4", + "file-uri-to-path": "2", + "fs-extra": "^8.1.0", + "ftp": "^0.3.10" + } + }, + "graceful-fs": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", + "dev": true + }, + "http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + } + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "pac-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-5.0.0.tgz", + "integrity": "sha512-CcFG3ZtnxO8McDigozwE3AqAw15zDvGH+OjXO4kzf7IkEKkQ4gxQ+3sdF50WmhQ4P/bVusXcqNE2S3XrNURwzQ==", + "dev": true, + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4", + "get-uri": "3", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "5", + "pac-resolver": "^5.0.0", + "raw-body": "^2.2.0", + "socks-proxy-agent": "5" + } + }, + "pac-resolver": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-5.0.0.tgz", + "integrity": "sha512-H+/A6KitiHNNW+bxBKREk2MCGSxljfqRX76NjummWEYIat7ldVXRU3dhRIE3iXZ0nvGBk6smv3nntxKkzRL8NA==", + "dev": true, + "requires": { + "degenerator": "^3.0.1", + "ip": "^1.1.5", + "netmask": "^2.0.1" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-5.0.0.tgz", + "integrity": "sha512-gkH7BkvLVkSfX9Dk27W6TyNOWWZWRilRfk1XxGNWOYJ2TuedAv1yFpCaU9QSBmBe716XOTNpYNOzhysyw8xn7g==", + "dev": true, + "requires": { + "agent-base": "^6.0.0", + "debug": "4", + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^5.0.0", + "lru-cache": "^5.1.1", + "pac-proxy-agent": "^5.0.0", + "proxy-from-env": "^1.0.0", + "socks-proxy-agent": "^5.0.0" + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, + "raw-body": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz", + "integrity": "sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==", + "dev": true, + "requires": { + "bytes": "3.1.1", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true + }, + "socks": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.1.tgz", + "integrity": "sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA==", + "dev": true, + "requires": { + "ip": "^1.1.5", + "smart-buffer": "^4.1.0" + } + }, + "socks-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz", + "integrity": "sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ==", + "dev": true, + "requires": { + "agent-base": "^6.0.2", + "debug": "4", + "socks": "^2.3.3" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true + }, + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "vm2": { + "version": "3.9.8", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.8.tgz", + "integrity": "sha512-/1PYg/BwdKzMPo8maOZ0heT7DLI0DAFTm7YQaz/Lim9oIaFZsJs3EdtalvXuBfZwczNwsYhju75NW4d6E+4q+w==", + "dev": true, + "requires": { + "acorn": "^8.7.0", + "acorn-walk": "^8.2.0" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "xregexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } +} diff --git a/packages/@aws-cdk/lambda-layer-node-proxy-agent/layer/package.json b/packages/@aws-cdk/lambda-layer-node-proxy-agent/layer/package.json new file mode 100644 index 0000000000000..d6e8118c9b4df --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-node-proxy-agent/layer/package.json @@ -0,0 +1,12 @@ +{ + "name": "proxy-agent-layer", + "private": true, + "version": "0.0.0", + "description": "", + "devDependencies": { + "proxy-agent": "^5.0.0" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/packages/@aws-cdk/lambda-layer-node-proxy-agent/package.json b/packages/@aws-cdk/lambda-layer-node-proxy-agent/package.json index d44918c6bd004..8f0dfffef911b 100644 --- a/packages/@aws-cdk/lambda-layer-node-proxy-agent/package.json +++ b/packages/@aws-cdk/lambda-layer-node-proxy-agent/package.json @@ -29,7 +29,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", @@ -66,12 +73,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", - "jest": "^27.4.5" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/aws-lambda": "0.0.0", diff --git a/packages/@aws-cdk/lambda-layer-node-proxy-agent/rosetta/default.ts-fixture b/packages/@aws-cdk/lambda-layer-node-proxy-agent/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..50d86e8a055ce --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-node-proxy-agent/rosetta/default.ts-fixture @@ -0,0 +1,10 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/lambda-layer-node-proxy-agent/test/proxy-agent-layer.test.ts b/packages/@aws-cdk/lambda-layer-node-proxy-agent/test/proxy-agent-layer.test.ts index 14264a8d080dc..72b1b1f85d4a0 100644 --- a/packages/@aws-cdk/lambda-layer-node-proxy-agent/test/proxy-agent-layer.test.ts +++ b/packages/@aws-cdk/lambda-layer-node-proxy-agent/test/proxy-agent-layer.test.ts @@ -1,6 +1,6 @@ +import { Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import { NodeProxyAgentLayer } from '../lib'; -import '@aws-cdk/assert-internal/jest'; test('synthesized to a layer version', () => { //GIVEN @@ -10,7 +10,7 @@ test('synthesized to a layer version', () => { new NodeProxyAgentLayer(stack, 'MyLayer'); // THEN - expect(stack).toHaveResource('AWS::Lambda::LayerVersion', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::LayerVersion', { Description: '/opt/nodejs/node_modules/proxy-agent', }); }); diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index b6477e465a80a..8571bd5cf2576 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -49,9 +49,10 @@ them. You can deploy to the same account and Region, or to a different one, with the same amount of code. The *CDK Pipelines* library takes care of the details. -CDK Pipelines supports multiple *deployment engines* (see below), and comes with -a deployment engine that deploys CDK apps using AWS CodePipeline. To use the -CodePipeline engine, define a `CodePipeline` construct. The following +CDK Pipelines supports multiple *deployment engines* (see +[Using a different deployment engine](#using-a-different-deployment-engine)), +and comes with a deployment engine that deploys CDK apps using AWS CodePipeline. +To use the CodePipeline engine, define a `CodePipeline` construct. The following example creates a CodePipeline that deploys an application from GitHub: ```ts @@ -149,21 +150,9 @@ pipeline will automatically reconfigure itself to deploy those new stages and stacks. (Note that have to *bootstrap* all environments before the above code -will work, see the section **CDK Environment Bootstrapping** below). - -## CDK Versioning - -This library uses prerelease features of the CDK framework, which can be enabled -by adding the following to `cdk.json`: - -```js -{ - // ... - "context": { - "@aws-cdk/core:newStyleStackSynthesis": true - } -} -``` +will work, and switch on "Modern synthesis" if you are using +CDKv1. See the section **CDK Environment Bootstrapping** below for +more information). ## Provisioning the pipeline @@ -225,9 +214,10 @@ const originalPipeline = new pipelines.CdkPipeline(this, 'Pipeline', { ## Definining the pipeline -This section of the documentation describes the AWS CodePipeline engine, which -comes with this library. If you want to use a different deployment engine, read -the section *Using a different deployment engine* below. +This section of the documentation describes the AWS CodePipeline engine, +which comes with this library. If you want to use a different deployment +engine, read the section +[Using a different deployment engine](#using-a-different-deployment-engine)below. ### Synth and sources @@ -348,6 +338,40 @@ const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { You can adapt these examples to your own situation. +#### Migrating from buildspec.yml files + +You may currently have the build instructions for your CodeBuild Projects in a +`buildspec.yml` file in your source repository. In addition to your build +commands, the CodeBuild Project's buildspec also controls some information that +CDK Pipelines manages for you, like artifact identifiers, input artifact +locations, Docker authorization, and exported variables. + +Since there is no way in general for CDK Pipelines to modify the file in your +resource repository, CDK Pipelines configures the BuildSpec directly on the +CodeBuild Project, instead of loading it from the `buildspec.yml` file. +This requires a pipeline self-mutation to update. + +To avoid this, put your build instructions in a separate script, for example +`build.sh`, and call that script from the build `commands` array: + +```ts +declare const source: pipelines.IFileSetProducer; + +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { + input: source, + commands: [ + // Abstract over doing the build + './build.sh', + ], + }) +}); +``` + +Doing so keeps your exact build instructions in sync with your source code in +the source repository where it belongs, and provides a convenient build script +for developers at the same time. + #### CodePipeline Sources In CodePipeline, *Sources* define where the source of your application lives. @@ -402,6 +426,16 @@ const bucket = s3.Bucket.fromBucketName(this, 'Bucket', 'my-bucket'); pipelines.CodePipelineSource.s3(bucket, 'my/source.zip'); ``` +##### ECR + +You can use a Docker image in ECR as the source of the pipeline. The pipeline will be +triggered every time an image is pushed to ECR: + +```ts +const repository = new ecr.Repository(this, 'Repository'); +pipelines.CodePipelineSource.ecr(repository); +``` + #### Additional inputs `ShellStep` allows passing in more than one input: additional @@ -766,6 +800,13 @@ class MyJenkinsStep extends pipelines.Step implements pipelines.ICodePipelineAct private readonly input: pipelines.FileSet, ) { super('MyJenkinsStep'); + + // This is necessary if your step accepts things like environment variables + // that may contain outputs from other steps. It doesn't matter what the + // structure is, as long as it contains the values that may contain outputs. + this.discoverReferencedOutputs({ + env: { /* ... */ } + }); } public produceAction(stage: codepipeline.IStage, options: pipelines.ProduceActionOptions): pipelines.CodePipelineActionFactoryResult { @@ -944,22 +985,30 @@ or future deployments to this environment will fail. If you want to upgrade the bootstrap stack to a newer version, do that by updating it in-place. > This library requires the *modern* bootstrapping stack which has -> been updated specifically to support cross-account continuous delivery. Starting, -> in CDK v2 this new bootstrapping stack will become the default, but for now it is still -> opt-in. +> been updated specifically to support cross-account continuous delivery. > -> The commands below assume you are running `cdk bootstrap` in a directory -> where `cdk.json` contains the `"@aws-cdk/core:newStyleStackSynthesis": true` -> setting in its context, which will switch to the new bootstrapping stack -> automatically. +> If you are using CDKv2, you do not need to do anything else. Modern +> bootstrapping and modern stack synthesis (also known as "default stack +> synthesis") is the default. > -> If run from another directory, be sure to run the bootstrap command with -> the environment variable `CDK_NEW_BOOTSTRAP=1` set. +> If you are using CDKv1, you need to opt in to modern bootstrapping and +> modern stack synthesis using a feature flag. Make sure `cdk.json` includes: +> +> ```json +> { +> "context": { +> "@aws-cdk/core:newStyleStackSynthesis": true +> } +> } +> ``` +> +> And be sure to run `cdk bootstrap` in the same directory as the `cdk.json` +> file. To bootstrap an environment for provisioning the pipeline: ```console -$ env CDK_NEW_BOOTSTRAP=1 npx cdk bootstrap \ +$ npx cdk bootstrap \ [--profile admin-profile-1] \ --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess \ aws://111111111111/us-east-1 @@ -969,7 +1018,7 @@ To bootstrap a different environment for deploying CDK applications into using a pipeline in account `111111111111`: ```console -$ env CDK_NEW_BOOTSTRAP=1 npx cdk bootstrap \ +$ npx cdk bootstrap \ [--profile admin-profile-2] \ --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess \ --trust 11111111111 \ @@ -980,7 +1029,7 @@ If you only want to trust an account to do lookups (e.g, when your CDK applicati `Vpc.fromLookup()` call), use the option `--trust-for-lookup`: ```console -$ env CDK_NEW_BOOTSTRAP=1 npx cdk bootstrap \ +$ npx cdk bootstrap \ [--profile admin-profile-2] \ --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess \ --trust-for-lookup 11111111111 \ @@ -1203,6 +1252,17 @@ pipeline.addStage(stage, { **Note**: Manual Approvals notifications only apply when an application has security check enabled. +## Using a different deployment engine + +CDK Pipelines supports multiple *deployment engines*, but this module vends a +construct for only one such engine: AWS CodePipeline. It is also possible to +use CDK Pipelines to build pipelines backed by other deployment engines. + +Here is a list of CDK Libraries that integrate CDK Pipelines with +alternative deployment engines: + +* GitHub Workflows: [`cdk-pipelines-github`](https://github.com/cdklabs/cdk-pipelines-github) + ## Troubleshooting Here are some common errors you may encounter while using this library. diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/manual-approval.ts b/packages/@aws-cdk/pipelines/lib/blueprint/manual-approval.ts index 859c279533fa3..31b93f5b9cf87 100644 --- a/packages/@aws-cdk/pipelines/lib/blueprint/manual-approval.ts +++ b/packages/@aws-cdk/pipelines/lib/blueprint/manual-approval.ts @@ -33,5 +33,7 @@ export class ManualApprovalStep extends Step { super(id); this.comment = props.comment; + + this.discoverReferencedOutputs(props.comment); } } \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/shell-step.ts b/packages/@aws-cdk/pipelines/lib/blueprint/shell-step.ts index 1f03105c78ee9..fa01624e9635a 100644 --- a/packages/@aws-cdk/pipelines/lib/blueprint/shell-step.ts +++ b/packages/@aws-cdk/pipelines/lib/blueprint/shell-step.ts @@ -87,11 +87,11 @@ export interface ShellStepProps { * @default - No primary output */ readonly primaryOutputDirectory?: string; - } /** - * Run shell script commands in the pipeline + * Run shell script commands in the pipeline. This is a generic step designed + * to be deployment engine agnostic. */ export class ShellStep extends Step { /** @@ -151,6 +151,11 @@ export class ShellStep extends Step { this.env = props.env ?? {}; this.envFromCfnOutputs = mapValues(props.envFromCfnOutputs ?? {}, StackOutputReference.fromCfnOutput); + // 'env' is the only thing that can contain outputs + this.discoverReferencedOutputs({ + env: this.env, + }); + // Inputs if (props.input) { const fileSet = props.input.primaryOutput; diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/step.ts b/packages/@aws-cdk/pipelines/lib/blueprint/step.ts index 2bd785a7c7640..c17402f22e850 100644 --- a/packages/@aws-cdk/pipelines/lib/blueprint/step.ts +++ b/packages/@aws-cdk/pipelines/lib/blueprint/step.ts @@ -1,4 +1,5 @@ import { Stack, Token } from '@aws-cdk/core'; +import { StepOutput } from '../helpers-internal/step-output'; import { FileSet, IFileSetProducer } from './file-set'; /** @@ -13,6 +14,10 @@ import { FileSet, IFileSetProducer } from './file-set'; export abstract class Step implements IFileSetProducer { /** * Define a sequence of steps to be executed in order. + * + * If you need more fine-grained step ordering, use the `addStepDependency()` + * API. For example, if you want `secondStep` to occur after `firstStep`, call + * `secondStep.addStepDependency(firstStep)`. */ public static sequence(steps: Step[]): Step[] { for (let i = 1; i < steps.length; i++) { @@ -35,7 +40,7 @@ export abstract class Step implements IFileSetProducer { private _primaryOutput?: FileSet; - private _dependencies: Step[] = []; + private _dependencies = new Set(); constructor( /** Identifier for this step */ @@ -50,7 +55,10 @@ export abstract class Step implements IFileSetProducer { * Return the steps this step depends on, based on the FileSets it requires */ public get dependencies(): Step[] { - return this.dependencyFileSets.map(f => f.producer).concat(this._dependencies); + return Array.from(new Set([ + ...this.dependencyFileSets.map(f => f.producer), + ...this._dependencies, + ])); } /** @@ -75,7 +83,7 @@ export abstract class Step implements IFileSetProducer { * Add a dependency on another step. */ public addStepDependency(step: Step) { - this._dependencies.push(step); + this._dependencies.add(step); } /** @@ -93,6 +101,21 @@ export abstract class Step implements IFileSetProducer { protected configurePrimaryOutput(fs: FileSet) { this._primaryOutput = fs; } + + /** + * Crawl the given structure for references to StepOutputs and add dependencies on all steps found + * + * Should be called by subclasses based on what the user passes in as + * construction properties. The format of the structure passed in here does + * not have to correspond exactly to what gets rendered into the engine, it + * just needs to contain the same amount of data. + */ + protected discoverReferencedOutputs(structure: any) { + for (const output of StepOutput.findAll(structure)) { + this._dependencies.add(output.step); + StepOutput.recordProducer(output); + } + } } /** @@ -124,5 +147,4 @@ export interface StackSteps { * @default - no additional steps */ readonly post?: Step[]; - } \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codebuild-step.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codebuild-step.ts index f835e261aba3d..c50d715e2cbe9 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/codebuild-step.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codebuild-step.ts @@ -1,8 +1,10 @@ -import { Duration } from '@aws-cdk/core'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; +import { Duration } from '@aws-cdk/core'; import { ShellStep, ShellStepProps } from '../blueprint'; +import { mergeBuildSpecs } from './private/buildspecs'; +import { makeCodePipelineOutput } from './private/outputs'; /** * Construction props for a CodeBuildStep @@ -96,6 +98,17 @@ export interface CodeBuildStepProps extends ShellStepProps { /** * Run a script as a CodeBuild Project + * + * The BuildSpec must be available inline--it cannot reference a file + * on disk. If your current build instructions are in a file like + * `buildspec.yml` in your repository, extract them to a script + * (say, `build.sh`) and invoke that script as part of the build: + * + * ```ts + * new pipelines.CodeBuildStep('Synth', { + * commands: ['./build.sh'], + * }); + * ``` */ export class CodeBuildStep extends ShellStep { /** @@ -105,13 +118,6 @@ export class CodeBuildStep extends ShellStep { */ public readonly projectName?: string; - /** - * Additional configuration that can only be configured via BuildSpec - * - * @default - No value specified at construction time, use defaults - */ - public readonly partialBuildSpec?: codebuild.BuildSpec; - /** * The VPC where to execute the SimpleSynth. * @@ -164,13 +170,16 @@ export class CodeBuildStep extends ShellStep { readonly timeout?: Duration; private _project?: codebuild.IProject; + private _partialBuildSpec?: codebuild.BuildSpec; + private readonly exportedVariables = new Set(); + private exportedVarsRendered = false; constructor(id: string, props: CodeBuildStepProps) { super(id, props); this.projectName = props.projectName; this.buildEnvironment = props.buildEnvironment; - this.partialBuildSpec = props.partialBuildSpec; + this._partialBuildSpec = props.partialBuildSpec; this.vpc = props.vpc; this.subnetSelection = props.subnetSelection; this.role = props.role; @@ -198,6 +207,44 @@ export class CodeBuildStep extends ShellStep { return this.project.grantPrincipal; } + /** + * Additional configuration that can only be configured via BuildSpec + * + * Contains exported variables + * + * @default - Contains the exported variables + */ + public get partialBuildSpec(): codebuild.BuildSpec | undefined { + this.exportedVarsRendered = true; + + const varsBuildSpec = this.exportedVariables.size > 0 ? codebuild.BuildSpec.fromObject({ + version: '0.2', + env: { + 'exported-variables': Array.from(this.exportedVariables), + }, + }) : undefined; + + return mergeBuildSpecs(varsBuildSpec, this._partialBuildSpec); + } + + /** + * Reference a CodePipeline variable defined by the CodeBuildStep. + * + * The variable must be set in the shell of the CodeBuild step when + * it finishes its `post_build` phase. + * + * @param variableName the name of the variable for reference. + */ + public exportedVariable(variableName: string): string { + if (this.exportedVarsRendered && !this.exportedVariables.has(variableName)) { + throw new Error('exportVariable(): Pipeline has already been produced, cannot call this function anymore'); + } + + this.exportedVariables.add(variableName); + + return makeCodePipelineOutput(this, variableName); + } + /** * Set the internal project value * diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts index 62c9fa86d025b..734c2fefa0128 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts @@ -23,6 +23,15 @@ export interface ProduceActionOptions { */ readonly runOrder: number; + /** + * If this step is producing outputs, the variables namespace assigned to it + * + * Pass this on to the Action you are creating. + * + * @default - Step doesn't produce any outputs + */ + readonly variablesNamespace?: string; + /** * Helper object to translate FileSets to CodePipeline Artifacts */ @@ -87,6 +96,8 @@ export interface ICodePipelineActionFactory { export interface CodePipelineActionFactoryResult { /** * How many RunOrders were consumed + * + * If you add 1 action, return the value 1 here. */ readonly runOrdersConsumed: number; diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts index dd40d0d6cf0e7..062df5b4b446f 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts @@ -3,12 +3,14 @@ import * as cp from '@aws-cdk/aws-codepipeline'; import { Artifact } from '@aws-cdk/aws-codepipeline'; import * as cp_actions from '@aws-cdk/aws-codepipeline-actions'; import { Action, CodeCommitTrigger, GitHubTrigger, S3Trigger } from '@aws-cdk/aws-codepipeline-actions'; +import { IRepository } from '@aws-cdk/aws-ecr'; import * as iam from '@aws-cdk/aws-iam'; import { IBucket } from '@aws-cdk/aws-s3'; -import { SecretValue, Token } from '@aws-cdk/core'; +import { Fn, SecretValue, Token } from '@aws-cdk/core'; import { Node } from 'constructs'; import { FileSet, Step } from '../blueprint'; import { CodePipelineActionFactoryResult, ProduceActionOptions, ICodePipelineActionFactory } from './codepipeline-action-factory'; +import { makeCodePipelineOutput } from './private/outputs'; /** * Factory for CodePipeline source steps @@ -56,6 +58,22 @@ export abstract class CodePipelineSource extends Step implements ICodePipelineAc return new S3Source(bucket, objectKey, props); } + /** + * Returns an ECR source. + * + * @param repository The repository that will be watched for changes. + * @param props The options, which include the image tag to be checked for changes. + * + * @example + * declare const repository: ecr.IRepository; + * pipelines.CodePipelineSource.ecr(repository, { + * imageTag: 'latest', + * }); + */ + public static ecr(repository: IRepository, props: ECRSourceOptions = {}): CodePipelineSource { + return new ECRSource(repository, props); + } + /** * Returns a CodeStar connection source. A CodeStar connection allows AWS CodePipeline to * access external resources, such as repositories in GitHub, GitHub Enterprise or @@ -104,12 +122,44 @@ export abstract class CodePipelineSource extends Step implements ICodePipelineAc public produceAction(stage: cp.IStage, options: ProduceActionOptions): CodePipelineActionFactoryResult { const output = options.artifacts.toCodePipeline(this.primaryOutput!); - const action = this.getAction(output, options.actionName, options.runOrder); + + const action = this.getAction(output, options.actionName, options.runOrder, options.variablesNamespace); stage.addAction(action); return { runOrdersConsumed: 1 }; } - protected abstract getAction(output: Artifact, actionName: string, runOrder: number): Action; + protected abstract getAction(output: Artifact, actionName: string, runOrder: number, variablesNamespace?: string): Action; + + /** + * Return an attribute of the current source revision + * + * These values can be passed into the environment variables of pipeline steps, + * so your steps can access information about the source revision. + * + * What attributes are available depends on the type of source. These attributes + * are supported: + * + * - GitHub, CodeCommit, and CodeStar connection + * - `AuthorDate` + * - `BranchName` + * - `CommitId` + * - `CommitMessage` + * - GitHub and CodeCommit + * - `CommitterDate` + * - `RepositoryName` + * - GitHub + * - `CommitUrl` + * - CodeStar Connection + * - `FullRepositoryName` + * - S3 + * - `ETag` + * - `VersionId` + * + * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-variables.html#reference-variables-list + */ + public sourceAttribute(name: string): string { + return makeCodePipelineOutput(this, name); + } } /** @@ -173,7 +223,7 @@ class GitHubSource extends CodePipelineSource { this.configurePrimaryOutput(new FileSet('Source', this)); } - protected getAction(output: Artifact, actionName: string, runOrder: number) { + protected getAction(output: Artifact, actionName: string, runOrder: number, variablesNamespace?: string) { return new cp_actions.GitHubSourceAction({ output, actionName, @@ -183,6 +233,7 @@ class GitHubSource extends CodePipelineSource { repo: this.repo, branch: this.branch, trigger: this.props.trigger, + variablesNamespace, }); } } @@ -216,7 +267,7 @@ class S3Source extends CodePipelineSource { this.configurePrimaryOutput(new FileSet('Source', this)); } - protected getAction(output: Artifact, _actionName: string, runOrder: number) { + protected getAction(output: Artifact, _actionName: string, runOrder: number, variablesNamespace?: string) { return new cp_actions.S3SourceAction({ output, // Bucket names are guaranteed to conform to ActionName restrictions @@ -225,6 +276,47 @@ class S3Source extends CodePipelineSource { bucketKey: this.objectKey, trigger: this.props.trigger, bucket: this.bucket, + variablesNamespace, + }); + } +} + +/** + * Options for ECR sources + */ +export interface ECRSourceOptions { + /** + * The image tag that will be checked for changes. + * + * @default latest + */ + readonly imageTag?: string; + + /** + * The action name used for this source in the CodePipeline + * + * @default - The repository name + */ + readonly actionName?: string; +} + +class ECRSource extends CodePipelineSource { + constructor(readonly repository: IRepository, readonly props: ECRSourceOptions) { + super(Node.of(repository).addr); + + this.configurePrimaryOutput(new FileSet('Source', this)); + } + + protected getAction(output: Artifact, _actionName: string, runOrder: number, variablesNamespace: string) { + // RepositoryName can contain '/' that is not a valid ActionName character, use '_' instead + const formattedRepositoryName = Fn.join('_', Fn.split('/', this.repository.repositoryName)); + return new cp_actions.EcrSourceAction({ + output, + actionName: this.props.actionName ?? formattedRepositoryName, + runOrder, + repository: this.repository, + imageTag: this.props.imageTag, + variablesNamespace, }); } } @@ -284,7 +376,7 @@ class CodeStarConnectionSource extends CodePipelineSource { this.configurePrimaryOutput(new FileSet('Source', this)); } - protected getAction(output: Artifact, actionName: string, runOrder: number) { + protected getAction(output: Artifact, actionName: string, runOrder: number, variablesNamespace?: string) { return new cp_actions.CodeStarConnectionsSourceAction({ output, actionName, @@ -295,6 +387,7 @@ class CodeStarConnectionSource extends CodePipelineSource { branch: this.branch, codeBuildCloneOutput: this.props.codeBuildCloneOutput, triggerOnPush: this.props.triggerOnPush, + variablesNamespace, }); } } @@ -341,7 +434,7 @@ class CodeCommitSource extends CodePipelineSource { this.configurePrimaryOutput(new FileSet('Source', this)); } - protected getAction(output: Artifact, _actionName: string, runOrder: number) { + protected getAction(output: Artifact, _actionName: string, runOrder: number, variablesNamespace?: string) { return new cp_actions.CodeCommitSourceAction({ output, // Guaranteed to be okay as action name @@ -352,6 +445,7 @@ class CodeCommitSource extends CodePipelineSource { repository: this.repository, eventRole: this.props.eventRole, codeBuildCloneOutput: this.props.codeBuildCloneOutput, + variablesNamespace, }); } } diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts index 67dd7c2f35cc4..84562b07aec8b 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts @@ -12,15 +12,17 @@ import { DockerCredential, dockerCredentialsInstallCommands, DockerCredentialUsa import { GraphNodeCollection, isGraph, AGraphNode, PipelineGraph } from '../helpers-internal'; import { PipelineBase } from '../main'; import { AssetSingletonRole } from '../private/asset-singleton-role'; +import { preferredCliVersion } from '../private/cli-version'; import { appOf, assemblyBuilderOf, embeddedAsmPath, obtainScope } from '../private/construct-internals'; import { toPosixPath } from '../private/fs'; import { actionName, stackVariableNamespace } from '../private/identifiers'; import { enumerate, flatten, maybeSuffix, noUndefined } from '../private/javascript'; import { writeTemplateConfiguration } from '../private/template-configuration'; -import { CodeBuildFactory, mergeCodeBuildOptions } from './_codebuild-factory'; import { ArtifactMap } from './artifact-map'; import { CodeBuildStep } from './codebuild-step'; import { CodePipelineActionFactoryResult, ICodePipelineActionFactory } from './codepipeline-action-factory'; +import { CodeBuildFactory, mergeCodeBuildOptions } from './private/codebuild-factory'; +import { namespaceStepOutputs } from './private/outputs'; /** @@ -303,6 +305,7 @@ export class CodePipeline extends PipelineBase { private _cloudAssemblyFileSet?: FileSet; private readonly singlePublisherPerAssetType: boolean; + private readonly cliVersion?: string; constructor(scope: Construct, id: string, private readonly props: CodePipelineProps) { super(scope, id, props); @@ -310,6 +313,7 @@ export class CodePipeline extends PipelineBase { this.selfMutation = props.selfMutation ?? true; this.dockerCredentials = props.dockerCredentials ?? []; this.singlePublisherPerAssetType = !(props.publishAssetsInParallel ?? true); + this.cliVersion = props.cliVersion ?? preferredCliVersion(); } /** @@ -415,9 +419,14 @@ export class CodePipeline extends PipelineBase { const factory = this.actionFromNode(node); const nodeType = this.nodeTypeFromNode(node); + const name = actionName(node, sharedParent); + + const variablesNamespace = node.data?.type === 'step' + ? namespaceStepOutputs(node.data.step, pipelineStage, name) + : undefined; const result = factory.produceAction(pipelineStage, { - actionName: actionName(node, sharedParent), + actionName: name, runOrder, artifacts: this.artifacts, scope: obtainScope(this.pipeline, stageName), @@ -426,6 +435,7 @@ export class CodePipeline extends PipelineBase { // If this step happens to produce a CodeBuild job, set the default options codeBuildDefaults: nodeType ? this.codeBuildDefaultsFor(nodeType) : undefined, beforeSelfMutation, + variablesNamespace, }); if (node.data?.type === 'self-update') { @@ -603,7 +613,7 @@ export class CodePipeline extends PipelineBase { } private selfMutateAction(): ICodePipelineActionFactory { - const installSuffix = this.props.cliVersion ? `@${this.props.cliVersion}` : ''; + const installSuffix = this.cliVersion ? `@${this.cliVersion}` : ''; const pipelineStack = Stack.of(this.pipeline); const pipelineStackIdentifier = pipelineStack.node.path ?? pipelineStack.stackName; @@ -649,7 +659,7 @@ export class CodePipeline extends PipelineBase { } private publishAssetsAction(node: AGraphNode, assets: StackAsset[]): ICodePipelineActionFactory { - const installSuffix = this.props.cliVersion ? `@${this.props.cliVersion}` : ''; + const installSuffix = this.cliVersion ? `@${this.cliVersion}` : ''; const commands = assets.map(asset => { const relativeAssetManifestPath = path.relative(this.myCxAsmRoot, asset.assetManifestPath); diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/private/buildspecs.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/private/buildspecs.ts new file mode 100644 index 0000000000000..f904d7c174629 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/private/buildspecs.ts @@ -0,0 +1,10 @@ +import * as codebuild from '@aws-cdk/aws-codebuild'; + +export function mergeBuildSpecs(a: codebuild.BuildSpec, b?: codebuild.BuildSpec): codebuild.BuildSpec; +export function mergeBuildSpecs(a: codebuild.BuildSpec | undefined, b: codebuild.BuildSpec): codebuild.BuildSpec; +export function mergeBuildSpecs(a?: codebuild.BuildSpec, b?: codebuild.BuildSpec): codebuild.BuildSpec | undefined; +export function mergeBuildSpecs(a?: codebuild.BuildSpec, b?: codebuild.BuildSpec) { + if (!a || !b) { return a ?? b; } + return codebuild.mergeBuildSpecs(a, b); +} + diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/private/codebuild-factory.ts similarity index 93% rename from packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts rename to packages/@aws-cdk/pipelines/lib/codepipeline/private/codebuild-factory.ts index 294a8844b81bb..84a0cf934a4ca 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/private/codebuild-factory.ts @@ -5,17 +5,19 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; -import { IDependable, Stack } from '@aws-cdk/core'; +import { IDependable, Stack, Token } from '@aws-cdk/core'; import { Construct, Node } from 'constructs'; -import { FileSetLocation, ShellStep, StackOutputReference } from '../blueprint'; -import { PipelineQueries } from '../helpers-internal/pipeline-queries'; -import { cloudAssemblyBuildSpecDir, obtainScope } from '../private/construct-internals'; -import { hash, stackVariableNamespace } from '../private/identifiers'; -import { mapValues, mkdict, noEmptyObject, noUndefined, partition } from '../private/javascript'; -import { ArtifactMap } from './artifact-map'; -import { CodeBuildStep } from './codebuild-step'; -import { CodeBuildOptions } from './codepipeline'; -import { ICodePipelineActionFactory, ProduceActionOptions, CodePipelineActionFactoryResult } from './codepipeline-action-factory'; +import { FileSetLocation, ShellStep, StackOutputReference } from '../../blueprint'; +import { PipelineQueries } from '../../helpers-internal/pipeline-queries'; +import { StepOutput } from '../../helpers-internal/step-output'; +import { cloudAssemblyBuildSpecDir, obtainScope } from '../../private/construct-internals'; +import { hash, stackVariableNamespace } from '../../private/identifiers'; +import { mapValues, mkdict, noEmptyObject, noUndefined, partition } from '../../private/javascript'; +import { ArtifactMap } from '../artifact-map'; +import { CodeBuildStep } from '../codebuild-step'; +import { CodeBuildOptions } from '../codepipeline'; +import { ICodePipelineActionFactory, ProduceActionOptions, CodePipelineActionFactoryResult } from '../codepipeline-action-factory'; +import { mergeBuildSpecs } from './buildspecs'; export interface CodeBuildFactoryProps { /** @@ -110,6 +112,11 @@ export interface CodeBuildFactoryProps { * @default false */ readonly isSynth?: boolean; + + /** + * StepOutputs produced by this CodeBuild step + */ + readonly producedStepOutputs?: StepOutput[]; } /** @@ -129,6 +136,7 @@ export class CodeBuildFactory implements ICodePipelineActionFactory { outputs: shellStep.outputs, stepId: shellStep.id, installCommands: shellStep.installCommands, + producedStepOutputs: StepOutput.producedStepOutputs(shellStep), ...additional, }); } @@ -271,8 +279,13 @@ export class CodeBuildFactory implements ICodePipelineActionFactory { projectScope = obtainScope(scope, actionName); } + const safePipelineName = Token.isUnresolved(options.pipeline.pipeline.pipelineName) + ? `${Stack.of(options.pipeline).stackName}/${Node.of(options.pipeline.pipeline).id}` + : options.pipeline.pipeline.pipelineName; + const project = new codebuild.PipelineProject(projectScope, this.constructId, { projectName: this.props.projectName, + description: `Pipeline step ${safePipelineName}/${stage.stageName}/${actionName}`.substring(0, 255), environment, vpc: projectOptions.vpc, subnetSelection: projectOptions.subnetSelection, @@ -309,6 +322,7 @@ export class CodeBuildFactory implements ICodePipelineActionFactory { outputs: outputArtifacts, project, runOrder: options.runOrder, + variablesNamespace: options.variablesNamespace, // Inclusion of the hash here will lead to the pipeline structure for any changes // made the config of the underlying CodeBuild Project. @@ -416,14 +430,6 @@ function mergeBuildEnvironments(a?: codebuild.BuildEnvironment, b?: codebuild.Bu }; } -export function mergeBuildSpecs(a: codebuild.BuildSpec, b?: codebuild.BuildSpec): codebuild.BuildSpec; -export function mergeBuildSpecs(a: codebuild.BuildSpec | undefined, b: codebuild.BuildSpec): codebuild.BuildSpec; -export function mergeBuildSpecs(a?: codebuild.BuildSpec, b?: codebuild.BuildSpec): codebuild.BuildSpec | undefined; -export function mergeBuildSpecs(a?: codebuild.BuildSpec, b?: codebuild.BuildSpec) { - if (!a || !b) { return a ?? b; } - return codebuild.mergeBuildSpecs(a, b); -} - function isDefined(x: A | undefined): x is NonNullable { return x !== undefined; } @@ -447,7 +453,7 @@ function serializeBuildEnvironment(env: codebuild.BuildEnvironment) { * Whether the given string contains a reference to a CodePipeline variable */ function containsPipelineVariable(s: string) { - return !!s.match(/#\{[^}]+\}/); + return !!s.match(/#\{[^}]+\}/) || StepOutput.findAll(s).length > 0; } /** @@ -502,4 +508,4 @@ function filterBuildSpecCommands(buildSpec: codebuild.BuildSpec, osType: ec2.Ope } return [undefined, x]; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/private/outputs.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/private/outputs.ts new file mode 100644 index 0000000000000..f721cb3e5212e --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/private/outputs.ts @@ -0,0 +1,38 @@ +import * as cp from '@aws-cdk/aws-codepipeline'; +import { Step } from '../../blueprint/step'; +import { StepOutput } from '../../helpers-internal'; + +const CODEPIPELINE_ENGINE_NAME = 'codepipeline'; + +export function makeCodePipelineOutput(step: Step, variableName: string) { + return new StepOutput(step, CODEPIPELINE_ENGINE_NAME, variableName).toString(); +} + +/** + * If the step is producing outputs, determine a variableNamespace for it, and configure that on the outputs + */ +export function namespaceStepOutputs(step: Step, stage: cp.IStage, name: string): string | undefined { + let ret: string | undefined; + for (const output of StepOutput.producedStepOutputs(step)) { + ret = namespaceName(stage, name); + if (output.engineName !== CODEPIPELINE_ENGINE_NAME) { + throw new Error(`Found unrecognized output type: ${output.engineName}`); + } + + if (typeof output.engineSpecificInformation !== 'string') { + throw new Error(`CodePipeline requires that 'engineSpecificInformation' is a string, got: ${JSON.stringify(output.engineSpecificInformation)}`); + } + output.defineResolution(`#{${ret}.${output.engineSpecificInformation}}`); + } + return ret; +} + +/** + * Generate a variable namespace from stage and action names + * + * Variable namespaces cannot have '.', but they can have '@'. Other than that, + * action names are more limited so they translate easily. + */ +export function namespaceName(stage: cp.IStage, name: string) { + return `${stage.stageName}/${name}`.replace(/[^a-zA-Z0-9@_-]/g, '@'); +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/docker-credentials.ts b/packages/@aws-cdk/pipelines/lib/docker-credentials.ts index 77b7d2c1b4381..05144d4957771 100644 --- a/packages/@aws-cdk/pipelines/lib/docker-credentials.ts +++ b/packages/@aws-cdk/pipelines/lib/docker-credentials.ts @@ -10,10 +10,10 @@ import { Fn } from '@aws-cdk/core'; export abstract class DockerCredential { /** * Creates a DockerCredential for DockerHub. - * Convenience method for `fromCustomRegistry('index.docker.io', opts)`. + * Convenience method for `customRegistry('https://index.docker.io/v1/', opts)`. */ public static dockerHub(secret: secretsmanager.ISecret, opts: ExternalDockerCredentialOptions = {}): DockerCredential { - return new ExternalDockerCredential('index.docker.io', secret, opts); + return new ExternalDockerCredential('https://index.docker.io/v1/', secret, opts); } /** diff --git a/packages/@aws-cdk/pipelines/lib/helpers-internal/graph.ts b/packages/@aws-cdk/pipelines/lib/helpers-internal/graph.ts index 6b1c2d85ee701..798cc207f0aeb 100644 --- a/packages/@aws-cdk/pipelines/lib/helpers-internal/graph.ts +++ b/packages/@aws-cdk/pipelines/lib/helpers-internal/graph.ts @@ -35,7 +35,7 @@ export class GraphNode { */ public get allDeps(): GraphNode[] { const fromParent = this.parentGraph?.allDeps ?? []; - return [...this.dependencies, ...fromParent]; + return Array.from(new Set([...this.dependencies, ...fromParent])); } public dependOn(...dependencies: Array | undefined>) { @@ -382,4 +382,4 @@ function projectDependencies(dependencies: Map, Set export function isGraph(x: GraphNode): x is Graph { return x instanceof Graph; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/pipelines/lib/helpers-internal/index.ts b/packages/@aws-cdk/pipelines/lib/helpers-internal/index.ts index 6709f7c84488f..8f081e10d88d8 100644 --- a/packages/@aws-cdk/pipelines/lib/helpers-internal/index.ts +++ b/packages/@aws-cdk/pipelines/lib/helpers-internal/index.ts @@ -1,2 +1,3 @@ export * from './pipeline-graph'; -export * from './graph'; \ No newline at end of file +export * from './graph'; +export * from './step-output'; \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/helpers-internal/step-output.ts b/packages/@aws-cdk/pipelines/lib/helpers-internal/step-output.ts new file mode 100644 index 0000000000000..eb71485184f68 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/helpers-internal/step-output.ts @@ -0,0 +1,160 @@ +import { IResolvable, IResolveContext, Token, Tokenization } from '@aws-cdk/core'; +import { Step } from '../blueprint/step'; + + +const STEP_OUTPUT_SYM = Symbol.for('@aws-cdk/pipelines.StepOutput'); + +const PRODUCED_OUTPUTS_SYM = Symbol.for('@aws-cdk/pipelines.outputs'); + + +/** + * A symbolic reference to a value produced by another step + * + * Generating and consuming outputs is engine-specific. Many engines will be + * able to support a feature like "outputs", but it's not guaranteed that + * all of them will. + * + * Outputs can only be generated by engine-specific steps (CodeBuildStep instead + * of ShellStep, etc), but can (currently) be consumed anywhere(*). When + * an engine-specific step generates an Output, it should put a well-known + * string and arbitrary data that is useful to the engine into the engine-specific + * fields on the StepOutput. + * + * The graph blueprint will take care of dependencies and ordering, the engine + * is responsible interpreting and rendering StepOutputs. The engine should call + * `defineResolution()` on all outputs. + * + * StepOutputs currently purposely aren't part of the public API because users + * shouldn't see the innards poking out. So, instead of keeping state on `Step`, + * we keep side-state here in a WeakMap which can be accessed via static members + * on `StepOutput`. + * + * (*) If we need to restrict this, we add the checking and erroring in the engine. + */ +export class StepOutput implements IResolvable { + /** + * Return true if the given IResolvable is a StepOutput + */ + public static isStepOutput(resolvable: IResolvable): resolvable is StepOutput { + return !!(resolvable as any)[STEP_OUTPUT_SYM]; + } + + /** + * Find all StepOutputs referenced in the given structure + */ + public static findAll(structure: any): StepOutput[] { + return findAllStepOutputs(structure); + } + + /** + * Return the produced outputs for the given step + */ + public static producedStepOutputs(step: Step): StepOutput[] { + return (step as any)[PRODUCED_OUTPUTS_SYM] ?? []; + } + + /** + * Add produced outputs for the given step + */ + public static recordProducer(...outputs: StepOutput[]) { + for (const output of outputs) { + const step = output.step; + let list = (step as any)[PRODUCED_OUTPUTS_SYM]; + if (!list) { + list = []; + (step as any)[PRODUCED_OUTPUTS_SYM] = list; + } + list.push(...outputs); + } + } + + /** + * The step that produces this output + */ + public readonly step: Step; + + /** + * Name of the engine for which this output is intended + */ + public readonly engineName: string; + + /** + * Additional data on the output, to be interpreted by the engine + */ + public readonly engineSpecificInformation: any; + + public readonly creationStack: string[] = []; + private resolution: any = undefined; + + constructor(step: Step, engineName: string, engineSpecificInformation: any) { + this.step = step; + this.engineName = engineName; + this.engineSpecificInformation = engineSpecificInformation; + Object.defineProperty(this, STEP_OUTPUT_SYM, { value: true }); + } + + /** + * Define the resolved value for this StepOutput. + * + * Should be called by the engine. + */ + public defineResolution(value: any) { + this.resolution = value; + } + + public resolve(_context: IResolveContext) { + if (this.resolution === undefined) { + throw new Error(`Output for step ${this.step} not configured. Either the step is not in the pipeline, or this engine does not support Outputs for this step.`); + } + return this.resolution; + } + + public toString(): string { + return Token.asString(this); + } +} + +function findAllStepOutputs(structure: any): StepOutput[] { + const ret = new Set(); + recurse(structure); + return Array.from(ret); + + function checkToken(x?: IResolvable) { + if (x && StepOutput.isStepOutput(x)) { + ret.add(x); + return true; + } + + // Return false if it wasn't a Token in the first place (in which case we recurse) + return x !== undefined; + } + + function recurse(x: any): void { + if (!x) { return; } + + if (Tokenization.isResolvable(x)) { + checkToken(x); + return; + } + if (Array.isArray(x)) { + if (!checkToken(Tokenization.reverseList(x))) { + x.forEach(recurse); + } + return; + } + if (typeof x === 'number') { + checkToken(Tokenization.reverseNumber(x)); + return; + } + if (typeof x === 'string') { + Tokenization.reverseString(x).tokens.forEach(checkToken); + return; + } + if (typeof x === 'object') { + for (const [k, v] of Object.entries(x)) { + recurse(k); + recurse(v); + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts b/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts index ef0d1d209619b..e7490778420b5 100644 --- a/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts @@ -9,6 +9,7 @@ import { AssetType } from '../blueprint/asset-type'; import { dockerCredentialsInstallCommands, DockerCredential, DockerCredentialUsage } from '../docker-credentials'; import { ApplicationSecurityCheck } from '../private/application-security-check'; import { AssetSingletonRole } from '../private/asset-singleton-role'; +import { preferredCliVersion } from '../private/cli-version'; import { appOf, assemblyBuilderOf } from '../private/construct-internals'; import { DeployCdkStackAction, PublishAssetsAction, UpdatePipelineAction } from './actions'; import { AddStageOptions, AssetPublishingCommand, BaseStageOptions, CdkStage, StackOutput } from './stage'; @@ -219,9 +220,11 @@ export class CdkPipeline extends CoreConstruct { private readonly _cloudAssemblyArtifact: codepipeline.Artifact; private readonly _dockerCredentials: DockerCredential[]; private _applicationSecurityCheck?: ApplicationSecurityCheck; + private readonly cliVersion?: string; constructor(scope: Construct, id: string, props: CdkPipelineProps) { super(scope, id); + this.cliVersion = props.cdkCliVersion ?? preferredCliVersion(); if (!App.isApp(this.node.root)) { throw new Error('CdkPipeline must be created under an App'); @@ -287,7 +290,7 @@ export class CdkPipeline extends CoreConstruct { actions: [new UpdatePipelineAction(this, 'UpdatePipeline', { cloudAssemblyInput: this._cloudAssemblyArtifact, pipelineStackHierarchicalId: pipelineStack.node.path, - cdkCliVersion: props.cdkCliVersion, + cdkCliVersion: this.cliVersion, projectName: maybeSuffix(props.pipelineName, '-selfupdate'), privileged: props.supportDockerAssets, dockerCredentials: this._dockerCredentials, @@ -298,7 +301,7 @@ export class CdkPipeline extends CoreConstruct { this._assets = new AssetPublishing(this, 'Assets', { cloudAssemblyInput: this._cloudAssemblyArtifact, - cdkCliVersion: props.cdkCliVersion, + cdkCliVersion: this.cliVersion, pipeline: this._pipeline, projectName: maybeSuffix(props.pipelineName, '-publish'), vpc: props.vpc, diff --git a/packages/@aws-cdk/pipelines/lib/private/cli-version.ts b/packages/@aws-cdk/pipelines/lib/private/cli-version.ts new file mode 100644 index 0000000000000..07ef8fa384fb3 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/private/cli-version.ts @@ -0,0 +1,41 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Return the preferred CLI version for the current CDK Library version + * + * This is necessary to prevent cxapi version incompatibility between the two + * CDK major versions. Since changes currently go into v1 before they go into + * v2, a cxapi change can be released in v1 while the v2 CLI doesn't support it + * yet. + * + * In those cases, simply installing the "latest" CLI (2) is not good enough + * because it won't be able to read the Cloud Assembly of the v1 app. + * + * Find this version by finding the containing `package.json` and reading + * `preferredCdkCliVersion` from it. + */ +export function preferredCliVersion(): string | undefined { + const pjLocation = findUp('package.json', __dirname); + if (!pjLocation) { + return undefined; + } + const pj = JSON.parse(fs.readFileSync(pjLocation, { encoding: 'utf-8' })); + return pj.preferredCdkCliVersion ? `${pj.preferredCdkCliVersion}` : undefined; +} + +export function findUp(name: string, directory: string): string | undefined { + const absoluteDirectory = path.resolve(directory); + + const file = path.join(directory, name); + if (fs.existsSync(file)) { + return file; + } + + const { root } = path.parse(absoluteDirectory); + if (absoluteDirectory == root) { + return undefined; + } + + return findUp(name, path.dirname(absoluteDirectory)); +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/package.json b/packages/@aws-cdk/pipelines/package.json index 350b0668c0fdd..53fd337d2df98 100644 --- a/packages/@aws-cdk/pipelines/package.json +++ b/packages/@aws-cdk/pipelines/package.json @@ -48,7 +48,7 @@ "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "aws-sdk": "^2.848.0" }, "peerDependencies": { @@ -156,5 +156,6 @@ "homepage": "https://github.com/aws/aws-cdk", "publishConfig": { "tag": "latest" - } + }, + "preferredCdkCliVersion": "1" } diff --git a/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/dependencies.test.ts b/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/dependencies.test.ts index f577ffae4f80c..9610472554301 100644 --- a/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/dependencies.test.ts +++ b/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/dependencies.test.ts @@ -50,3 +50,22 @@ describe('with nested graphs', () => { ]); }); }); + +test('duplicate dependencies are ignored', () => { + mkGraph('G', G => { + const A = G.graph('A', [], GA => { + GA.node('aa'); + }); + + // parent graph depnds on A also + const B = G.graph('B', [A], GB => { + // duplicate dependency on A + GB.graph('BB', [A], GBB => { + GBB.node('bbb'); + }); + GB.node('bb'); + }); + + expect(nodeNames(B.tryGetChild('BB')!.allDeps)).toStrictEqual(['A']); + }); +}); diff --git a/packages/@aws-cdk/pipelines/test/codepipeline/codebuild-step.test.ts b/packages/@aws-cdk/pipelines/test/codepipeline/codebuild-step.test.ts index c905c3cbc4cfa..5b9538a5a4d00 100644 --- a/packages/@aws-cdk/pipelines/test/codepipeline/codebuild-step.test.ts +++ b/packages/@aws-cdk/pipelines/test/codepipeline/codebuild-step.test.ts @@ -43,6 +43,23 @@ test('additionalinputs creates the right commands', () => { }); }); +test('CodeBuild projects have a description', () => { + new cdkp.CodePipeline(pipelineStack, 'Pipeline', { + synth: new cdkp.CodeBuildStep('Synth', { + commands: ['/bin/true'], + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + }), + }); + + // THEN + Template.fromStack(pipelineStack).hasResourceProperties( + 'AWS::CodeBuild::Project', + { + Description: 'Pipeline step PipelineStack/Pipeline/Build/Synth', + }, + ); +}); + test('long duration steps are supported', () => { // WHEN new cdkp.CodePipeline(pipelineStack, 'Pipeline', { @@ -124,4 +141,70 @@ test('envFromOutputs works even with very long stage and stack names', () => { }); // THEN - did not throw an error about identifier lengths +}); + +test('exportedVariables', () => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + // GIVEN + const producer = new cdkp.CodeBuildStep('Produce', { + commands: ['export MY_VAR=hello'], + }); + + const consumer = new cdkp.CodeBuildStep('Consume', { + env: { + THE_VAR: producer.exportedVariable('MY_VAR'), + }, + commands: [ + 'echo "The variable was: $THE_VAR"', + ], + }); + + // WHEN + pipeline.addWave('MyWave', { + post: [consumer, producer], + }); + + // THEN + const template = Template.fromStack(pipelineStack); + template.hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: [ + { Name: 'Source' }, + { Name: 'Build' }, + { Name: 'UpdatePipeline' }, + { + Name: 'MyWave', + Actions: [ + Match.objectLike({ + Name: 'Produce', + Namespace: 'MyWave@Produce', + RunOrder: 1, + }), + Match.objectLike({ + Name: 'Consume', + RunOrder: 2, + Configuration: Match.objectLike({ + EnvironmentVariables: Match.serializedJson(Match.arrayWith([ + { + name: 'THE_VAR', + type: 'PLAINTEXT', + value: '#{MyWave@Produce.MY_VAR}', + }, + ])), + }), + }), + ], + }, + ], + }); + + template.hasResourceProperties('AWS::CodeBuild::Project', { + Source: { + BuildSpec: Match.serializedJson(Match.objectLike({ + env: { + 'exported-variables': ['MY_VAR'], + }, + })), + }, + }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts b/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts index 46fb468c37623..93284d031d11c 100644 --- a/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts +++ b/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts @@ -1,6 +1,7 @@ import { Capture, Match, Template } from '@aws-cdk/assertions'; import * as ccommit from '@aws-cdk/aws-codecommit'; import { CodeCommitTrigger, GitHubTrigger } from '@aws-cdk/aws-codepipeline-actions'; +import * as ecr from '@aws-cdk/aws-ecr'; import { AnyPrincipal, Role } from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; import { SecretValue, Stack, Token } from '@aws-cdk/core'; @@ -75,9 +76,9 @@ test('CodeCommit source honors all valid properties', () => { }); test('S3 source handles tokenized names correctly', () => { - const buckit = new s3.Bucket(pipelineStack, 'Buckit'); + const bucket = new s3.Bucket(pipelineStack, 'Bucket'); new ModernTestGitHubNpmPipeline(pipelineStack, 'Pipeline', { - input: cdkp.CodePipelineSource.s3(buckit, 'thefile.zip'), + input: cdkp.CodePipelineSource.s3(bucket, 'thefile.zip'), }); Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { @@ -96,6 +97,40 @@ test('S3 source handles tokenized names correctly', () => { }); }); +test('ECR source handles tokenized and namespaced names correctly', () => { + const repository = new ecr.Repository(pipelineStack, 'Repository', { repositoryName: 'namespace/repo' }); + new ModernTestGitHubNpmPipeline(pipelineStack, 'Pipeline', { + input: cdkp.CodePipelineSource.ecr(repository), + }); + + const template = Template.fromStack(pipelineStack); + template.hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ + Name: 'Source', + Actions: [ + Match.objectLike({ + Configuration: Match.objectLike({ + RepositoryName: { Ref: Match.anyValue() }, + }), + Name: Match.objectLike({ + 'Fn::Join': [ + '_', + { + 'Fn::Split': [ + '/', + { + Ref: Match.anyValue(), + }, + ], + }, + ], + }), + }), + ], + }]), + }); +}); + test('GitHub source honors all valid properties', () => { new ModernTestGitHubNpmPipeline(pipelineStack, 'Pipeline', { input: cdkp.CodePipelineSource.gitHub('owner/repo', 'main', { @@ -178,4 +213,45 @@ test('artifact names are never longer than 128 characters', () => { }); expect(artifactId.asString().length).toBeLessThanOrEqual(128); +}); + +test('can use source attributes in pipeline', () => { + const gitHub = cdkp.CodePipelineSource.gitHub('owner/my-repo', 'main'); + + // WHEN + new ModernTestGitHubNpmPipeline(pipelineStack, 'Pipeline', { + input: gitHub, + synth: new cdkp.ShellStep('Synth', { + env: { + GITHUB_URL: gitHub.sourceAttribute('CommitUrl'), + }, + commands: [ + 'echo "Click here: $GITHUB_URL"', + ], + }), + selfMutation: false, + }); + + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: [ + { Name: 'Source' }, + { + Name: 'Build', + Actions: [ + { + Name: 'Synth', + Configuration: Match.objectLike({ + EnvironmentVariables: Match.serializedJson([ + { + name: 'GITHUB_URL', + type: 'PLAINTEXT', + value: '#{Source@owner_my-repo.CommitUrl}', + }, + ]), + }), + }, + ], + }, + ], + }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/compliance/assets.test.ts b/packages/@aws-cdk/pipelines/test/compliance/assets.test.ts index e4bb58afc47f5..2828d494c1c3b 100644 --- a/packages/@aws-cdk/pipelines/test/compliance/assets.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/assets.test.ts @@ -599,7 +599,7 @@ behavior('can supply pre-install scripts to asset upload', (suite) => { BuildSpec: Match.serializedJson(Match.objectLike({ phases: { install: { - commands: ['npm config set registry https://registry.com', 'npm install -g cdk-assets'], + commands: ['npm config set registry https://registry.com', 'npm install -g cdk-assets@1'], }, }, })), diff --git a/packages/@aws-cdk/pipelines/test/compliance/self-mutation.test.ts b/packages/@aws-cdk/pipelines/test/compliance/self-mutation.test.ts index f672898107c30..ca552d56f1332 100644 --- a/packages/@aws-cdk/pipelines/test/compliance/self-mutation.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/self-mutation.test.ts @@ -52,7 +52,7 @@ behavior('CodePipeline has self-mutation stage', (suite) => { BuildSpec: Match.serializedJson(Match.objectLike({ phases: { install: { - commands: ['npm install -g aws-cdk'], + commands: ['npm install -g aws-cdk@1'], }, build: { commands: Match.arrayWith(['cdk -a . deploy PipelineStack --require-approval=never --verbose']), @@ -288,7 +288,7 @@ behavior('self-mutation stage can be customized with BuildSpec', (suite) => { BuildSpec: Match.serializedJson(Match.objectLike({ phases: { install: { - commands: ['npm config set registry example.com', 'npm install -g aws-cdk'], + commands: ['npm config set registry example.com', 'npm install -g aws-cdk@1'], }, build: { commands: Match.arrayWith(['cdk -a . deploy PipelineStack --require-approval=never --verbose']), diff --git a/packages/@aws-cdk/pipelines/test/docker-credentials.test.ts b/packages/@aws-cdk/pipelines/test/docker-credentials.test.ts index a2b5fc2c577dd..902c13a4129b7 100644 --- a/packages/@aws-cdk/pipelines/test/docker-credentials.test.ts +++ b/packages/@aws-cdk/pipelines/test/docker-credentials.test.ts @@ -29,7 +29,7 @@ describe('ExternalDockerCredential', () => { test('dockerHub defaults registry domain', () => { const creds = cdkp.DockerCredential.dockerHub(secret); - expect(Object.keys(creds._renderCdkAssetsConfig())).toEqual(['index.docker.io']); + expect(Object.keys(creds._renderCdkAssetsConfig())).toEqual(['https://index.docker.io/v1/']); }); test('minimal example only renders secret', () => { diff --git a/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.expected.json b/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.expected.json index 1180a0c03f971..8aa6f3c8893ca 100644 --- a/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.expected.json +++ b/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.expected.json @@ -544,6 +544,40 @@ }, "PolicyDocument": { "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, { "Action": [ "s3:GetObject*", @@ -633,6 +667,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -824,7 +862,7 @@ "ProjectName": { "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" }, - "EnvironmentVariables": "[{\"name\":\"_PROJECT_CONFIG_HASH\",\"type\":\"PLAINTEXT\",\"value\":\"9eda7f97d24aac861052bb47a41b80eecdd56096bf9a88a27c88d94c463785c8\"}]" + "EnvironmentVariables": "[{\"name\":\"_PROJECT_CONFIG_HASH\",\"type\":\"PLAINTEXT\",\"value\":\"c0779bd925c3a7f19be75a4973c668d10d00ce3552b882c7d2ba3fa3cee6d976\"}]" }, "InputArtifacts": [ { @@ -1242,6 +1280,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -1321,6 +1363,7 @@ "Cache": { "Type": "NO_CACHE" }, + "Description": "Pipeline step PipelineStack/Pipeline/Build/Synth", "EncryptionKey": "alias/aws/s3", "VpcConfig": { "SecurityGroupIds": [ @@ -1902,12 +1945,13 @@ ] }, "Source": { - "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g aws-cdk\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk -a . deploy PipelineStack --require-approval=never --verbose\"\n ]\n }\n }\n}", + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g aws-cdk@1\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk -a . deploy PipelineStack --require-approval=never --verbose\"\n ]\n }\n }\n}", "Type": "CODEPIPELINE" }, "Cache": { "Type": "NO_CACHE" }, + "Description": "Pipeline step PipelineStack/Pipeline/UpdatePipeline/SelfMutate", "EncryptionKey": "alias/aws/s3", "VpcConfig": { "SecurityGroupIds": [ @@ -2244,12 +2288,13 @@ ] }, "Source": { - "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g cdk-assets\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk-assets --path \\\"assembly-PipelineStack-Beta/PipelineStackBetaStack1E6541489.assets.json\\\" --verbose publish \\\"8289faf53c7da377bb2b90615999171adef5e1d8f6b88810e5fef75e6ca09ba5:current_account-current_region\\\"\"\n ]\n }\n }\n}", + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g cdk-assets@1\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk-assets --path \\\"assembly-PipelineStack-Beta/PipelineStackBetaStack1E6541489.assets.json\\\" --verbose publish \\\"8289faf53c7da377bb2b90615999171adef5e1d8f6b88810e5fef75e6ca09ba5:current_account-current_region\\\"\"\n ]\n }\n }\n}", "Type": "CODEPIPELINE" }, "Cache": { "Type": "NO_CACHE" }, + "Description": "Pipeline step PipelineStack/Pipeline/Assets/FileAsset1", "EncryptionKey": "alias/aws/s3", "VpcConfig": { "SecurityGroupIds": [ @@ -2345,12 +2390,13 @@ ] }, "Source": { - "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g cdk-assets\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk-assets --path \\\"assembly-PipelineStack-Beta/PipelineStackBetaStack1E6541489.assets.json\\\" --verbose publish \\\"ac76997971c3f6ddf37120660003f1ced72b4fc58c498dfd99c78fa77e721e0e:current_account-current_region\\\"\"\n ]\n }\n }\n}", + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g cdk-assets@1\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk-assets --path \\\"assembly-PipelineStack-Beta/PipelineStackBetaStack1E6541489.assets.json\\\" --verbose publish \\\"ac76997971c3f6ddf37120660003f1ced72b4fc58c498dfd99c78fa77e721e0e:current_account-current_region\\\"\"\n ]\n }\n }\n}", "Type": "CODEPIPELINE" }, "Cache": { "Type": "NO_CACHE" }, + "Description": "Pipeline step PipelineStack/Pipeline/Assets/FileAsset2", "EncryptionKey": "alias/aws/s3", "VpcConfig": { "SecurityGroupIds": [ @@ -2416,4 +2462,4 @@ ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/pipelines/test/integ.newpipeline.expected.json b/packages/@aws-cdk/pipelines/test/integ.newpipeline.expected.json index 13a2fa4b5a954..414a43cc3fd0e 100644 --- a/packages/@aws-cdk/pipelines/test/integ.newpipeline.expected.json +++ b/packages/@aws-cdk/pipelines/test/integ.newpipeline.expected.json @@ -30,6 +30,40 @@ }, "PolicyDocument": { "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, { "Action": [ "s3:GetObject*", @@ -119,6 +153,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -290,7 +328,7 @@ "ProjectName": { "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" }, - "EnvironmentVariables": "[{\"name\":\"_PROJECT_CONFIG_HASH\",\"type\":\"PLAINTEXT\",\"value\":\"9eda7f97d24aac861052bb47a41b80eecdd56096bf9a88a27c88d94c463785c8\"}]" + "EnvironmentVariables": "[{\"name\":\"_PROJECT_CONFIG_HASH\",\"type\":\"PLAINTEXT\",\"value\":\"c0779bd925c3a7f19be75a4973c668d10d00ce3552b882c7d2ba3fa3cee6d976\"}]" }, "InputArtifacts": [ { @@ -1939,6 +1977,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -2002,6 +2044,7 @@ "Cache": { "Type": "NO_CACHE" }, + "Description": "Pipeline step PipelineStack/Pipeline/Build/Synth", "EncryptionKey": "alias/aws/s3" } }, @@ -2295,12 +2338,13 @@ ] }, "Source": { - "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g aws-cdk\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk -a . deploy PipelineStack --require-approval=never --verbose\"\n ]\n }\n }\n}", + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g aws-cdk@1\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk -a . deploy PipelineStack --require-approval=never --verbose\"\n ]\n }\n }\n}", "Type": "CODEPIPELINE" }, "Cache": { "Type": "NO_CACHE" }, + "Description": "Pipeline step PipelineStack/Pipeline/UpdatePipeline/SelfMutate", "EncryptionKey": "alias/aws/s3" } } @@ -2339,4 +2383,4 @@ ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-security.expected.json b/packages/@aws-cdk/pipelines/test/integ.pipeline-security.expected.json index 7f9c7a276e8b6..f9f2ec14d2199 100644 --- a/packages/@aws-cdk/pipelines/test/integ.pipeline-security.expected.json +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-security.expected.json @@ -103,6 +103,40 @@ }, "PolicyDocument": { "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, { "Action": [ "s3:GetObject*", @@ -181,6 +215,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -1276,6 +1314,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets-single-upload.expected.json b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets-single-upload.expected.json index 1e3b8da882e14..26cecb377ee68 100644 --- a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets-single-upload.expected.json +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets-single-upload.expected.json @@ -103,6 +103,40 @@ }, "PolicyDocument": { "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, { "Action": [ "s3:GetObject*", @@ -181,6 +215,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -379,7 +417,8 @@ "Configuration": { "ProjectName": { "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" - } + }, + "EnvironmentVariables": "[{\"name\":\"CDK_CLI_VERSION\",\"type\":\"PLAINTEXT\",\"value\":\"1\"}]" }, "InputArtifacts": [ { @@ -410,7 +449,8 @@ "Configuration": { "ProjectName": { "Ref": "PipelineAssetsFileAsset5D8C5DA6" - } + }, + "EnvironmentVariables": "[{\"name\":\"CDK_CLI_VERSION\",\"type\":\"PLAINTEXT\",\"value\":\"1\"}]" }, "InputArtifacts": [ { @@ -710,6 +750,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -1330,7 +1374,7 @@ ] }, "Source": { - "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g aws-cdk\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk -a . deploy PipelineStack --require-approval=never --verbose\"\n ]\n }\n }\n}", + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g aws-cdk@1\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk -a . deploy PipelineStack --require-approval=never --verbose\"\n ]\n }\n }\n}", "Type": "CODEPIPELINE" }, "Cache": { diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.expected.json b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.expected.json index 86ea5b197c1fe..c757e3097d633 100644 --- a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.expected.json +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.expected.json @@ -103,6 +103,40 @@ }, "PolicyDocument": { "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, { "Action": [ "s3:GetObject*", @@ -181,6 +215,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -379,7 +417,8 @@ "Configuration": { "ProjectName": { "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" - } + }, + "EnvironmentVariables": "[{\"name\":\"CDK_CLI_VERSION\",\"type\":\"PLAINTEXT\",\"value\":\"1\"}]" }, "InputArtifacts": [ { @@ -410,7 +449,8 @@ "Configuration": { "ProjectName": { "Ref": "PipelineAssetsFileAsset185A67CB4" - } + }, + "EnvironmentVariables": "[{\"name\":\"CDK_CLI_VERSION\",\"type\":\"PLAINTEXT\",\"value\":\"1\"}]" }, "InputArtifacts": [ { @@ -436,7 +476,8 @@ "Configuration": { "ProjectName": { "Ref": "PipelineAssetsFileAsset24D2D639B" - } + }, + "EnvironmentVariables": "[{\"name\":\"CDK_CLI_VERSION\",\"type\":\"PLAINTEXT\",\"value\":\"1\"}]" }, "InputArtifacts": [ { @@ -736,6 +777,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -1356,7 +1401,7 @@ ] }, "Source": { - "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g aws-cdk\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk -a . deploy PipelineStack --require-approval=never --verbose\"\n ]\n }\n }\n}", + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g aws-cdk@1\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk -a . deploy PipelineStack --require-approval=never --verbose\"\n ]\n }\n }\n}", "Type": "CODEPIPELINE" }, "Cache": { @@ -1544,7 +1589,7 @@ ] }, "Source": { - "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g cdk-assets\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk-assets --path \\\"assembly-PipelineStack-PreProd/PipelineStackPreProdStack65A0AD1F.assets.json\\\" --verbose publish \\\"8289faf53c7da377bb2b90615999171adef5e1d8f6b88810e5fef75e6ca09ba5:12345678-test-region\\\"\"\n ]\n }\n }\n}", + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g cdk-assets@1\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk-assets --path \\\"assembly-PipelineStack-PreProd/PipelineStackPreProdStack65A0AD1F.assets.json\\\" --verbose publish \\\"8289faf53c7da377bb2b90615999171adef5e1d8f6b88810e5fef75e6ca09ba5:12345678-test-region\\\"\"\n ]\n }\n }\n}", "Type": "CODEPIPELINE" }, "Cache": { @@ -1578,7 +1623,7 @@ ] }, "Source": { - "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g cdk-assets\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk-assets --path \\\"assembly-PipelineStack-PreProd/PipelineStackPreProdStack65A0AD1F.assets.json\\\" --verbose publish \\\"ac76997971c3f6ddf37120660003f1ced72b4fc58c498dfd99c78fa77e721e0e:12345678-test-region\\\"\"\n ]\n }\n }\n}", + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g cdk-assets@1\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk-assets --path \\\"assembly-PipelineStack-PreProd/PipelineStackPreProdStack65A0AD1F.assets.json\\\" --verbose publish \\\"ac76997971c3f6ddf37120660003f1ced72b4fc58c498dfd99c78fa77e721e0e:12345678-test-region\\\"\"\n ]\n }\n }\n}", "Type": "CODEPIPELINE" }, "Cache": { diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-variables.expected.json b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-variables.expected.json new file mode 100644 index 0000000000000..ccc779347f32f --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-variables.expected.json @@ -0,0 +1,1014 @@ +{ + "Resources": { + "PipelineArtifactsBucketAEA9A052": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "aws:kms" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "PipelineArtifactsBucketPolicyF53CCC52": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "PipelineArtifactsBucketAEA9A052" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleB27FAA37": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleDefaultPolicy7BDC1ABB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineBuildSynthCodePipelineActionRole4E7A6C97", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineMyWaveProduceCodePipelineActionRoleE0DCE9D3", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineMyWaveConsumeCodePipelineActionRole7FAA4EFA", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineRoleDefaultPolicy7BDC1ABB", + "Roles": [ + { + "Ref": "PipelineRoleB27FAA37" + } + ] + } + }, + "Pipeline9850B417": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "PipelineRoleB27FAA37", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "ThirdParty", + "Provider": "GitHub", + "Version": "1" + }, + "Configuration": { + "Owner": "cdklabs", + "Repo": "construct-hub-probe", + "Branch": "main", + "OAuthToken": "{{resolve:secretsmanager:github-token:SecretString:::}}", + "PollForSourceChanges": true + }, + "Name": "cdklabs_construct-hub-probe", + "OutputArtifacts": [ + { + "Name": "cdklabs_construct_hub_probe_Source" + } + ], + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + } + }, + "InputArtifacts": [ + { + "Name": "cdklabs_construct_hub_probe_Source" + } + ], + "Name": "Synth", + "OutputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "RoleArn": { + "Fn::GetAtt": [ + "PipelineBuildSynthCodePipelineActionRole4E7A6C97", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "Build" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "PipelineMyWaveProduce884410D6" + } + }, + "InputArtifacts": [ + { + "Name": "cdklabs_construct_hub_probe_Source" + } + ], + "Name": "Produce", + "Namespace": "MyWave@Produce", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineMyWaveProduceCodePipelineActionRoleE0DCE9D3", + "Arn" + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "PipelineMyWaveConsumeC5D5CCD7" + }, + "EnvironmentVariables": "[{\"name\":\"THE_VAR\",\"type\":\"PLAINTEXT\",\"value\":\"#{MyWave@Produce.MY_VAR}\"}]" + }, + "InputArtifacts": [ + { + "Name": "cdklabs_construct_hub_probe_Source" + } + ], + "Name": "Consume", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineMyWaveConsumeCodePipelineActionRole7FAA4EFA", + "Arn" + ] + }, + "RunOrder": 2 + } + ], + "Name": "MyWave" + } + ], + "ArtifactStore": { + "Location": { + "Ref": "PipelineArtifactsBucketAEA9A052" + }, + "Type": "S3" + }, + "RestartExecutionOnUpdate": true + }, + "DependsOn": [ + "PipelineRoleDefaultPolicy7BDC1ABB", + "PipelineRoleB27FAA37" + ] + }, + "PipelineBuildSynthCdkBuildProjectRole231EEA2A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineBuildSynthCdkBuildProjectRoleDefaultPolicyFB6C941C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + }, + "-*" + ] + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineBuildSynthCdkBuildProjectRoleDefaultPolicyFB6C941C", + "Roles": [ + { + "Ref": "PipelineBuildSynthCdkBuildProjectRole231EEA2A" + } + ] + } + }, + "PipelineBuildSynthCdkBuildProject6BEFA8E6": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:5.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "PipelineBuildSynthCdkBuildProjectRole231EEA2A", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"mkdir cdk.out\",\n \"touch cdk.out/dummy\"\n ]\n }\n },\n \"artifacts\": {\n \"base-directory\": \"cdk.out\",\n \"files\": \"**/*\"\n }\n}", + "Type": "CODEPIPELINE" + }, + "Cache": { + "Type": "NO_CACHE" + }, + "Description": "Pipeline step VariablePipelineStack/Pipeline/Build/Synth", + "EncryptionKey": "alias/aws/s3" + } + }, + "PipelineBuildSynthCodePipelineActionRole4E7A6C97": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineBuildSynthCodePipelineActionRoleDefaultPolicy92C90290": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineBuildSynthCdkBuildProject6BEFA8E6", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineBuildSynthCodePipelineActionRoleDefaultPolicy92C90290", + "Roles": [ + { + "Ref": "PipelineBuildSynthCodePipelineActionRole4E7A6C97" + } + ] + } + }, + "PipelineMyWaveProduceRole24E3565D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineMyWaveProduceRoleDefaultPolicy209239D4": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineMyWaveProduce884410D6" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineMyWaveProduce884410D6" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "PipelineMyWaveProduce884410D6" + }, + "-*" + ] + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineMyWaveProduceRoleDefaultPolicy209239D4", + "Roles": [ + { + "Ref": "PipelineMyWaveProduceRole24E3565D" + } + ] + } + }, + "PipelineMyWaveProduce884410D6": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:5.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "PipelineMyWaveProduceRole24E3565D", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"env\": {\n \"exported-variables\": [\n \"MY_VAR\"\n ]\n },\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"export MY_VAR=hello\"\n ]\n }\n }\n}", + "Type": "CODEPIPELINE" + }, + "Cache": { + "Type": "NO_CACHE" + }, + "Description": "Pipeline step VariablePipelineStack/Pipeline/MyWave/Produce", + "EncryptionKey": "alias/aws/s3" + } + }, + "PipelineMyWaveProduceCodePipelineActionRoleE0DCE9D3": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineMyWaveProduceCodePipelineActionRoleDefaultPolicy34DCB79A": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineMyWaveProduce884410D6", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineMyWaveProduceCodePipelineActionRoleDefaultPolicy34DCB79A", + "Roles": [ + { + "Ref": "PipelineMyWaveProduceCodePipelineActionRoleE0DCE9D3" + } + ] + } + }, + "PipelineMyWaveConsumeRole2A96FF33": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineMyWaveConsumeRoleDefaultPolicyC80F0194": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineMyWaveConsumeC5D5CCD7" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineMyWaveConsumeC5D5CCD7" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "PipelineMyWaveConsumeC5D5CCD7" + }, + "-*" + ] + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineMyWaveConsumeRoleDefaultPolicyC80F0194", + "Roles": [ + { + "Ref": "PipelineMyWaveConsumeRole2A96FF33" + } + ] + } + }, + "PipelineMyWaveConsumeC5D5CCD7": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:5.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "PipelineMyWaveConsumeRole2A96FF33", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"echo \\\"The variable was: $THE_VAR\\\"\"\n ]\n }\n }\n}", + "Type": "CODEPIPELINE" + }, + "Cache": { + "Type": "NO_CACHE" + }, + "Description": "Pipeline step VariablePipelineStack/Pipeline/MyWave/Consume", + "EncryptionKey": "alias/aws/s3" + } + }, + "PipelineMyWaveConsumeCodePipelineActionRole7FAA4EFA": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineMyWaveConsumeCodePipelineActionRoleDefaultPolicy3666898A": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineMyWaveConsumeC5D5CCD7", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineMyWaveConsumeCodePipelineActionRoleDefaultPolicy3666898A", + "Roles": [ + { + "Ref": "PipelineMyWaveConsumeCodePipelineActionRole7FAA4EFA" + } + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-variables.ts b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-variables.ts new file mode 100644 index 0000000000000..2a2351375ef62 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-variables.ts @@ -0,0 +1,52 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +/// !cdk-integ VariablePipelineStack pragma:set-context:@aws-cdk/core:newStyleStackSynthesis=true +import { GitHubTrigger } from '@aws-cdk/aws-codepipeline-actions'; +import { App, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as pipelines from '../lib'; + +class PipelineStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { + input: pipelines.CodePipelineSource.gitHub('cdklabs/construct-hub-probe', 'main', { + trigger: GitHubTrigger.POLL, + }), + commands: ['mkdir cdk.out', 'touch cdk.out/dummy'], + }), + selfMutation: false, + }); + + const producer = new pipelines.CodeBuildStep('Produce', { + commands: ['export MY_VAR=hello'], + }); + + const consumer = new pipelines.CodeBuildStep('Consume', { + env: { + THE_VAR: producer.exportedVariable('MY_VAR'), + }, + commands: [ + 'echo "The variable was: $THE_VAR"', + ], + }); + + // WHEN + pipeline.addWave('MyWave', { + post: [consumer, producer], + }); + } +} + +const app = new App({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': '1', + }, +}); + +new PipelineStack(app, 'VariablePipelineStack', { + env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, +}); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline.expected.json b/packages/@aws-cdk/pipelines/test/integ.pipeline.expected.json index 0cdaf3a38943d..ab3e7cbede27a 100644 --- a/packages/@aws-cdk/pipelines/test/integ.pipeline.expected.json +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline.expected.json @@ -103,6 +103,40 @@ }, "PolicyDocument": { "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, { "Action": [ "s3:GetObject*", @@ -181,6 +215,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -369,7 +407,8 @@ "Configuration": { "ProjectName": { "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" - } + }, + "EnvironmentVariables": "[{\"name\":\"CDK_CLI_VERSION\",\"type\":\"PLAINTEXT\",\"value\":\"1\"}]" }, "InputArtifacts": [ { @@ -669,6 +708,10 @@ "s3:List*", "s3:DeleteObject*", "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -1289,7 +1332,7 @@ ] }, "Source": { - "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g aws-cdk\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk -a . deploy PipelineStack --require-approval=never --verbose\"\n ]\n }\n }\n}", + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g aws-cdk@1\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk -a . deploy PipelineStack --require-approval=never --verbose\"\n ]\n }\n }\n}", "Type": "CODEPIPELINE" }, "Cache": { diff --git a/packages/@aws-cdk/pipelines/test/testhelpers/matchers.ts b/packages/@aws-cdk/pipelines/test/testhelpers/matchers.ts index 97a02fc1dc10d..f7ba6458f7449 100644 --- a/packages/@aws-cdk/pipelines/test/testhelpers/matchers.ts +++ b/packages/@aws-cdk/pipelines/test/testhelpers/matchers.ts @@ -26,7 +26,7 @@ class StringLike extends Matcher { public test(actual: any): MatchResult { if (typeof(actual) !== 'string') { - throw new Error(`Expected string but found ${typeof(actual)}`); + throw new Error(`Expected string but found ${typeof(actual)} ${JSON.stringify(actual)}`); } const re = new RegExp(`^${this.pattern.split('*').map(escapeRegex).join('.*')}$`); diff --git a/packages/@aws-cdk/region-info/README.md b/packages/@aws-cdk/region-info/README.md index 0f1186318ee49..b8d75339c3320 100644 --- a/packages/@aws-cdk/region-info/README.md +++ b/packages/@aws-cdk/region-info/README.md @@ -22,10 +22,8 @@ the form of the `RegionInfo` class. This is the preferred way to interact with the regional information database: ```ts -import { RegionInfo } from '@aws-cdk/region-info'; - // Get the information for "eu-west-1": -const region = RegionInfo.get('eu-west-1'); +const region = regionInfo.RegionInfo.get('eu-west-1'); // Access attributes: region.s3StaticWebsiteEndpoint; // s3-website-eu-west-1.amazonaws.com @@ -44,8 +42,6 @@ a list of known fact names, which can then be used with the `RegionInfo` to retrieve a particular value: ```ts -import * as regionInfo from '@aws-cdk/region-info'; - const codeDeployPrincipal = regionInfo.Fact.find('us-east-1', regionInfo.FactName.servicePrincipal('codedeploy.amazonaws.com')); // => codedeploy.us-east-1.amazonaws.com @@ -60,11 +56,13 @@ missing from the library. In such cases, the `Fact.register` method can be used to inject FactName into the database: ```ts -regionInfo.Fact.register({ - region: 'bermuda-triangle-1', - name: regionInfo.FactName.servicePrincipal('s3.amazonaws.com'), - value: 's3-website.bermuda-triangle-1.nowhere.com', -}); +class MyFact implements regionInfo.IFact { + public readonly region = 'bermuda-triangle-1'; + public readonly name = regionInfo.FactName.servicePrincipal('s3.amazonaws.com'); + public readonly value = 's3-website.bermuda-triangle-1.nowhere.com'; +} + +regionInfo.Fact.register(new MyFact()); ``` ## Overriding incorrect information @@ -74,11 +72,13 @@ overridden using the same `Fact.register` method demonstrated above, simply adding an extra boolean argument: ```ts -regionInfo.Fact.register({ - region: 'us-east-1', - name: regionInfo.FactName.servicePrincipal('service.amazonaws.com'), - value: 'the-correct-principal.amazonaws.com', -}, true /* Allow overriding information */); +class MyFact implements regionInfo.IFact { + public readonly region = 'us-east-1'; + public readonly name = regionInfo.FactName.servicePrincipal('service.amazonaws.com'); + public readonly value = 'the-correct-principal.amazonaws.com'; +} + +regionInfo.Fact.register(new MyFact(), true /* Allow overriding information */); ``` If you happen to have stumbled upon incorrect data built into this library, it diff --git a/packages/@aws-cdk/region-info/build-tools/fact-tables.ts b/packages/@aws-cdk/region-info/build-tools/fact-tables.ts index e641f654ba532..c5de5d2e6e785 100644 --- a/packages/@aws-cdk/region-info/build-tools/fact-tables.ts +++ b/packages/@aws-cdk/region-info/build-tools/fact-tables.ts @@ -42,6 +42,7 @@ export const ROUTE_53_BUCKET_WEBSITE_ZONE_IDS: { [region: string]: string } = { 'ap-south-1': 'Z11RGJOFQNVJUP', 'ap-southeast-1': 'Z3O0J2DXBE1FTB', 'ap-southeast-2': 'Z1WCIGYICN2BYD', + 'ap-southeast-3': 'Z01613992JD795ZI93075', 'ca-central-1': 'Z1QDHH18159H29', 'cn-northwest-1': 'Z282HJ1KT0DH03', 'eu-central-1': 'Z21DNDUVLTQW6Q', @@ -102,6 +103,7 @@ export const PARTITION_MAP: { [region: string]: Region } = { }; // https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-access-logs.html#access-logging-bucket-permissions +// https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/enable-access-logs.html#attach-bucket-policy export const ELBV2_ACCOUNTS: { [region: string]: string } = { 'af-south-1': '098369216593', 'ap-east-1': '754344448648', @@ -111,6 +113,7 @@ export const ELBV2_ACCOUNTS: { [region: string]: string } = { 'ap-south-1': '718504428378', 'ap-southeast-1': '114774131450', 'ap-southeast-2': '783225319266', + 'ap-southeast-3': '589379963580', 'ca-central-1': '985666609251', 'cn-north-1': '638102146993', 'cn-northwest-1': '037604701340', diff --git a/packages/@aws-cdk/region-info/lib/aws-entities.ts b/packages/@aws-cdk/region-info/lib/aws-entities.ts index 84749afba24b9..9c14e89605607 100644 --- a/packages/@aws-cdk/region-info/lib/aws-entities.ts +++ b/packages/@aws-cdk/region-info/lib/aws-entities.ts @@ -1,17 +1,14 @@ -// Rule prefix -const RULE_ = 'RULE_'; - /** * After this point, SSM only creates regional principals */ -export const RULE_SSM_PRINCIPALS_ARE_REGIONAL = `${RULE_}SSM_PRINCIPALS_ARE_REGIONAL`; +export const RULE_SSM_PRINCIPALS_ARE_REGIONAL = Symbol('SSM_PRINCIPALS_ARE_REGIONAL'); /** * After this point, S3 website domains look like `s3-website.REGION.s3.amazonaws.com` * * Before this point, S3 website domains look like `s3-website-REGION.s3.amazonaws.com`. */ -export const RULE_S3_WEBSITE_REGIONAL_SUBDOMAIN = `${RULE_}S3_WEBSITE_REGIONAL_SUBDOMAIN`; +export const RULE_S3_WEBSITE_REGIONAL_SUBDOMAIN = Symbol('S3_WEBSITE_REGIONAL_SUBDOMAIN'); /** * List of AWS region, ordered by launch date (oldest to newest) @@ -21,13 +18,13 @@ export const RULE_S3_WEBSITE_REGIONAL_SUBDOMAIN = `${RULE_}S3_WEBSITE_REGIONAL_S * regions are left as-is. * * We mix the list of regions with a list of rules that were introduced over - * time (rules are strings starting with `RULE_`). + * time (rules are symbols). * * Therefore, if we want to know if a rule applies to a certain region, we * only need to check its position in the list and compare it to when a * rule was introduced. */ -export const AWS_REGIONS_AND_RULES = [ +export const AWS_REGIONS_AND_RULES: readonly (string | symbol)[] = [ 'us-east-1', // US East (N. Virginia) 'eu-west-1', // Europe (Ireland) 'us-west-1', // US West (N. California) @@ -68,15 +65,15 @@ export const AWS_REGIONS_AND_RULES = [ * Not in the list ==> no built-in data for that region. */ export const AWS_REGIONS = AWS_REGIONS_AND_RULES - .filter((x) => !x.startsWith(RULE_)) - .sort(); + .filter((x) => typeof x === 'string') + .sort() as readonly string[]; /** * Possibly non-exaustive list of all service names, used to locate service principals. * * Not in the list ==> default service principal mappings. */ -export const AWS_SERVICES = [ +export const AWS_SERVICES: readonly string[] = [ 'application-autoscaling', 'autoscaling', 'codedeploy', @@ -96,10 +93,10 @@ export const AWS_SERVICES = [ * * Unknown region => we have to assume no. */ -export function before(region: string, ruleOrRegion: string) { +export function before(region: string, ruleOrRegion: string | symbol) { const ruleIx = AWS_REGIONS_AND_RULES.indexOf(ruleOrRegion); if (ruleIx === -1) { - throw new Error(`Unknown rule: ${ruleOrRegion}`); + throw new Error(`Unknown rule: ${String(ruleOrRegion)}`); } const regionIx = AWS_REGIONS_AND_RULES.indexOf(region); return regionIx === -1 ? false : regionIx < ruleIx; @@ -108,17 +105,19 @@ export function before(region: string, ruleOrRegion: string) { /** * Return all regions before a given rule was introduced (or region) */ -export function regionsBefore(ruleOrRegion: string): string[] { +export function regionsBefore(ruleOrRegion: string | symbol): string[] { const ruleIx = AWS_REGIONS_AND_RULES.indexOf(ruleOrRegion); if (ruleIx === -1) { - throw new Error(`Unknown rule: ${ruleOrRegion}`); + throw new Error(`Unknown rule: ${String(ruleOrRegion)}`); } - return AWS_REGIONS_AND_RULES.filter((_, i) => i < ruleIx).sort(); + return AWS_REGIONS_AND_RULES.slice(0, ruleIx) + .filter((entry) => typeof entry === 'string') + .sort() as string[]; } -export interface Region { partition: string, domainSuffix: string } +export interface Region { readonly partition: string, readonly domainSuffix: string } -const PARTITION_MAP: { [region: string]: Region } = { +const PARTITION_MAP: {readonly [region: string]: Region } = { 'default': { partition: 'aws', domainSuffix: 'amazonaws.com' }, 'cn-': { partition: 'aws-cn', domainSuffix: 'amazonaws.com.cn' }, 'us-gov-': { partition: 'aws-us-gov', domainSuffix: 'amazonaws.com' }, diff --git a/packages/@aws-cdk/region-info/lib/default.ts b/packages/@aws-cdk/region-info/lib/default.ts index b11aa881f955c..e306bd8c1fc25 100644 --- a/packages/@aws-cdk/region-info/lib/default.ts +++ b/packages/@aws-cdk/region-info/lib/default.ts @@ -23,8 +23,8 @@ export class Default { * @param urlSuffix deprecated and ignored. */ public static servicePrincipal(serviceFqn: string, region: string, urlSuffix: string): string { - const service = extractSimpleName(serviceFqn); - if (!service) { + const serviceName = extractSimpleName(serviceFqn); + if (!serviceName) { // Return "service" if it does not look like any of the following: // - s3 // - s3.amazonaws.com @@ -34,72 +34,86 @@ export class Default { return serviceFqn; } - // Exceptions for Service Principals in us-iso-* - const US_ISO_EXCEPTIONS = new Set([ - 'cloudhsm', - 'config', - 'states', - 'workspaces', - ]); - - // Account for idiosyncratic Service Principals in `us-iso-*` regions - if (region.startsWith('us-iso-') && US_ISO_EXCEPTIONS.has(service)) { - switch (service) { - // Services with universal principal - case ('states'): - return `${service}.amazonaws.com`; - - // Services with a partitional principal - default: - return `${service}.${urlSuffix}`; + function determineConfiguration(service: string): (service: string, region: string, urlSuffix: string) => string { + function universal(s: string) { return `${s}.amazonaws.com`; }; + function partitional(s: string, _: string, u: string) { return `${s}.${u}`; }; + function regional(s: string, r: string) { return `${s}.${r}.amazonaws.com`; }; + function regionalPartitional(s: string, r: string, u: string) { return `${s}.${r}.${u}`; }; + + // Exceptions for Service Principals in us-iso-* + const US_ISO_EXCEPTIONS = new Set([ + 'cloudhsm', + 'config', + 'states', + 'workspaces', + ]); + + // Account for idiosyncratic Service Principals in `us-iso-*` regions + if (region.startsWith('us-iso-') && US_ISO_EXCEPTIONS.has(service)) { + switch (service) { + // Services with universal principal + case ('states'): + return universal; + + // Services with a partitional principal + default: + return partitional; + } } - } - // Exceptions for Service Principals in us-isob-* - const US_ISOB_EXCEPTIONS = new Set([ - 'dms', - 'states', - ]); + // Exceptions for Service Principals in us-isob-* + const US_ISOB_EXCEPTIONS = new Set([ + 'dms', + 'states', + ]); + + // Account for idiosyncratic Service Principals in `us-isob-*` regions + if (region.startsWith('us-isob-') && US_ISOB_EXCEPTIONS.has(service)) { + switch (service) { + // Services with universal principal + case ('states'): + return universal; + + // Services with a partitional principal + default: + return partitional; + } + } - // Account for idiosyncratic Service Principals in `us-isob-*` regions - if (region.startsWith('us-isob-') && US_ISOB_EXCEPTIONS.has(service)) { switch (service) { - // Services with universal principal - case ('states'): - return `${service}.amazonaws.com`; + // SSM turned from global to regional at some point + case 'ssm': + return before(region, RULE_SSM_PRINCIPALS_ARE_REGIONAL) + ? universal + : regional; + + // CodeDeploy is regional+partitional in CN, only regional everywhere else + case 'codedeploy': + return region.startsWith('cn-') + ? regionalPartitional + : regional; + + // Services with a regional AND partitional principal + case 'logs': + return regionalPartitional; + + // Services with a regional principal + case 'states': + return regional; // Services with a partitional principal - default: - return `${service}.${urlSuffix}`; - } - } - - // SSM turned from global to regional at some point - if (service === 'ssm') { - return before(region, RULE_SSM_PRINCIPALS_ARE_REGIONAL) - ? `${service}.amazonaws.com` - : `${service}.${region}.amazonaws.com`; - } - - switch (service) { - // Services with a regional AND partitional principal - case 'codedeploy': - case 'logs': - return `${service}.${region}.${urlSuffix}`; - - // Services with a regional principal - case 'states': - return `${service}.${region}.amazonaws.com`; + case 'ec2': + return partitional; - // Services with a partitional principal - case 'ec2': - return `${service}.${urlSuffix}`; + // Services with a universal principal across all regions/partitions (the default case) + default: + return universal; - // Services with a universal principal across all regions/partitions (the default case) - default: - return `${service}.amazonaws.com`; + } + }; - } + const configuration = determineConfiguration(serviceName); + return configuration(serviceName, region, urlSuffix); } private constructor() { } @@ -108,4 +122,4 @@ export class Default { function extractSimpleName(serviceFqn: string) { const matches = serviceFqn.match(/^([^.]+)(?:(?:\.amazonaws\.com(?:\.cn)?)|(?:\.c2s\.ic\.gov)|(?:\.sc2s\.sgov\.gov))?$/); return matches ? matches[1] : undefined; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/region-info/lib/fact.ts b/packages/@aws-cdk/region-info/lib/fact.ts index 0c4d12a53bec4..9c2831f67d2c6 100644 --- a/packages/@aws-cdk/region-info/lib/fact.ts +++ b/packages/@aws-cdk/region-info/lib/fact.ts @@ -9,7 +9,8 @@ export class Fact { * may not be an exhaustive list of all available AWS regions. */ public static get regions(): string[] { - return AWS_REGIONS; + // Return by copy to ensure no modifications can be made to the undelying constant. + return Array.from(AWS_REGIONS); } /** diff --git a/packages/@aws-cdk/region-info/package.json b/packages/@aws-cdk/region-info/package.json index c271c3112e1e2..701e87c62a2c7 100644 --- a/packages/@aws-cdk/region-info/package.json +++ b/packages/@aws-cdk/region-info/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "scripts": { "gen": "bash build-tools/generate.sh", @@ -56,7 +63,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", "@types/fs-extra": "^8.1.2", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "fs-extra": "^9.1.0" }, "repository": { diff --git a/packages/@aws-cdk/region-info/rosetta/default.ts-fixture b/packages/@aws-cdk/region-info/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..3a11f7f47a475 --- /dev/null +++ b/packages/@aws-cdk/region-info/rosetta/default.ts-fixture @@ -0,0 +1,11 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as regionInfo from '@aws-cdk/region-info'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap b/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap index 7ba9353557b9b..678a65fb4ccc3 100644 --- a/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap +++ b/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap @@ -795,7 +795,7 @@ Object { "servicePrincipals": Object { "application-autoscaling": "application-autoscaling.amazonaws.com", "autoscaling": "autoscaling.amazonaws.com", - "codedeploy": "codedeploy.us-iso-east-1.c2s.ic.gov", + "codedeploy": "codedeploy.us-iso-east-1.amazonaws.com", "ec2": "ec2.c2s.ic.gov", "events": "events.amazonaws.com", "lambda": "lambda.amazonaws.com", @@ -826,7 +826,7 @@ Object { "servicePrincipals": Object { "application-autoscaling": "application-autoscaling.amazonaws.com", "autoscaling": "autoscaling.amazonaws.com", - "codedeploy": "codedeploy.us-iso-west-1.c2s.ic.gov", + "codedeploy": "codedeploy.us-iso-west-1.amazonaws.com", "ec2": "ec2.c2s.ic.gov", "events": "events.amazonaws.com", "lambda": "lambda.amazonaws.com", @@ -857,7 +857,7 @@ Object { "servicePrincipals": Object { "application-autoscaling": "application-autoscaling.amazonaws.com", "autoscaling": "autoscaling.amazonaws.com", - "codedeploy": "codedeploy.us-isob-east-1.sc2s.sgov.gov", + "codedeploy": "codedeploy.us-isob-east-1.amazonaws.com", "ec2": "ec2.sc2s.sgov.gov", "events": "events.amazonaws.com", "lambda": "lambda.amazonaws.com", diff --git a/packages/@aws-cdk/region-info/test/default.test.ts b/packages/@aws-cdk/region-info/test/default.test.ts index d1a320fcaac60..dd0dcd68b0a8d 100644 --- a/packages/@aws-cdk/region-info/test/default.test.ts +++ b/packages/@aws-cdk/region-info/test/default.test.ts @@ -5,12 +5,12 @@ const urlSuffix = '.nowhere.null'; describe('servicePrincipal', () => { for (const suffix of ['', '.amazonaws.com', '.amazonaws.com.cn']) { - for (const service of ['states', 'ssm']) { + for (const service of ['codedeploy', 'states', 'ssm']) { test(`${service}${suffix}`, () => { expect(Default.servicePrincipal(`${service}${suffix}`, region, urlSuffix)).toBe(`${service}.${region}.amazonaws.com`); }); } - for (const service of ['codedeploy', 'logs']) { + for (const service of ['logs']) { test(`${service}${suffix}`, () => { expect(Default.servicePrincipal(`${service}${suffix}`, region, urlSuffix)).toBe(`${service}.${region}.${urlSuffix}`); }); @@ -48,6 +48,12 @@ describe('servicePrincipal', () => { }); } + for (const cnRegion of ['cn-north-1', 'cn-northwest-1']) { + test(`Exceptions: codedeploy in ${cnRegion}`, () => { + expect(Default.servicePrincipal('codedeploy', cnRegion, 'amazonaws.com.cn')).toBe(`codedeploy.${cnRegion}.amazonaws.com.cn`); + }); + } + }); diff --git a/packages/@aws-cdk/triggers/.eslintrc.js b/packages/@aws-cdk/triggers/.eslintrc.js new file mode 100644 index 0000000000000..2658ee8727166 --- /dev/null +++ b/packages/@aws-cdk/triggers/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/triggers/.gitignore b/packages/@aws-cdk/triggers/.gitignore new file mode 100644 index 0000000000000..d8a8561d50885 --- /dev/null +++ b/packages/@aws-cdk/triggers/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +nyc.config.js +.LAST_PACKAGE +*.snk +!.eslintrc.js +!jest.config.js + +junit.xml \ No newline at end of file diff --git a/packages/@aws-cdk/triggers/.npmignore b/packages/@aws-cdk/triggers/.npmignore new file mode 100644 index 0000000000000..6077c04a3e8a3 --- /dev/null +++ b/packages/@aws-cdk/triggers/.npmignore @@ -0,0 +1,28 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml +!*.lit.ts +test/ \ No newline at end of file diff --git a/packages/@aws-cdk/triggers/LICENSE b/packages/@aws-cdk/triggers/LICENSE new file mode 100644 index 0000000000000..82ad00bb02d0b --- /dev/null +++ b/packages/@aws-cdk/triggers/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/triggers/NOTICE b/packages/@aws-cdk/triggers/NOTICE new file mode 100644 index 0000000000000..1b7adbb891265 --- /dev/null +++ b/packages/@aws-cdk/triggers/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/triggers/README.md b/packages/@aws-cdk/triggers/README.md new file mode 100644 index 0000000000000..4ed41ce05ca0a --- /dev/null +++ b/packages/@aws-cdk/triggers/README.md @@ -0,0 +1,94 @@ +# Triggers + + +--- + +![cdk-constructs: Stable](https://img.shields.io/badge/cdk--constructs-stable-success.svg?style=for-the-badge) + +--- + + + +Triggers allows you to execute code during deployments. This can be used for a +variety of use cases such as: + +* Self tests: validate something after a resource/construct been provisioned +* Data priming: add initial data to resources after they are created +* Preconditions: check things such as account limits or external dependencies + before deployment. + +## Usage + +The `TriggerFunction` construct will define an AWS Lambda function which is +triggered *during* deployment: + +```ts +import * as lambda from '@aws-cdk/aws-lambda'; +import * as triggers from '@aws-cdk/triggers'; +import { Stack } from '@aws-cdk/core'; + +declare const stack: Stack; + +new triggers.TriggerFunction(stack, 'MyTrigger', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(__dirname + '/my-trigger'), +}); +``` + +In the above example, the AWS Lambda function defined in `myLambdaFunction` will +be invoked when the stack is deployed. + +## Trigger Failures + +If the trigger handler fails (e.g. an exception is raised), the CloudFormation +deployment will fail, as if a resource failed to provision. This makes it easy +to implement "self tests" via triggers by simply making a set of assertions on +some provisioned infrastructure. + +## Order of Execution + +By default, a trigger will be executed by CloudFormation after the associated +handler is provisioned. This means that if the handler takes an implicit +dependency on other resources (e.g. via environment variables), those resources +will be provisioned *before* the trigger is executed. + +In most cases, implicit ordering should be sufficient, but you can also use +`executeAfter` and `executeBefore` to control the order of execution. + +The following example defines the following order: `(hello, world) => myTrigger => goodbye`. +The resources under `hello` and `world` will be provisioned in +parallel, and then the trigger `myTrigger` will be executed. Only then the +resources under `goodbye` will be provisioned: + +```ts +import { Construct, Node } from 'constructs'; +import * as triggers from '@aws-cdk/triggers'; + +declare const myTrigger: triggers.Trigger; +declare const hello: Construct; +declare const world: Construct; +declare const goodbye: Construct; + +myTrigger.executeAfter(hello, world); +myTrigger.executeBefore(goodbye); +``` + +Note that `hello` and `world` are construct *scopes*. This means that they can +be specific resources (such as an `s3.Bucket` object) or groups of resources +composed together into constructs. + +## Re-execution of Triggers + +By default, `executeOnHandlerChange` is enabled. This implies that the trigger +is re-executed every time the handler function code or configuration changes. If +this option is disabled, the trigger will be executed only once upon first +deployment. + +In the future we will consider adding support for additional re-execution modes: + +* `executeOnEveryDeployment: boolean` - re-executes every time the stack is + deployed (add random "salt" during synthesis). +* `executeOnResourceChange: Construct[]` - re-executes when one of the resources + under the specified scopes has changed (add the hash the CloudFormation + resource specs). diff --git a/packages/@aws-cdk/triggers/jest.config.js b/packages/@aws-cdk/triggers/jest.config.js new file mode 100644 index 0000000000000..3a2fd93a1228a --- /dev/null +++ b/packages/@aws-cdk/triggers/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/triggers/lib/index.ts b/packages/@aws-cdk/triggers/lib/index.ts new file mode 100644 index 0000000000000..1e2bf5e2dab37 --- /dev/null +++ b/packages/@aws-cdk/triggers/lib/index.ts @@ -0,0 +1,2 @@ +export * from './trigger'; +export * from './trigger-function'; \ No newline at end of file diff --git a/packages/@aws-cdk/triggers/lib/lambda/.gitignore b/packages/@aws-cdk/triggers/lib/lambda/.gitignore new file mode 100644 index 0000000000000..eef5e8dd7e177 --- /dev/null +++ b/packages/@aws-cdk/triggers/lib/lambda/.gitignore @@ -0,0 +1 @@ +__entrypoint__.js diff --git a/packages/@aws-cdk/triggers/lib/lambda/index.ts b/packages/@aws-cdk/triggers/lib/lambda/index.ts new file mode 100644 index 0000000000000..7975ac5b56bc0 --- /dev/null +++ b/packages/@aws-cdk/triggers/lib/lambda/index.ts @@ -0,0 +1,56 @@ +/* eslint-disable no-console */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import * as AWS from 'aws-sdk'; + +export type InvokeFunction = (functionName: string) => Promise; + +export const invoke: InvokeFunction = async functionName => { + const lambda = new AWS.Lambda(); + const invokeRequest = { FunctionName: functionName }; + console.log({ invokeRequest }); + const invokeResponse = await lambda.invoke(invokeRequest).promise(); + console.log({ invokeResponse }); + return invokeResponse; +}; + +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) { + console.log({ event }); + + if (event.RequestType === 'Delete') { + console.log('not calling trigger on DELETE'); + return; + } + + const handlerArn = event.ResourceProperties.HandlerArn; + if (!handlerArn) { + throw new Error('The "HandlerArn" property is required'); + } + + const invokeResponse = await invoke(handlerArn); + + if (invokeResponse.StatusCode !== 200) { + throw new Error(`Trigger handler failed with status code ${invokeResponse.StatusCode}`); + } + + // if the lambda function throws an error, parse the error message and fail + if (invokeResponse.FunctionError) { + throw new Error(parseError(invokeResponse.Payload?.toString())); + } +}; + +/** + * Parse the error message from the lambda function. + */ +function parseError(payload: string | undefined): string { + console.log(`Error payload: ${payload}`); + if (!payload) { return 'unknown handler error'; } + try { + const error = JSON.parse(payload); + const concat = [error.errorMessage, error.trace].filter(x => x).join('\n'); + return concat.length > 0 ? concat : payload; + } catch (e) { + // fall back to just returning the payload + return payload; + } +} diff --git a/packages/@aws-cdk/triggers/lib/trigger-function.ts b/packages/@aws-cdk/triggers/lib/trigger-function.ts new file mode 100644 index 0000000000000..6504c9f5d1334 --- /dev/null +++ b/packages/@aws-cdk/triggers/lib/trigger-function.ts @@ -0,0 +1,37 @@ +import * as lambda from '@aws-cdk/aws-lambda'; +import { Construct } from 'constructs'; +import { ITrigger, Trigger, TriggerOptions } from '.'; + +/** + * Props for `InvokeFunction`. + */ +export interface TriggerFunctionProps extends lambda.FunctionProps, TriggerOptions { +} + +/** + * Invokes an AWS Lambda function during deployment. + */ +export class TriggerFunction extends lambda.Function implements ITrigger { + + /** + * The underlying trigger resource. + */ + public readonly trigger: Trigger; + + constructor(scope: Construct, id: string, props: TriggerFunctionProps) { + super(scope, id, props); + + this.trigger = new Trigger(this, 'Trigger', { + ...props, + handler: this, + }); + } + + public executeAfter(...scopes: Construct[]): void { + this.trigger.executeAfter(...scopes); + } + + public executeBefore(...scopes: Construct[]): void { + this.trigger.executeBefore(...scopes); + } +} diff --git a/packages/@aws-cdk/triggers/lib/trigger.ts b/packages/@aws-cdk/triggers/lib/trigger.ts new file mode 100644 index 0000000000000..88299f2373294 --- /dev/null +++ b/packages/@aws-cdk/triggers/lib/trigger.ts @@ -0,0 +1,141 @@ +import { join } from 'path'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { CustomResource, CustomResourceProvider, CustomResourceProviderRuntime, IConstruct } from '@aws-cdk/core'; + +import { Construct, Node } from 'constructs'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +/** + * Interface for triggers. + */ +export interface ITrigger extends IConstruct { + /** + * Adds trigger dependencies. Execute this trigger only after these construct + * scopes have been provisioned. + * + * @param scopes A list of construct scopes which this trigger will depend on. + */ + executeAfter(...scopes: Construct[]): void; + + /** + * Adds this trigger as a dependency on other constructs. This means that this + * trigger will get executed *before* the given construct(s). + * + * @param scopes A list of construct scopes which will take a dependency on + * this trigger. + */ + executeBefore(...scopes: Construct[]): void; +} + +/** + * Options for `Trigger`. + */ +export interface TriggerOptions { + /** + * Adds trigger dependencies. Execute this trigger only after these construct + * scopes have been provisioned. + * + * You can also use `trigger.executeAfter()` to add additional dependencies. + * + * @default [] + */ + readonly executeAfter?: Construct[]; + + /** + * Adds this trigger as a dependency on other constructs. This means that this + * trigger will get executed *before* the given construct(s). + * + * You can also use `trigger.executeBefore()` to add additional dependants. + * + * @default [] + */ + readonly executeBefore?: Construct[]; + + /** + * Re-executes the trigger every time the handler changes. + * + * This implies that the trigger is associated with the `currentVersion` of + * the handler, which gets recreated every time the handler or its + * configuration is updated. + * + * @default true + */ + readonly executeOnHandlerChange?: boolean; +} + +/** + * Props for `Trigger`. + */ +export interface TriggerProps extends TriggerOptions { + /** + * The AWS Lambda function of the handler to execute. + */ + readonly handler: lambda.Function; +} + +/** + * Triggers an AWS Lambda function during deployment. + */ +export class Trigger extends CoreConstruct implements ITrigger { + constructor(scope: Construct, id: string, props: TriggerProps) { + super(scope, id); + + const handlerArn = this.determineHandlerArn(props); + const provider = CustomResourceProvider.getOrCreateProvider(this, 'AWSCDK.TriggerCustomResourceProvider', { + runtime: CustomResourceProviderRuntime.NODEJS_14_X, + codeDirectory: join(__dirname, 'lambda'), + policyStatements: [ + { + Effect: 'Allow', + Action: ['lambda:InvokeFunction'], + Resource: [handlerArn], + }, + ], + }); + + new CustomResource(this, 'Default', { + resourceType: 'Custom::Trigger', + serviceToken: provider.serviceToken, + properties: { + HandlerArn: handlerArn, + }, + }); + + this.executeAfter(...props.executeAfter ?? []); + this.executeBefore(...props.executeBefore ?? []); + } + + public executeAfter(...scopes: Construct[]): void { + Node.of(this).addDependency(...scopes); + } + + public executeBefore(...scopes: Construct[]): void { + for (const s of scopes) { + Node.of(s).addDependency(this); + } + } + + private determineHandlerArn(props: TriggerProps) { + return props.handler.currentVersion.functionArn; + // const executeOnHandlerChange = props.executeOnHandlerChange ?? true; + // if (executeOnHandlerChange) { + // } + + // return props.handler.functionArn; + } +} + +/** + * Determines + */ +export enum TriggerInvalidation { + /** + * The trigger will be executed every time the handler (or its configuration) + * changes. This is implemented by associated the trigger with the `currentVersion` + * of the AWS Lambda function, which gets recreated every time the handler changes. + */ + HANDLER_CHANGE = 'WHEN_FUNCTION_CHANGES', +} \ No newline at end of file diff --git a/packages/@aws-cdk/triggers/package.json b/packages/@aws-cdk/triggers/package.json new file mode 100644 index 0000000000000..3a52bcf9ebf61 --- /dev/null +++ b/packages/@aws-cdk/triggers/package.json @@ -0,0 +1,120 @@ +{ + "name": "@aws-cdk/triggers", + "version": "0.0.0", + "description": "Execute AWS Lambda functions during deployment", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awscdk.triggers", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "cdk-triggers" + } + }, + "dotnet": { + "namespace": "Amazon.CDK.Triggers", + "packageId": "Amazon.CDK.Triggers", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-cdk.triggers", + "module": "aws_cdk.triggers", + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 1" + ] + } + }, + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/triggers" + }, + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "build+test": "yarn build && yarn test", + "build+test+package": "yarn build+test && yarn package", + "compat": "cdk-compat", + "rosetta:extract": "yarn --silent jsii-rosetta extract", + "build+extract": "yarn build && yarn rosetta:extract", + "build+test+extract": "yarn build+test && yarn rosetta:extract" + }, + "keywords": [ + "aws", + "cdk", + "example", + "construct", + "library", + "triggers" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assertions": "0.0.0", + "@aws-cdk/cdk-build-tools": "0.0.0", + "@aws-cdk/cdk-integ-tools": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", + "aws-sdk": "^2.848.0", + "@aws-cdk/pkglint": "0.0.0", + "@types/jest": "^27.4.1", + "jest": "^27.5.1" + }, + "dependencies": { + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.3.69" + }, + "homepage": "https://github.com/aws/aws-cdk", + "peerDependencies": { + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.3.69" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "stable", + "maturity": "stable", + "awscdkio": { + "announce": false + }, + "cdk-build": { + "env": { + "AWSLINT_BASE_CONSTRUCT": true + } + }, + "ubergen": { + "exclude": true + }, + "publishConfig": { + "tag": "latest" + }, + "awslint": { + "exclude": [ + "ref-via-interface:@aws-cdk/triggers.TriggerProps.handler" + ] + } +} diff --git a/packages/@aws-cdk/triggers/test/integ.triggers.expected.json b/packages/@aws-cdk/triggers/test/integ.triggers.expected.json new file mode 100644 index 0000000000000..94a0cf390bc50 --- /dev/null +++ b/packages/@aws-cdk/triggers/test/integ.triggers.expected.json @@ -0,0 +1,203 @@ +{ + "Resources": { + "Topic198E71B3E": { + "Type": "AWS::SNS::Topic", + "DependsOn": [ + "MyFunctionTriggerDB129D7B" + ] + }, + "Topic269377B75": { + "Type": "AWS::SNS::Topic" + }, + "MyFunctionServiceRole3C357FF2": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "MyFunction3BAA72D1": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = function() { console.log(\"hi\"); };" + }, + "Role": { + "Fn::GetAtt": [ + "MyFunctionServiceRole3C357FF2", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "MyFunctionServiceRole3C357FF2" + ] + }, + "MyFunctionCurrentVersion197490AF776ea8de2edf446759649703b18110a4": { + "Type": "AWS::Lambda::Version", + "Properties": { + "FunctionName": { + "Ref": "MyFunction3BAA72D1" + } + } + }, + "MyFunctionTriggerDB129D7B": { + "Type": "Custom::Trigger", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AWSCDKTriggerCustomResourceProviderCustomResourceProviderHandler97BECD91", + "Arn" + ] + }, + "HandlerArn": { + "Ref": "MyFunctionCurrentVersion197490AF776ea8de2edf446759649703b18110a4" + } + }, + "DependsOn": [ + "Topic269377B75" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ], + "Policies": [ + { + "PolicyName": "Inline", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "lambda:InvokeFunction" + ], + "Resource": [ + { + "Ref": "MyFunctionCurrentVersion197490AF776ea8de2edf446759649703b18110a4" + } + ] + } + ] + } + } + ] + } + }, + "AWSCDKTriggerCustomResourceProviderCustomResourceProviderHandler97BECD91": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters9a94767d68ec7d462ebafb65903f259f527cae0775d02a4eb2db7ac720bc61ffS3Bucket8B4BAF9C" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters9a94767d68ec7d462ebafb65903f259f527cae0775d02a4eb2db7ac720bc61ffS3VersionKey2B3BD417" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters9a94767d68ec7d462ebafb65903f259f527cae0775d02a4eb2db7ac720bc61ffS3VersionKey2B3BD417" + } + ] + } + ] + } + ] + ] + } + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "__entrypoint__.handler", + "Role": { + "Fn::GetAtt": [ + "AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A", + "Arn" + ] + }, + "Runtime": "nodejs14.x" + }, + "DependsOn": [ + "AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A" + ] + } + }, + "Parameters": { + "AssetParameters9a94767d68ec7d462ebafb65903f259f527cae0775d02a4eb2db7ac720bc61ffS3Bucket8B4BAF9C": { + "Type": "String", + "Description": "S3 bucket for asset \"9a94767d68ec7d462ebafb65903f259f527cae0775d02a4eb2db7ac720bc61ff\"" + }, + "AssetParameters9a94767d68ec7d462ebafb65903f259f527cae0775d02a4eb2db7ac720bc61ffS3VersionKey2B3BD417": { + "Type": "String", + "Description": "S3 key for asset version \"9a94767d68ec7d462ebafb65903f259f527cae0775d02a4eb2db7ac720bc61ff\"" + }, + "AssetParameters9a94767d68ec7d462ebafb65903f259f527cae0775d02a4eb2db7ac720bc61ffArtifactHash4518D68D": { + "Type": "String", + "Description": "Artifact hash for asset \"9a94767d68ec7d462ebafb65903f259f527cae0775d02a4eb2db7ac720bc61ff\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/triggers/test/integ.triggers.ts b/packages/@aws-cdk/triggers/test/integ.triggers.ts new file mode 100644 index 0000000000000..ad9a7e104438b --- /dev/null +++ b/packages/@aws-cdk/triggers/test/integ.triggers.ts @@ -0,0 +1,21 @@ +import * as lambda from '@aws-cdk/aws-lambda'; +import * as sns from '@aws-cdk/aws-sns'; +import { App, Stack } from '@aws-cdk/core'; +import * as triggers from '../lib'; + +const app = new App(); +const stack = new Stack(app, 'MyStack'); + +const topic1 = new sns.Topic(stack, 'Topic1'); +const topic2 = new sns.Topic(stack, 'Topic2'); + +const trigger = new triggers.TriggerFunction(stack, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.fromInline('exports.handler = function() { console.log("hi"); };'), + executeBefore: [topic1], +}); + +trigger.executeAfter(topic2); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/triggers/test/trigger-handler.test.ts b/packages/@aws-cdk/triggers/test/trigger-handler.test.ts new file mode 100644 index 0000000000000..ab2081c86c572 --- /dev/null +++ b/packages/@aws-cdk/triggers/test/trigger-handler.test.ts @@ -0,0 +1,86 @@ +import * as lambda from '../lib/lambda'; + +afterEach(() => { + jest.resetAllMocks(); +}); + +const handlerArn = 'arn:aws:lambda:us-east-1:123456789012:function:MyTrigger'; +const mockRequest = { + LogicalResourceId: 'MyTrigger', + StackId: 'arn:aws:cloudformation:us-east-1:123456789012:stack/MyStack/12345678-1234-1234-1234-123456789012', + ResponseURL: 'https://cloudformation-custom-resource-response-MyTrigger/', + ResourceProperties: { + ServiceToken: 'arn:aws:lambda:us-east-1:123456789012:function:MyFunction', + HandlerArn: handlerArn, + }, + RequestId: 'MyRequestId', + ResourceType: 'Custom::Trigger', + ServiceToken: 'arn:aws:lambda:us-east-1:123456789012:function:MyFunction', +}; + +test('Create', async () => { + const invokeMock = jest.spyOn(lambda, 'invoke').mockResolvedValue({ + StatusCode: 200, + }); + + await lambda.handler({ RequestType: 'Create', ...mockRequest }); + + expect(invokeMock).toBeCalledTimes(1); + expect(invokeMock).toBeCalledWith(handlerArn); +}); + +test('Update', async () => { + const invokeMock = jest.spyOn(lambda, 'invoke').mockResolvedValue({ + StatusCode: 200, + }); + + await lambda.handler({ RequestType: 'Update', PhysicalResourceId: 'PRID', OldResourceProperties: {}, ...mockRequest }); + + expect(invokeMock).toBeCalledTimes(1); + expect(invokeMock).toBeCalledWith(handlerArn); +}); + +test('Delete - handler not called', async () => { + const invokeMock = jest.spyOn(lambda, 'invoke'); + await lambda.handler({ RequestType: 'Delete', PhysicalResourceId: 'PRID', ...mockRequest }); + expect(invokeMock).not.toBeCalled(); +}); + +test('non-200 status code throws an error', async () => { + const invokeMock = jest.spyOn(lambda, 'invoke').mockResolvedValue({ + StatusCode: 500, + }); + + await expect(lambda.handler({ RequestType: 'Create', ...mockRequest })) + .rejects + .toMatchObject({ message: 'Trigger handler failed with status code 500' }); + + expect(invokeMock).toBeCalledTimes(1); + expect(invokeMock).toBeCalledWith(handlerArn); +}); + +describe('function error', () => { + + const makeTest = (payload: string | undefined, expectedError: string) => { + return async () => { + const invokeMock = jest.spyOn(lambda, 'invoke').mockResolvedValue({ + StatusCode: 200, + FunctionError: 'Unhandled', + Payload: payload, + }); + + await expect(lambda.handler({ RequestType: 'Create', ...mockRequest })) + .rejects + .toMatchObject({ message: expectedError }); + + expect(invokeMock).toBeCalledTimes(1); + expect(invokeMock).toBeCalledWith(handlerArn); + }; + }; + + test('undefined payload', makeTest(undefined, 'unknown handler error')); + test('empty payload', makeTest('', 'unknown handler error')); + test('invalid JSON payload', makeTest('{', '{')); + test('valid JSON payload', makeTest('{"errorMessage": "my error"}', 'my error')); + test('with stack trace', makeTest('{"errorMessage": "my error", "trace": "my stack trace"}', 'my error\nmy stack trace')); +}); diff --git a/packages/@aws-cdk/triggers/test/triggers.test.ts b/packages/@aws-cdk/triggers/test/triggers.test.ts new file mode 100644 index 0000000000000..dd89046c6475a --- /dev/null +++ b/packages/@aws-cdk/triggers/test/triggers.test.ts @@ -0,0 +1,67 @@ +import { Template } from '@aws-cdk/assertions'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as sns from '@aws-cdk/aws-sns'; +import { Stack } from '@aws-cdk/core'; +import * as triggers from '../lib'; + +test('minimal', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new triggers.TriggerFunction(stack, 'MyTrigger', { + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_12_X, + code: lambda.Code.fromInline('foo'), + }); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::Lambda::Function', {}); + template.hasResourceProperties('Custom::Trigger', { + HandlerArn: { Ref: 'MyTriggerCurrentVersion8802742B707afb4f5c680fa04113c095ec4e8b5d' }, + }); +}); + +test('before/after', () => { + // GIVEN + const stack = new Stack(); + const topic1 = new sns.Topic(stack, 'Topic1'); + const topic2 = new sns.Topic(stack, 'Topic2'); + const topic3 = new sns.Topic(stack, 'Topic3'); + const topic4 = new sns.Topic(stack, 'Topic4'); + + // WHEN + const myTrigger = new triggers.TriggerFunction(stack, 'MyTrigger', { + runtime: lambda.Runtime.NODEJS_12_X, + code: lambda.Code.fromInline('zoo'), + handler: 'index.handler', + + // through props + executeAfter: [topic1], + executeBefore: [topic3], + }); + + // through methods + myTrigger.executeBefore(topic4); + myTrigger.executeAfter(topic2); + + // THEN + const triggerId = 'MyTrigger5A0C728D'; + const topic1Id = 'Topic198E71B3E'; + const topic2Id = 'Topic269377B75'; + const topic3Id = 'Topic3DEAE47A7'; + const topic4Id = 'Topic4F5C0CEE2'; + + const template = Template.fromStack(stack); + const resources = template.toJSON().Resources; + + const dependsOn = (sourceId: string, targetId: string) => { + expect(resources[sourceId].DependsOn).toContain(targetId); + }; + + dependsOn(triggerId, topic1Id); + dependsOn(triggerId, topic2Id); + dependsOn(topic3Id, triggerId); + dependsOn(topic4Id, triggerId); +}); diff --git a/packages/@aws-cdk/yaml-cfn/package.json b/packages/@aws-cdk/yaml-cfn/package.json index 1fae601d56fc9..819ed1a4c77d1 100644 --- a/packages/@aws-cdk/yaml-cfn/package.json +++ b/packages/@aws-cdk/yaml-cfn/package.json @@ -48,7 +48,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "scripts": { "build": "cdk-build", @@ -69,12 +76,12 @@ "yaml": "1.10.2" }, "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "@types/yaml": "^1.9.7", - "jest": "^27.4.5" + "jest": "^27.5.1" }, "bundledDependencies": [ "yaml" diff --git a/packages/@aws-cdk/yaml-cfn/test/deserialization.test.ts b/packages/@aws-cdk/yaml-cfn/test/deserialization.test.ts index 3d790ea9d60e6..3c6bbc09f2397 100644 --- a/packages/@aws-cdk/yaml-cfn/test/deserialization.test.ts +++ b/packages/@aws-cdk/yaml-cfn/test/deserialization.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import * as yaml_cfn from '../lib'; test('Unquoted year-month-day is treated as a string, not a Date', () => { diff --git a/packages/@aws-cdk/yaml-cfn/test/serialization.test.ts b/packages/@aws-cdk/yaml-cfn/test/serialization.test.ts index 3237f16f986b4..7b9d07ca2a50d 100644 --- a/packages/@aws-cdk/yaml-cfn/test/serialization.test.ts +++ b/packages/@aws-cdk/yaml-cfn/test/serialization.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import * as yaml_cfn from '../lib'; test('An object with a single string value is serialized as a simple string', () => { diff --git a/packages/@monocdk-experiment/assert/package.json b/packages/@monocdk-experiment/assert/package.json index 12efef46cab9d..afbce52f06eb8 100644 --- a/packages/@monocdk-experiment/assert/package.json +++ b/packages/@monocdk-experiment/assert/package.json @@ -34,21 +34,21 @@ "license": "Apache-2.0", "devDependencies": { "@monocdk-experiment/rewrite-imports": "0.0.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "@types/node": "^10.17.60", "@aws-cdk/cdk-build-tools": "0.0.0", "constructs": "^3.3.69", - "jest": "^27.4.5", + "jest": "^27.5.1", "monocdk": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "ts-jest": "^27.1.2" + "ts-jest": "^27.1.3" }, "dependencies": { "@aws-cdk/cloudformation-diff": "0.0.0" }, "peerDependencies": { "constructs": "^3.3.69", - "jest": "^27.4.5", + "jest": "^27.5.1", "monocdk": "^0.0.0" }, "repository": { @@ -64,8 +64,8 @@ "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, - "stability": "experimental", - "maturity": "developer-preview", + "stability": "deprecated", + "maturity": "deprecated", "publishConfig": { "tag": "latest" } diff --git a/packages/@monocdk-experiment/assert/tsconfig.json b/packages/@monocdk-experiment/assert/tsconfig.json index b426f95fcb96a..e1b9688cb975e 100644 --- a/packages/@monocdk-experiment/assert/tsconfig.json +++ b/packages/@monocdk-experiment/assert/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { - "target":"ES2018", - "lib": ["es2018"], + "target":"ES2019", + "lib": ["es2019"], "module": "CommonJS", "declaration": true, "strict": true, diff --git a/packages/@monocdk-experiment/rewrite-imports/package.json b/packages/@monocdk-experiment/rewrite-imports/package.json index ed968b6376e81..2e36b49a7b51f 100644 --- a/packages/@monocdk-experiment/rewrite-imports/package.json +++ b/packages/@monocdk-experiment/rewrite-imports/package.json @@ -36,7 +36,7 @@ }, "devDependencies": { "@types/glob": "^7.2.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "@types/node": "^10.17.60", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0" diff --git a/packages/aws-cdk-lib/README.md b/packages/aws-cdk-lib/README.md index 017a85437675c..b9c8689353109 100644 --- a/packages/aws-cdk-lib/README.md +++ b/packages/aws-cdk-lib/README.md @@ -88,6 +88,45 @@ organize their deployments with. If you want to vend a reusable construct, define it as a subclasses of `Construct`: the consumers of your construct will decide where to place it in their own stacks. +## Stack Synthesizers + +Each Stack has a *synthesizer*, an object that determines how and where +the Stack should be synthesized and deployed. The synthesizer controls +aspects like: + +- How does the stack reference assets? (Either through CloudFormation + parameters the CLI supplies, or because the Stack knows a predefined + location where assets will be uploaded). +- What roles are used to deploy the stack? These can be bootstrapped + roles, roles created in some other way, or just the CLI's current + credentials. + +The following synthesizers are available: + +- `DefaultStackSynthesizer`: recommended. Uses predefined asset locations and + roles created by the modern bootstrap template. Access control is done by + controlling who can assume the deploy role. This is the default stack + synthesizer in CDKv2. +- `LegacyStackSynthesizer`: Uses CloudFormation parameters to communicate + asset locations, and the CLI's current permissions to deploy stacks. The + is the default stack synthesizer in CDKv1. +- `CliCredentialsStackSynthesizer`: Uses predefined asset locations, and the + CLI's current permissions. + +Each of these synthesizers takes configuration arguments. To configure +a stack with a synthesizer, pass it as one of its properties: + +```ts +new MyStack(app, 'MyStack', { + synthesizer: new DefaultStackSynthesizer({ + fileAssetsBucketName: 'my-orgs-asset-bucket', + }), +}); +``` + +For more information on bootstrapping accounts and customizing synthesis, +see [Bootstrapping in the CDK Developer Guide](https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html). + ## Nested Stacks [Nested stacks](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-nested-stacks.html) are stacks created as part of other stacks. You create a nested stack within another stack by using the `NestedStack` construct. diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index f6fd2eca54f1e..b710471c6b70f 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -110,7 +110,7 @@ "fs-extra": "^9.1.0", "ignore": "^5.2.0", "jsonschema": "^1.4.0", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "punycode": "^2.1.1", "semver": "^7.3.5", "yaml": "1.10.2" @@ -171,6 +171,7 @@ "@aws-cdk/aws-codestarconnections": "0.0.0", "@aws-cdk/aws-codestarnotifications": "0.0.0", "@aws-cdk/aws-cognito": "0.0.0", + "@aws-cdk/aws-cognito-identitypool": "0.0.0", "@aws-cdk/aws-config": "0.0.0", "@aws-cdk/aws-connect": "0.0.0", "@aws-cdk/aws-cur": "0.0.0", @@ -210,6 +211,7 @@ "@aws-cdk/aws-finspace": "0.0.0", "@aws-cdk/aws-fis": "0.0.0", "@aws-cdk/aws-fms": "0.0.0", + "@aws-cdk/aws-forecast": "0.0.0", "@aws-cdk/aws-frauddetector": "0.0.0", "@aws-cdk/aws-fsx": "0.0.0", "@aws-cdk/aws-gamelift": "0.0.0", @@ -224,23 +226,28 @@ "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-imagebuilder": "0.0.0", "@aws-cdk/aws-inspector": "0.0.0", + "@aws-cdk/aws-inspectorv2": "0.0.0", "@aws-cdk/aws-iot": "0.0.0", "@aws-cdk/aws-iot-actions": "0.0.0", "@aws-cdk/aws-iot1click": "0.0.0", "@aws-cdk/aws-iotanalytics": "0.0.0", "@aws-cdk/aws-iotcoredeviceadvisor": "0.0.0", "@aws-cdk/aws-iotevents": "0.0.0", + "@aws-cdk/aws-iotevents-actions": "0.0.0", "@aws-cdk/aws-iotfleethub": "0.0.0", "@aws-cdk/aws-iotsitewise": "0.0.0", "@aws-cdk/aws-iotthingsgraph": "0.0.0", "@aws-cdk/aws-iotwireless": "0.0.0", "@aws-cdk/aws-ivs": "0.0.0", + "@aws-cdk/aws-kafkaconnect": "0.0.0", "@aws-cdk/aws-kendra": "0.0.0", "@aws-cdk/aws-kinesis": "0.0.0", "@aws-cdk/aws-kinesisanalytics": "0.0.0", "@aws-cdk/aws-kinesisanalytics-flink": "0.0.0", + "@aws-cdk/aws-kinesisanalyticsv2": "0.0.0", "@aws-cdk/aws-kinesisfirehose": "0.0.0", "@aws-cdk/aws-kinesisfirehose-destinations": "0.0.0", + "@aws-cdk/aws-kinesisvideo": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-lakeformation": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", @@ -346,7 +353,7 @@ "@types/fs-extra": "^8.1.2", "@types/node": "^10.17.60", "constructs": "^3.3.69", - "esbuild": "^0.14.9", + "esbuild": "^0.14.23", "fs-extra": "^9.1.0", "ts-node": "^9.1.1", "typescript": "~3.8.3" @@ -466,6 +473,7 @@ "./aws-finspace": "./aws-finspace/index.js", "./aws-fis": "./aws-fis/index.js", "./aws-fms": "./aws-fms/index.js", + "./aws-forecast": "./aws-forecast/index.js", "./aws-frauddetector": "./aws-frauddetector/index.js", "./aws-fsx": "./aws-fsx/index.js", "./aws-gamelift": "./aws-gamelift/index.js", @@ -480,6 +488,7 @@ "./aws-iam": "./aws-iam/index.js", "./aws-imagebuilder": "./aws-imagebuilder/index.js", "./aws-inspector": "./aws-inspector/index.js", + "./aws-inspectorv2": "./aws-inspectorv2/index.js", "./aws-iot": "./aws-iot/index.js", "./aws-iot1click": "./aws-iot1click/index.js", "./aws-iotanalytics": "./aws-iotanalytics/index.js", @@ -493,7 +502,9 @@ "./aws-kendra": "./aws-kendra/index.js", "./aws-kinesis": "./aws-kinesis/index.js", "./aws-kinesisanalytics": "./aws-kinesisanalytics/index.js", + "./aws-kinesisanalyticsv2": "./aws-kinesisanalyticsv2/index.js", "./aws-kinesisfirehose": "./aws-kinesisfirehose/index.js", + "./aws-kinesisvideo": "./aws-kinesisvideo/index.js", "./aws-kms": "./aws-kms/index.js", "./aws-lakeformation": "./aws-lakeformation/index.js", "./aws-lambda": "./aws-lambda/index.js", @@ -594,5 +605,6 @@ "./pipelines/.warnings.jsii.js": "./pipelines/.warnings.jsii.js", "./pipelines/lib/helpers-internal": "./pipelines/lib/helpers-internal/index.js", "./region-info": "./region-info/index.js" - } + }, + "preferredCdkCliVersion": "2" } diff --git a/packages/aws-cdk-lib/scripts/verify-imports-resolve-same.ts b/packages/aws-cdk-lib/scripts/verify-imports-resolve-same.ts index ad15896f072b0..99b8ca286a1e2 100644 --- a/packages/aws-cdk-lib/scripts/verify-imports-resolve-same.ts +++ b/packages/aws-cdk-lib/scripts/verify-imports-resolve-same.ts @@ -84,7 +84,7 @@ async function compileAndResolve(fileName: string, contents: string, symbolName: } // Return the filename - const srcFile = sym.declarations?.[0].getSourceFile().fileName.replace(/.ts|.js|.d.ts/, ''); + const srcFile = sym.declarations?.[0].getSourceFile().fileName.replace(/[.](ts|js|d\.ts)$/, ''); if (!srcFile) { console.log(sym); throw new Error(`Symbol ${symbolName} in '${contents}' does not resolve to a source location`); diff --git a/packages/aws-cdk-migration/package.json b/packages/aws-cdk-migration/package.json index 73d72e6a0b5c6..fc601186248ca 100644 --- a/packages/aws-cdk-migration/package.json +++ b/packages/aws-cdk-migration/package.json @@ -39,7 +39,7 @@ }, "devDependencies": { "@types/glob": "^7.2.0", - "@types/jest": "^27.0.3", + "@types/jest": "^27.4.1", "@types/node": "^10.17.60", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0" diff --git a/packages/aws-cdk/CONTRIBUTING.md b/packages/aws-cdk/CONTRIBUTING.md index 3d1190e4a1609..10bd6fcd29ee5 100644 --- a/packages/aws-cdk/CONTRIBUTING.md +++ b/packages/aws-cdk/CONTRIBUTING.md @@ -131,7 +131,7 @@ Its value can either be that of `CANDIDATE_VERSION` (for testing against the lat Following are the steps involved in running these tests: -1. Run [`./bump-candidate.sh`](../../bump-candidate.sh) to differentiate between the local version and the published version. For example, if the version in `lerna.json` is `1.67.0`, this script will result in a version `1.67.0-rc.0`. This is needed so that we can launch a verdaccio instance serving local tarballs without worrying about conflicts with the public npm uplink. This will help us avoid version quirks that might happen during the *post-release-pre-merge-back* time window. +1. Run [`./bump-candidate.sh`](../../scripts/bump-candidate.sh) to differentiate between the local version and the published version. For example, if the version in `lerna.json` is `1.67.0`, this script will result in a version `1.67.0-rc.0`. This is needed so that we can launch a verdaccio instance serving local tarballs without worrying about conflicts with the public npm uplink. This will help us avoid version quirks that might happen during the *post-release-pre-merge-back* time window. 2. Run [`./align-version.sh`](../../scripts/align-version.sh) to configure the above version in all our packages. diff --git a/packages/aws-cdk/NOTICE b/packages/aws-cdk/NOTICE index 1b7adbb891265..2fdce4d2a0591 100644 --- a/packages/aws-cdk/NOTICE +++ b/packages/aws-cdk/NOTICE @@ -1,2 +1,16 @@ AWS Cloud Development Kit (AWS CDK) Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Third party attributions of this package can be found in the THIRD_PARTY_LICENSES file \ No newline at end of file diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 0afbb913fa0e0..372be74d765b3 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -11,18 +11,20 @@ The AWS CDK Toolkit provides the `cdk` command-line interface that can be used to work with AWS CDK applications. -Command | Description -----------------------------------|------------------------------------------------------------------------------------- -[`cdk docs`](#cdk-docs) | Access the online documentation -[`cdk init`](#cdk-init) | Start a new CDK project (app or library) -[`cdk list`](#cdk-list) | List stacks in an application -[`cdk synth`](#cdk-synthesize) | Synthesize a CDK app to CloudFormation template(s) -[`cdk diff`](#cdk-diff) | Diff stacks against current state -[`cdk deploy`](#cdk-deploy) | Deploy a stack into an AWS account -[`cdk watch`](#cdk-watch) | Watches a CDK app for deployable and hotswappable changes -[`cdk destroy`](#cdk-destroy) | Deletes a stack from an AWS account -[`cdk bootstrap`](#cdk-bootstrap) | Deploy a toolkit stack to support deploying large stacks & artifacts -[`cdk doctor`](#cdk-doctor) | Inspect the environment and produce information useful for troubleshooting +Command | Description +--------------------------------------|--------------------------------------------------------------------------------- +[`cdk docs`](#cdk-docs) | Access the online documentation +[`cdk init`](#cdk-init) | Start a new CDK project (app or library) +[`cdk list`](#cdk-list) | List stacks in an application +[`cdk synth`](#cdk-synthesize) | Synthesize a CDK app to CloudFormation template(s) +[`cdk diff`](#cdk-diff) | Diff stacks against current state +[`cdk deploy`](#cdk-deploy) | Deploy a stack into an AWS account +[`cdk watch`](#cdk-watch) | Watches a CDK app for deployable and hotswappable changes +[`cdk destroy`](#cdk-destroy) | Deletes a stack from an AWS account +[`cdk bootstrap`](#cdk-bootstrap) | Deploy a toolkit stack to support deploying large stacks & artifacts +[`cdk doctor`](#cdk-doctor) | Inspect the environment and produce information useful for troubleshooting +[`cdk acknowledge`](#cdk-acknowledge) | Acknowledge (and hide) a notice by issue number +[`cdk notices`](#cdk-notices) | List all relevant notices for the application This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. @@ -362,12 +364,13 @@ and that you have the necessary IAM permissions to update the resources that are Hotswapping is currently supported for the following changes (additional changes will be supported in the future): -- Code asset and tag changes of AWS Lambda functions. +- Code asset (including Docker image and inline code) and tag changes of AWS Lambda functions. - AWS Lambda Versions and Aliases changes. - Definition changes of AWS Step Functions State Machines. - Container asset changes of AWS ECS Services. - Website asset changes of AWS S3 Bucket Deployments. - Source and Environment changes of AWS CodeBuild Projects. +- VTL mapping template changes for AppSync Resolvers and Functions **⚠ Note #1**: This command deliberately introduces drift in CloudFormation stacks in order to speed up deployments. For this reason, only use it for development purposes. @@ -436,6 +439,13 @@ for example: Note that `watch` by default uses hotswap deployments (see above for details) -- to turn them off, pass the `--no-hotswap` option when invoking it. +By default `watch` will also monitor all CloudWatch Log Groups in your application and stream the log events +locally to your terminal. To disable this feature you can pass the `--no-logs` option when invoking it: + +```console +$ cdk watch --no-logs +``` + **Note**: This command is considered experimental, and might have breaking changes in the future. @@ -495,6 +505,102 @@ $ cdk doctor - AWS_SDK_LOAD_CONFIG = 1 ``` +## Notices + +CDK Notices are important messages regarding security vulnerabilities, regressions, and usage of unsupported +versions. Relevant notices appear on every command by default. For example, + +```console +$ cdk deploy + +... # Normal output of the command + +NOTICES + +16603 Toggling off auto_delete_objects for Bucket empties the bucket + + Overview: If a stack is deployed with an S3 bucket with + auto_delete_objects=True, and then re-deployed with + auto_delete_objects=False, all the objects in the bucket + will be deleted. + + Affected versions: <1.126.0. + + More information at: https://github.com/aws/aws-cdk/issues/16603 + + +17061 Error when building EKS cluster with monocdk import + + Overview: When using monocdk/aws-eks to build a stack containing + an EKS cluster, error is thrown about missing + lambda-layer-node-proxy-agent/layer/package.json. + + Affected versions: >=1.126.0 <=1.130.0. + + More information at: https://github.com/aws/aws-cdk/issues/17061 + + +If you don’t want to see an notice anymore, use "cdk acknowledge ID". For example, "cdk acknowledge 16603". +``` + +You can suppress warnings in a variety of ways: + +- per individual execution: + + `cdk deploy --no-notices` + +- disable all notices indefinitely through context in `cdk.json`: + + ```json + { + "notices": false, + "context": { + ... + } + } + ``` + +- acknowleding individual notices via `cdk acknowledge` (see below). + +### `cdk acknowledge` + +To hide a particular notice that has been addressed or does not apply, call `cdk acknowledge` with the ID of +the notice: + +```console +$cdk acknowledge 16603 +``` + +> Please note that the acknowledgements are made project by project. If you acknowledge an notice in one CDK +> project, it will still appear on other projects when you run any CDK commands, unless you have suppressed +> or disabled notices. + + +### `cdk notices` + +List the notices that are relevant to the current CDK repository, regardless of context flags or notices that +have been acknowledged: + +```console +$ cdk notices + +NOTICES + +16603 Toggling off auto_delete_objects for Bucket empties the bucket + + Overview: if a stack is deployed with an S3 bucket with + auto_delete_objects=True, and then re-deployed with + auto_delete_objects=False, all the objects in the bucket + will be deleted. + + Affected versions: framework: <=2.15.0 >=2.10.0 + + More information at: https://github.com/aws/aws-cdk/issues/16603 + + +If you don’t want to see a notice anymore, use "cdk acknowledge ". For example, "cdk acknowledge 16603". +``` + ### Bundling By default asset bundling is skipped for `cdk list` and `cdk destroy`. For `cdk deploy`, `cdk diff` @@ -542,8 +648,8 @@ Some of the interesting keys that can be used in the JSON configuration files: ``` If specified, the command in the `build` key will be executed immediately before synthesis. -This can be used to build Lambda Functions, CDK Application code, or other assets. -`build` cannot be specified on the command line or in the User configuration, +This can be used to build Lambda Functions, CDK Application code, or other assets. +`build` cannot be specified on the command line or in the User configuration, and must be specified in the Project configuration. The command specified in `build` will be executed by the "watch" process before deployment. diff --git a/packages/aws-cdk/THIRD_PARTY_LICENSES b/packages/aws-cdk/THIRD_PARTY_LICENSES new file mode 100644 index 0000000000000..f36cbfb82a1b9 --- /dev/null +++ b/packages/aws-cdk/THIRD_PARTY_LICENSES @@ -0,0 +1,3768 @@ +The aws-cdk package includes the following third-party software/licensing: + +** source-map@0.6.1 - https://www.npmjs.com/package/source-map/v/0.6.1 | BSD-3-Clause + +Copyright (c) 2009-2011, Mozilla Foundation and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the names of the Mozilla Foundation nor the names of project + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------- + +** buffer-from@1.1.2 - https://www.npmjs.com/package/buffer-from/v/1.1.2 | MIT +MIT License + +Copyright (c) 2016, 2018 Linus Unnebäck + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** source-map-support@0.5.21 - https://www.npmjs.com/package/source-map-support/v/0.5.21 | MIT + +---------------- + +** color-name@1.1.4 - https://www.npmjs.com/package/color-name/v/1.1.4 | MIT +The MIT License (MIT) +Copyright (c) 2015 Dmitry Ivanov + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---------------- + +** color-convert@2.0.1 - https://www.npmjs.com/package/color-convert/v/2.0.1 | MIT +Copyright (c) 2011-2016 Heather Arthur + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +---------------- + +** ansi-styles@4.3.0 - https://www.npmjs.com/package/ansi-styles/v/4.3.0 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** has-flag@4.0.0 - https://www.npmjs.com/package/has-flag/v/4.0.0 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** supports-color@7.2.0 - https://www.npmjs.com/package/supports-color/v/7.2.0 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** chalk@4.1.2 - https://www.npmjs.com/package/chalk/v/4.1.2 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** universalify@2.0.0 - https://www.npmjs.com/package/universalify/v/2.0.0 | MIT +(The MIT License) + +Copyright (c) 2017, Ryan Zimmerman + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the 'Software'), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** graceful-fs@4.2.9 - https://www.npmjs.com/package/graceful-fs/v/4.2.9 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter, Ben Noordhuis, and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** fs-extra@9.1.0 - https://www.npmjs.com/package/fs-extra/v/9.1.0 | MIT +(The MIT License) + +Copyright (c) 2011-2017 JP Richardson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** at-least-node@1.0.0 - https://www.npmjs.com/package/at-least-node/v/1.0.0 | ISC +The ISC License +Copyright (c) 2020 Ryan Zimmerman + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** jsonfile@6.1.0 - https://www.npmjs.com/package/jsonfile/v/6.1.0 | MIT +(The MIT License) + +Copyright (c) 2012-2015, JP Richardson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** yaml@1.10.2 - https://www.npmjs.com/package/yaml/v/1.10.2 | ISC +Copyright 2018 Eemeli Aro + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + +---------------- + +** jsonschema@1.4.0 - https://www.npmjs.com/package/jsonschema/v/1.4.0 | MIT +jsonschema is licensed under MIT license. + +Copyright (C) 2012-2015 Tom de Grunt + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** semver@7.3.5 - https://www.npmjs.com/package/semver/v/7.3.5 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** yallist@4.0.0 - https://www.npmjs.com/package/yallist/v/4.0.0 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** lru-cache@6.0.0 - https://www.npmjs.com/package/lru-cache/v/6.0.0 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** aws-sdk@2.1079.0 - https://www.npmjs.com/package/aws-sdk/v/2.1079.0 | Apache-2.0 +AWS SDK for JavaScript +Copyright 2012-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +This product includes software developed at +Amazon Web Services, Inc. (http://aws.amazon.com/). + + +---------------- + +** jmespath@0.16.0 - https://www.npmjs.com/package/jmespath/v/0.16.0 | Apache-2.0 +Copyright 2014 James Saryerwinnie + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------- + +** uuid@3.3.2 - https://www.npmjs.com/package/uuid/v/3.3.2 | MIT + +---------------- + +** xml2js@0.4.19 - https://www.npmjs.com/package/xml2js/v/0.4.19 | MIT +Copyright 2010, 2011, 2012, 2013. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + +---------------- + +** xmlbuilder@9.0.7 - https://www.npmjs.com/package/xmlbuilder/v/9.0.7 | MIT +The MIT License (MIT) + +Copyright (c) 2013 Ozgur Ozcitak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** sax@1.2.4 - https://www.npmjs.com/package/sax/v/1.2.4 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +==== + +`String.fromCodePoint` by Mathias Bynens used according to terms of MIT +License, as follows: + + Copyright Mathias Bynens + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** fast-deep-equal@3.1.3 - https://www.npmjs.com/package/fast-deep-equal/v/3.1.3 | MIT +MIT License + +Copyright (c) 2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** ansi-regex@5.0.1 - https://www.npmjs.com/package/ansi-regex/v/5.0.1 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** strip-ansi@6.0.1 - https://www.npmjs.com/package/strip-ansi/v/6.0.1 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** is-fullwidth-code-point@3.0.0 - https://www.npmjs.com/package/is-fullwidth-code-point/v/3.0.0 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** emoji-regex@8.0.0 - https://www.npmjs.com/package/emoji-regex/v/8.0.0 | MIT +Copyright Mathias Bynens + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** string-width@4.2.3 - https://www.npmjs.com/package/string-width/v/4.2.3 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** astral-regex@2.0.0 - https://www.npmjs.com/package/astral-regex/v/2.0.0 | MIT +MIT License + +Copyright (c) Kevin Mårtensson (github.com/kevva) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** slice-ansi@4.0.0 - https://www.npmjs.com/package/slice-ansi/v/4.0.0 | MIT +MIT License + +Copyright (c) DC +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** table@6.8.0 - https://www.npmjs.com/package/table/v/6.8.0 | BSD-3-Clause +Copyright (c) 2018, Gajus Kuizinas (http://gajus.com/) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Gajus Kuizinas (http://gajus.com/) nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL ANUARY BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------- + +** ajv@8.10.0 - https://www.npmjs.com/package/ajv/v/8.10.0 | MIT +The MIT License (MIT) + +Copyright (c) 2015-2021 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +---------------- + +** lodash.truncate@4.4.2 - https://www.npmjs.com/package/lodash.truncate/v/4.4.2 | MIT +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +---------------- + +** diff@5.0.0 - https://www.npmjs.com/package/diff/v/5.0.0 | BSD-3-Clause +Software License Agreement (BSD License) + +Copyright (c) 2009-2015, Kevin Decker + +All rights reserved. + +Redistribution and use of this software in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + +* Neither the name of Kevin Decker nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---------------- + +** mute-stream@0.0.8 - https://www.npmjs.com/package/mute-stream/v/0.0.8 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** read@1.0.7 - https://www.npmjs.com/package/read/v/1.0.7 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** promptly@3.2.0 - https://www.npmjs.com/package/promptly/v/3.2.0 | MIT +The MIT License (MIT) + +Copyright (c) 2018 Made With MOXY Lda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** yallist@3.1.1 - https://www.npmjs.com/package/yallist/v/3.1.1 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** lru-cache@5.1.1 - https://www.npmjs.com/package/lru-cache/v/5.1.1 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** ms@2.1.2 - https://www.npmjs.com/package/ms/v/2.1.2 | MIT + +---------------- + +** debug@4.3.3 - https://www.npmjs.com/package/debug/v/4.3.3 | MIT +(The MIT License) + +Copyright (c) 2014-2017 TJ Holowaychuk +Copyright (c) 2018-2021 Josh Junon + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +---------------- + +** agent-base@6.0.2 - https://www.npmjs.com/package/agent-base/v/6.0.2 | MIT + +---------------- + +** proxy-from-env@1.1.0 - https://www.npmjs.com/package/proxy-from-env/v/1.1.0 | MIT +The MIT License + +Copyright (C) 2016-2018 Rob Wu + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** data-uri-to-buffer@3.0.1 - https://www.npmjs.com/package/data-uri-to-buffer/v/3.0.1 | MIT + +---------------- + +** get-uri@3.0.2 - https://www.npmjs.com/package/get-uri/v/3.0.2 | MIT + +---------------- + +** universalify@0.1.2 - https://www.npmjs.com/package/universalify/v/0.1.2 | MIT +(The MIT License) + +Copyright (c) 2017, Ryan Zimmerman + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the 'Software'), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** fs-extra@8.1.0 - https://www.npmjs.com/package/fs-extra/v/8.1.0 | MIT +(The MIT License) + +Copyright (c) 2011-2017 JP Richardson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** jsonfile@4.0.0 - https://www.npmjs.com/package/jsonfile/v/4.0.0 | MIT +(The MIT License) + +Copyright (c) 2012-2015, JP Richardson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** file-uri-to-path@2.0.0 - https://www.npmjs.com/package/file-uri-to-path/v/2.0.0 | MIT +Copyright (c) 2014 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** @tootallnate/once@1.1.2 - https://www.npmjs.com/package/@tootallnate/once/v/1.1.2 | MIT + +---------------- + +** isarray@0.0.1 - https://www.npmjs.com/package/isarray/v/0.0.1 | MIT + +---------------- + +** core-util-is@1.0.3 - https://www.npmjs.com/package/core-util-is/v/1.0.3 | MIT +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + +---------------- + +** inherits@2.0.4 - https://www.npmjs.com/package/inherits/v/2.0.4 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + + +---------------- + +** readable-stream@1.1.14 - https://www.npmjs.com/package/readable-stream/v/1.1.14 | MIT +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + +---------------- + +** string_decoder@0.10.31 - https://www.npmjs.com/package/string_decoder/v/0.10.31 | MIT +Copyright Joyent, Inc. and other Node contributors. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** xregexp@2.0.0 - https://www.npmjs.com/package/xregexp/v/2.0.0 | MIT + +---------------- + +** ftp@0.3.10 - https://www.npmjs.com/package/ftp/v/0.3.10 | MIT +Copyright Brian White. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + +---------------- + +** bytes@3.1.2 - https://www.npmjs.com/package/bytes/v/3.1.2 | MIT +(The MIT License) + +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015 Jed Watson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** depd@2.0.0 - https://www.npmjs.com/package/depd/v/2.0.0 | MIT +(The MIT License) + +Copyright (c) 2014-2018 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** setprototypeof@1.2.0 - https://www.npmjs.com/package/setprototypeof/v/1.2.0 | ISC +Copyright (c) 2015, Wes Todd + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** statuses@2.0.1 - https://www.npmjs.com/package/statuses/v/2.0.1 | MIT + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** toidentifier@1.0.1 - https://www.npmjs.com/package/toidentifier/v/1.0.1 | MIT +MIT License + +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** http-errors@2.0.0 - https://www.npmjs.com/package/http-errors/v/2.0.0 | MIT + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong me@jongleberry.com +Copyright (c) 2016 Douglas Christopher Wilson doug@somethingdoug.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** safer-buffer@2.1.2 - https://www.npmjs.com/package/safer-buffer/v/2.1.2 | MIT +MIT License + +Copyright (c) 2018 Nikita Skovoroda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** iconv-lite@0.4.24 - https://www.npmjs.com/package/iconv-lite/v/0.4.24 | MIT +Copyright (c) 2011 Alexander Shtuchkin + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +---------------- + +** unpipe@1.0.0 - https://www.npmjs.com/package/unpipe/v/1.0.0 | MIT +(The MIT License) + +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** raw-body@2.5.0 - https://www.npmjs.com/package/raw-body/v/2.5.0 | MIT +The MIT License (MIT) + +Copyright (c) 2013-2014 Jonathan Ong +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** http-proxy-agent@4.0.1 - https://www.npmjs.com/package/http-proxy-agent/v/4.0.1 | MIT + +---------------- + +** https-proxy-agent@5.0.0 - https://www.npmjs.com/package/https-proxy-agent/v/5.0.0 | MIT + +---------------- + +** ip@1.1.5 - https://www.npmjs.com/package/ip/v/1.1.5 | MIT + +---------------- + +** smart-buffer@4.2.0 - https://www.npmjs.com/package/smart-buffer/v/4.2.0 | MIT +The MIT License (MIT) + +Copyright (c) 2013-2017 Josh Glazebrook + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** socks@2.6.2 - https://www.npmjs.com/package/socks/v/2.6.2 | MIT +The MIT License (MIT) + +Copyright (c) 2013 Josh Glazebrook + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** socks-proxy-agent@5.0.1 - https://www.npmjs.com/package/socks-proxy-agent/v/5.0.1 | MIT + +---------------- + +** estraverse@4.3.0 - https://www.npmjs.com/package/estraverse/v/4.3.0 | BSD-2-Clause +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------- + +** esutils@2.0.3 - https://www.npmjs.com/package/esutils/v/2.0.3 | BSD-2-Clause +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------- + +** escodegen@1.14.3 - https://www.npmjs.com/package/escodegen/v/1.14.3 | BSD-2-Clause +Copyright (C) 2012 Yusuke Suzuki (twitter: @Constellation) and other contributors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------- + +** esprima@4.0.1 - https://www.npmjs.com/package/esprima/v/4.0.1 | BSD-2-Clause +Copyright JS Foundation and other contributors, https://js.foundation/ + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------- + +** tslib@2.3.1 - https://www.npmjs.com/package/tslib/v/2.3.1 | 0BSD +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +---------------- + +** ast-types@0.13.4 - https://www.npmjs.com/package/ast-types/v/0.13.4 | MIT +Copyright (c) 2013 Ben Newman + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** vm2@3.9.8 - https://www.npmjs.com/package/vm2/v/3.9.8 | MIT + +---------------- + +** acorn@8.7.0 - https://www.npmjs.com/package/acorn/v/8.7.0 | MIT +MIT License + +Copyright (C) 2012-2020 by various contributors (see AUTHORS) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** acorn-walk@8.2.0 - https://www.npmjs.com/package/acorn-walk/v/8.2.0 | MIT +MIT License + +Copyright (C) 2012-2020 by various contributors (see AUTHORS) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** degenerator@3.0.2 - https://www.npmjs.com/package/degenerator/v/3.0.2 | MIT + +---------------- + +** pac-resolver@5.0.0 - https://www.npmjs.com/package/pac-resolver/v/5.0.0 | MIT + +---------------- + +** netmask@2.0.2 - https://www.npmjs.com/package/netmask/v/2.0.2 | MIT + +---------------- + +** pac-proxy-agent@5.0.0 - https://www.npmjs.com/package/pac-proxy-agent/v/5.0.0 | MIT + +---------------- + +** proxy-agent@5.0.0 - https://www.npmjs.com/package/proxy-agent/v/5.0.0 | MIT + +---------------- + +** uuid@8.3.2 - https://www.npmjs.com/package/uuid/v/8.3.2 | MIT + +---------------- + +** mime@2.6.0 - https://www.npmjs.com/package/mime/v/2.6.0 | MIT +The MIT License (MIT) + +Copyright (c) 2010 Benjamin Thomas, Robert Kieffer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** fs.realpath@1.0.0 - https://www.npmjs.com/package/fs.realpath/v/1.0.0 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +---- + +This library bundles a version of the `fs.realpath` and `fs.realpathSync` +methods from Node.js v0.10 under the terms of the Node.js MIT license. + +Node's license follows, also included at the header of `old.js` which contains +the licensed code: + + Copyright Joyent, Inc. and other Node contributors. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + +---------------- + +** concat-map@0.0.1 - https://www.npmjs.com/package/concat-map/v/0.0.1 | MIT +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** balanced-match@1.0.2 - https://www.npmjs.com/package/balanced-match/v/1.0.2 | MIT + +---------------- + +** brace-expansion@1.1.11 - https://www.npmjs.com/package/brace-expansion/v/1.1.11 | MIT +MIT License + +Copyright (c) 2013 Julian Gruber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** minimatch@3.1.2 - https://www.npmjs.com/package/minimatch/v/3.1.2 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** path-is-absolute@1.0.1 - https://www.npmjs.com/package/path-is-absolute/v/1.0.1 | MIT +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** glob@7.2.0 - https://www.npmjs.com/package/glob/v/7.2.0 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +## Glob Logo + +Glob's logo created by Tanya Brassie , licensed +under a Creative Commons Attribution-ShareAlike 4.0 International License +https://creativecommons.org/licenses/by-sa/4.0/ + + +---------------- + +** wrappy@1.0.2 - https://www.npmjs.com/package/wrappy/v/1.0.2 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** once@1.4.0 - https://www.npmjs.com/package/once/v/1.4.0 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** inflight@1.0.6 - https://www.npmjs.com/package/inflight/v/1.0.6 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** readdir-glob@1.1.1 - https://www.npmjs.com/package/readdir-glob/v/1.1.1 | Apache-2.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Yann Armelin + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +---------------- + +** async@3.2.3 - https://www.npmjs.com/package/async/v/3.2.3 | MIT +Copyright (c) 2010-2018 Caolan McMahon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** process-nextick-args@2.0.1 - https://www.npmjs.com/package/process-nextick-args/v/2.0.1 | MIT + +---------------- + +** isarray@1.0.0 - https://www.npmjs.com/package/isarray/v/1.0.0 | MIT + +---------------- + +** readable-stream@2.3.7 - https://www.npmjs.com/package/readable-stream/v/2.3.7 | MIT +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + + +---------------- + +** safe-buffer@5.1.2 - https://www.npmjs.com/package/safe-buffer/v/5.1.2 | MIT +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** util-deprecate@1.0.2 - https://www.npmjs.com/package/util-deprecate/v/1.0.2 | MIT +(The MIT License) + +Copyright (c) 2014 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** string_decoder@1.1.1 - https://www.npmjs.com/package/string_decoder/v/1.1.1 | MIT +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + + + +---------------- + +** lazystream@1.0.1 - https://www.npmjs.com/package/lazystream/v/1.0.1 | MIT +Copyright (c) 2013 J. Pommerening, contributors. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + + +---------------- + +** normalize-path@3.0.0 - https://www.npmjs.com/package/normalize-path/v/3.0.0 | MIT +The MIT License (MIT) + +Copyright (c) 2014-2018, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** lodash.defaults@4.2.0 - https://www.npmjs.com/package/lodash.defaults/v/4.2.0 | MIT +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +---------------- + +** lodash.flatten@4.4.0 - https://www.npmjs.com/package/lodash.flatten/v/4.4.0 | MIT +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +---------------- + +** lodash.difference@4.5.0 - https://www.npmjs.com/package/lodash.difference/v/4.5.0 | MIT +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +---------------- + +** lodash.union@4.6.0 - https://www.npmjs.com/package/lodash.union/v/4.6.0 | MIT +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +---------------- + +** lodash.isplainobject@4.0.6 - https://www.npmjs.com/package/lodash.isplainobject/v/4.0.6 | MIT +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +---------------- + +** archiver-utils@2.1.0 - https://www.npmjs.com/package/archiver-utils/v/2.1.0 | MIT +Copyright (c) 2015 Chris Talkington. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +---------------- + +** archiver@5.3.0 - https://www.npmjs.com/package/archiver/v/5.3.0 | MIT +Copyright (c) 2012-2014 Chris Talkington, contributors. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +---------------- + +** readable-stream@3.6.0 - https://www.npmjs.com/package/readable-stream/v/3.6.0 | MIT +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + + +---------------- + +** safe-buffer@5.2.1 - https://www.npmjs.com/package/safe-buffer/v/5.2.1 | MIT +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** string_decoder@1.3.0 - https://www.npmjs.com/package/string_decoder/v/1.3.0 | MIT +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + + + +---------------- + +** compress-commons@4.1.1 - https://www.npmjs.com/package/compress-commons/v/4.1.1 | MIT +Copyright (c) 2014 Chris Talkington, contributors. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +---------------- + +** buffer-crc32@0.2.13 - https://www.npmjs.com/package/buffer-crc32/v/0.2.13 | MIT +The MIT License + +Copyright (c) 2013 Brian J. Brennan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** crc-32@1.2.1 - https://www.npmjs.com/package/crc-32/v/1.2.1 | Apache-2.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (C) 2014-present SheetJS LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---------------- + +** crc32-stream@4.0.2 - https://www.npmjs.com/package/crc32-stream/v/4.0.2 | MIT +Copyright (c) 2014 Chris Talkington, contributors. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +---------------- + +** zip-stream@4.1.0 - https://www.npmjs.com/package/zip-stream/v/4.1.0 | MIT +Copyright (c) 2014 Chris Talkington, contributors. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +---------------- + +** bl@4.1.0 - https://www.npmjs.com/package/bl/v/4.1.0 | MIT + +---------------- + +** tar-stream@2.2.0 - https://www.npmjs.com/package/tar-stream/v/2.2.0 | MIT +The MIT License (MIT) + +Copyright (c) 2014 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------- + +** fs-constants@1.0.0 - https://www.npmjs.com/package/fs-constants/v/1.0.0 | MIT +The MIT License (MIT) + +Copyright (c) 2018 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** end-of-stream@1.4.4 - https://www.npmjs.com/package/end-of-stream/v/1.4.4 | MIT +The MIT License (MIT) + +Copyright (c) 2014 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------- + +** wrap-ansi@7.0.0 - https://www.npmjs.com/package/wrap-ansi/v/7.0.0 | MIT +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** @jsii/check-node@1.54.0 - https://www.npmjs.com/package/@jsii/check-node/v/1.54.0 | Apache-2.0 +jsii +Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + +---------------- + +** brace-expansion@2.0.1 - https://www.npmjs.com/package/brace-expansion/v/2.0.1 | MIT +MIT License + +Copyright (c) 2013 Julian Gruber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** minimatch@5.0.0 - https://www.npmjs.com/package/minimatch/v/5.0.0 | ISC +The ISC License + +Copyright (c) 2011-2022 Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** picomatch@2.3.1 - https://www.npmjs.com/package/picomatch/v/2.3.1 | MIT +The MIT License (MIT) + +Copyright (c) 2017-present, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** readdirp@3.6.0 - https://www.npmjs.com/package/readdirp/v/3.6.0 | MIT +MIT License + +Copyright (c) 2012-2019 Thorsten Lorenz, Paul Miller (https://paulmillr.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** anymatch@3.1.2 - https://www.npmjs.com/package/anymatch/v/3.1.2 | ISC +The ISC License + +Copyright (c) 2019 Elan Shanker, Paul Miller (https://paulmillr.com) + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** is-extglob@2.1.1 - https://www.npmjs.com/package/is-extglob/v/2.1.1 | MIT +The MIT License (MIT) + +Copyright (c) 2014-2016, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** is-glob@4.0.3 - https://www.npmjs.com/package/is-glob/v/4.0.3 | MIT +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** glob-parent@5.1.2 - https://www.npmjs.com/package/glob-parent/v/5.1.2 | ISC +The ISC License + +Copyright (c) 2015, 2019 Elan Shanker + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** braces@3.0.2 - https://www.npmjs.com/package/braces/v/3.0.2 | MIT +The MIT License (MIT) + +Copyright (c) 2014-2018, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** is-number@7.0.0 - https://www.npmjs.com/package/is-number/v/7.0.0 | MIT +The MIT License (MIT) + +Copyright (c) 2014-present, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** to-regex-range@5.0.1 - https://www.npmjs.com/package/to-regex-range/v/5.0.1 | MIT +The MIT License (MIT) + +Copyright (c) 2015-present, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** fill-range@7.0.1 - https://www.npmjs.com/package/fill-range/v/7.0.1 | MIT +The MIT License (MIT) + +Copyright (c) 2014-present, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** binary-extensions@2.2.0 - https://www.npmjs.com/package/binary-extensions/v/2.2.0 | MIT +MIT License + +Copyright (c) 2019 Sindre Sorhus (https://sindresorhus.com), Paul Miller (https://paulmillr.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** is-binary-path@2.1.0 - https://www.npmjs.com/package/is-binary-path/v/2.1.0 | MIT +MIT License + +Copyright (c) 2019 Sindre Sorhus (https://sindresorhus.com), Paul Miller (https://paulmillr.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** chokidar@3.5.3 - https://www.npmjs.com/package/chokidar/v/3.5.3 | MIT +The MIT License (MIT) + +Copyright (c) 2012-2019 Paul Miller (https://paulmillr.com), Elan Shanker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** camelcase@6.3.0 - https://www.npmjs.com/package/camelcase/v/6.3.0 | MIT +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** decamelize@5.0.1 - https://www.npmjs.com/package/decamelize/v/5.0.1 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** y18n@5.0.8 - https://www.npmjs.com/package/y18n/v/5.0.8 | ISC +Copyright (c) 2015, Contributors + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + +---------------- + +** yargs-parser@20.2.9 - https://www.npmjs.com/package/yargs-parser/v/20.2.9 | ISC +Copyright (c) 2016, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** cliui@7.0.4 - https://www.npmjs.com/package/cliui/v/7.0.4 | ISC +Copyright (c) 2015, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** escalade@3.1.1 - https://www.npmjs.com/package/escalade/v/3.1.1 | MIT +MIT License + +Copyright (c) Luke Edwards (lukeed.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** get-caller-file@2.0.5 - https://www.npmjs.com/package/get-caller-file/v/2.0.5 | ISC + +---------------- + +** require-directory@2.1.1 - https://www.npmjs.com/package/require-directory/v/2.1.1 | MIT +The MIT License (MIT) + +Copyright (c) 2011 Troy Goode + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** yargs@16.2.0 - https://www.npmjs.com/package/yargs/v/16.2.0 | MIT +MIT License + +Copyright 2010 James Halliday (mail@substack.net); Modified work Copyright 2014 Contributors (ben@npmjs.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 3f90dfc8ff3e2..4f2652b107236 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -1,523 +1,3 @@ -#!/usr/bin/env node -import 'source-map-support/register'; -import * as cxapi from '@aws-cdk/cx-api'; -import '@jsii/check-node/run'; -import * as colors from 'colors/safe'; -import * as yargs from 'yargs'; +import { cli } from '../lib'; -import { SdkProvider } from '../lib/api/aws-auth'; -import { BootstrapSource, Bootstrapper } from '../lib/api/bootstrap'; -import { CloudFormationDeployments } from '../lib/api/cloudformation-deployments'; -import { StackSelector } from '../lib/api/cxapp/cloud-assembly'; -import { CloudExecutable } from '../lib/api/cxapp/cloud-executable'; -import { execProgram } from '../lib/api/cxapp/exec'; -import { ToolkitInfo } from '../lib/api/toolkit-info'; -import { StackActivityProgress } from '../lib/api/util/cloudformation/stack-activity-monitor'; -import { CdkToolkit } from '../lib/cdk-toolkit'; -import { RequireApproval } from '../lib/diff'; -import { availableInitLanguages, cliInit, printAvailableTemplates } from '../lib/init'; -import { data, debug, error, print, setLogLevel } from '../lib/logging'; -import { PluginHost } from '../lib/plugin'; -import { serializeStructure } from '../lib/serialize'; -import { Command, Configuration, Settings } from '../lib/settings'; -import * as version from '../lib/version'; - -/* eslint-disable max-len */ -/* eslint-disable @typescript-eslint/no-shadow */ // yargs - -async function parseCommandLineArguments() { - // Use the following configuration for array arguments: - // - // { type: 'array', default: [], nargs: 1, requiresArg: true } - // - // The default behavior of yargs is to eat all strings following an array argument: - // - // ./prog --arg one two positional => will parse to { arg: ['one', 'two', 'positional'], _: [] } (so no positional arguments) - // ./prog --arg one two -- positional => does not help, for reasons that I can't understand. Still gets parsed incorrectly. - // - // By using the config above, every --arg will only consume one argument, so you can do the following: - // - // ./prog --arg one --arg two position => will parse to { arg: ['one', 'two'], _: ['positional'] }. - - const initTemplateLanguages = await availableInitLanguages(); - return yargs - .env('CDK') - .usage('Usage: cdk -a COMMAND') - .option('app', { type: 'string', alias: 'a', desc: 'REQUIRED: command-line for executing your app or a cloud assembly directory (e.g. "node bin/my-app.js")', requiresArg: true }) - .option('context', { type: 'array', alias: 'c', desc: 'Add contextual string parameter (KEY=VALUE)', nargs: 1, requiresArg: true }) - .option('plugin', { type: 'array', alias: 'p', desc: 'Name or path of a node package that extend the CDK features. Can be specified multiple times', nargs: 1 }) - .option('trace', { type: 'boolean', desc: 'Print trace for stack warnings' }) - .option('strict', { type: 'boolean', desc: 'Do not construct stacks with warnings' }) - .option('lookups', { type: 'boolean', desc: 'Perform context lookups (synthesis fails if this is disabled and context lookups need to be performed)', default: true }) - .option('ignore-errors', { type: 'boolean', default: false, desc: 'Ignores synthesis errors, which will likely produce an invalid output' }) - .option('json', { type: 'boolean', alias: 'j', desc: 'Use JSON output instead of YAML when templates are printed to STDOUT', default: false }) - .option('verbose', { type: 'boolean', alias: 'v', desc: 'Show debug logs (specify multiple times to increase verbosity)', default: false }) - .count('verbose') - .option('debug', { type: 'boolean', desc: 'Enable emission of additional debugging information, such as creation stack traces of tokens', default: false }) - .option('profile', { type: 'string', desc: 'Use the indicated AWS profile as the default environment', requiresArg: true }) - .option('proxy', { type: 'string', desc: 'Use the indicated proxy. Will read from HTTPS_PROXY environment variable if not specified', requiresArg: true }) - .option('ca-bundle-path', { type: 'string', desc: 'Path to CA certificate to use when validating HTTPS requests. Will read from AWS_CA_BUNDLE environment variable if not specified', requiresArg: true }) - .option('ec2creds', { type: 'boolean', alias: 'i', default: undefined, desc: 'Force trying to fetch EC2 instance credentials. Default: guess EC2 instance status' }) - .option('version-reporting', { type: 'boolean', desc: 'Include the "AWS::CDK::Metadata" resource in synthesized templates (enabled by default)', default: undefined }) - .option('path-metadata', { type: 'boolean', desc: 'Include "aws:cdk:path" CloudFormation metadata for each resource (enabled by default)', default: true }) - .option('asset-metadata', { type: 'boolean', desc: 'Include "aws:asset:*" CloudFormation metadata for resources that uses assets (enabled by default)', default: true }) - .option('role-arn', { type: 'string', alias: 'r', desc: 'ARN of Role to use when invoking CloudFormation', default: undefined, requiresArg: true }) - .option('toolkit-stack-name', { type: 'string', desc: 'The name of the CDK toolkit stack', requiresArg: true }) - .option('staging', { type: 'boolean', desc: 'Copy assets to the output directory (use --no-staging to disable, needed for local debugging the source files with SAM CLI)', default: true }) - .option('output', { type: 'string', alias: 'o', desc: 'Emits the synthesized cloud assembly into a directory (default: cdk.out)', requiresArg: true }) - .option('no-color', { type: 'boolean', desc: 'Removes colors and other style from console output', default: false }) - .command(['list [STACKS..]', 'ls [STACKS..]'], 'Lists all stacks in the app', yargs => yargs - .option('long', { type: 'boolean', default: false, alias: 'l', desc: 'Display environment information for each stack' }), - ) - .command(['synthesize [STACKS..]', 'synth [STACKS..]'], 'Synthesizes and prints the CloudFormation template for this stack', yargs => yargs - .option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only synthesize requested stacks, don\'t include dependencies' }) - .option('validation', { type: 'boolean', desc: 'After synthesis, validate stacks with the "validateOnSynth" attribute set (can also be controlled with CDK_VALIDATION)', default: true }) - .option('quiet', { type: 'boolean', alias: 'q', desc: 'Do not output CloudFormation Template to stdout', default: false })) - .command('bootstrap [ENVIRONMENTS..]', 'Deploys the CDK toolkit stack into an AWS environment', yargs => yargs - .option('bootstrap-bucket-name', { type: 'string', alias: ['b', 'toolkit-bucket-name'], desc: 'The name of the CDK toolkit bucket; bucket will be created and must not exist', default: undefined }) - .option('bootstrap-kms-key-id', { type: 'string', desc: 'AWS KMS master key ID used for the SSE-KMS encryption', default: undefined, conflicts: 'bootstrap-customer-key' }) - .option('bootstrap-customer-key', { type: 'boolean', desc: 'Create a Customer Master Key (CMK) for the bootstrap bucket (you will be charged but can customize permissions, modern bootstrapping only)', default: undefined, conflicts: 'bootstrap-kms-key-id' }) - .option('qualifier', { type: 'string', desc: 'String which must be unique for each bootstrap stack. You must configure it on your CDK app if you change this from the default.', default: undefined }) - .option('public-access-block-configuration', { type: 'boolean', desc: 'Block public access configuration on CDK toolkit bucket (enabled by default) ', default: undefined }) - .option('tags', { type: 'array', alias: 't', desc: 'Tags to add for the stack (KEY=VALUE)', nargs: 1, requiresArg: true, default: [] }) - .option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true }) - .option('trust', { type: 'array', desc: 'The AWS account IDs that should be trusted to perform deployments into this environment (may be repeated, modern bootstrapping only)', default: [], nargs: 1, requiresArg: true }) - .option('trust-for-lookup', { type: 'array', desc: 'The AWS account IDs that should be trusted to look up values in this environment (may be repeated, modern bootstrapping only)', default: [], nargs: 1, requiresArg: true }) - .option('cloudformation-execution-policies', { type: 'array', desc: 'The Managed Policy ARNs that should be attached to the role performing deployments into this environment (may be repeated, modern bootstrapping only)', default: [], nargs: 1, requiresArg: true }) - .option('force', { alias: 'f', type: 'boolean', desc: 'Always bootstrap even if it would downgrade template version', default: false }) - .option('termination-protection', { type: 'boolean', default: undefined, desc: 'Toggle CloudFormation termination protection on the bootstrap stacks' }) - .option('show-template', { type: 'boolean', desc: 'Instead of actual bootstrapping, print the current CLI\'s bootstrapping template to stdout for customization', default: false }) - .option('template', { type: 'string', requiresArg: true, desc: 'Use the template from the given file instead of the built-in one (use --show-template to obtain an example)' }), - ) - .command('deploy [STACKS..]', 'Deploys the stack(s) named STACKS into your AWS account', yargs => yargs - .option('all', { type: 'boolean', default: false, desc: 'Deploy all available stacks' }) - .option('build-exclude', { type: 'array', alias: 'E', nargs: 1, desc: 'Do not rebuild asset with the given ID. Can be specified multiple times', default: [] }) - .option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only deploy requested stacks, don\'t include dependencies' }) - .option('require-approval', { type: 'string', choices: [RequireApproval.Never, RequireApproval.AnyChange, RequireApproval.Broadening], desc: 'What security-sensitive changes need manual approval' }) - .option('ci', { type: 'boolean', desc: 'Force CI detection', default: process.env.CI !== undefined }) - .option('notification-arns', { type: 'array', desc: 'ARNs of SNS topics that CloudFormation will notify with stack related events', nargs: 1, requiresArg: true }) - // @deprecated(v2) -- tags are part of the Cloud Assembly and tags specified here will be overwritten on the next deployment - .option('tags', { type: 'array', alias: 't', desc: 'Tags to add to the stack (KEY=VALUE), overrides tags from Cloud Assembly (deprecated)', nargs: 1, requiresArg: true }) - .option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true }) - .option('change-set-name', { type: 'string', desc: 'Name of the CloudFormation change set to create' }) - .option('force', { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false }) - .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('rollback', { - type: 'boolean', - desc: "Rollback stack to stable state on failure. Defaults to 'true', iterate more rapidly with --no-rollback or -R. " + - 'Note: do **not** disable this flag for deployments with resource replacements, as that will always fail', - }) - // 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) - .option('hotswap', { - type: 'boolean', - desc: "Attempts to perform a 'hotswap' deployment, " + - 'which skips CloudFormation and updates the resources directly, ' + - 'and falls back to a full deployment if that is not possible. ' + - 'Do not use this in production environments', - }) - .option('watch', { - type: 'boolean', - desc: 'Continuously observe the project files, ' + - 'and deploy the given stack(s) automatically when changes are detected. ' + - 'Implies --hotswap by default', - }), - ) - .command('watch [STACKS..]', "Shortcut for 'deploy --watch'", yargs => yargs - // I'm fairly certain none of these options, present for 'deploy', make sense for 'watch': - // .option('all', { type: 'boolean', default: false, desc: 'Deploy all available stacks' }) - // .option('ci', { type: 'boolean', desc: 'Force CI detection', default: process.env.CI !== undefined }) - // @deprecated(v2) -- tags are part of the Cloud Assembly and tags specified here will be overwritten on the next deployment - // .option('tags', { type: 'array', alias: 't', desc: 'Tags to add to the stack (KEY=VALUE), overrides tags from Cloud Assembly (deprecated)', nargs: 1, requiresArg: true }) - // .option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true }) - // These options, however, are more subtle - I could be convinced some of these should also be available for 'watch': - // .option('require-approval', { type: 'string', choices: [RequireApproval.Never, RequireApproval.AnyChange, RequireApproval.Broadening], desc: 'What security-sensitive changes need manual approval' }) - // .option('parameters', { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', nargs: 1, requiresArg: true, default: {} }) - // .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('outputs-file', { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true }) - // .option('notification-arns', { type: 'array', desc: 'ARNs of SNS topics that CloudFormation will notify with stack related events', nargs: 1, requiresArg: true }) - .option('build-exclude', { type: 'array', alias: 'E', nargs: 1, desc: 'Do not rebuild asset with the given ID. Can be specified multiple times', default: [] }) - .option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only deploy requested stacks, don\'t include dependencies' }) - .option('change-set-name', { type: 'string', desc: 'Name of the CloudFormation change set to create' }) - .option('force', { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false }) - .option('progress', { type: 'string', choices: [StackActivityProgress.BAR, StackActivityProgress.EVENTS], desc: 'Display mode for stack activity events' }) - .option('rollback', { - type: 'boolean', - desc: "Rollback stack to stable state on failure. Defaults to 'true', iterate more rapidly with --no-rollback or -R. " + - 'Note: do **not** disable this flag for deployments with resource replacements, as that will always fail', - }) - // same hack for -R as above in 'deploy' - .option('R', { type: 'boolean', hidden: true }).middleware(yargsNegativeAlias('R', 'rollback'), true) - .option('hotswap', { - type: 'boolean', - desc: "Attempts to perform a 'hotswap' deployment, " + - 'which skips CloudFormation and updates the resources directly, ' + - 'and falls back to a full deployment if that is not possible. ' + - "'true' by default, use --no-hotswap to turn off", - }), - ) - .command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', yargs => yargs - .option('all', { type: 'boolean', default: false, desc: 'Destroy all available stacks' }) - .option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only destroy requested stacks, don\'t include dependees' }) - .option('force', { type: 'boolean', alias: 'f', desc: 'Do not ask for confirmation before destroying the stacks' })) - .command('diff [STACKS..]', 'Compares the specified stack with the deployed stack or a local template file, and returns with status 1 if any difference is found', yargs => yargs - .option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only diff requested stacks, don\'t include dependencies' }) - .option('context-lines', { type: 'number', desc: 'Number of context lines to include in arbitrary JSON diff rendering', default: 3, requiresArg: true }) - .option('template', { type: 'string', desc: 'The path to the CloudFormation template to compare with', requiresArg: true }) - .option('strict', { type: 'boolean', desc: 'Do not filter out AWS::CDK::Metadata resources', default: false }) - .option('security-only', { type: 'boolean', desc: 'Only diff for broadened security changes', default: false }) - .option('fail', { type: 'boolean', desc: 'Fail with exit code 1 in case of diff', default: false })) - .command('metadata [STACK]', 'Returns all metadata associated with this stack') - .command('init [TEMPLATE]', 'Create a new, empty CDK project from a template.', yargs => yargs - .option('language', { type: 'string', alias: 'l', desc: 'The language to be used for the new project (default can be configured in ~/.cdk.json)', choices: initTemplateLanguages }) - .option('list', { type: 'boolean', desc: 'List the available templates' }) - .option('generate-only', { type: 'boolean', default: false, desc: 'If true, only generates project files, without executing additional operations such as setting up a git repo, installing dependencies or compiling the project' }), - ) - .commandDir('../lib/commands', { exclude: /^_.*/ }) - .version(version.DISPLAY_VERSION) - .demandCommand(1, '') // just print help - .recommendCommands() - .help() - .alias('h', 'help') - .epilogue([ - 'If your app has a single stack, there is no need to specify the stack name', - 'If one of cdk.json or ~/.cdk.json exists, options specified there will be used as defaults. Settings in cdk.json take precedence.', - ].join('\n\n')) - .argv; -} - -if (!process.stdout.isTTY) { - colors.disable(); -} - -async function initCommandLine() { - const argv = await parseCommandLineArguments(); - if (argv.verbose) { - setLogLevel(argv.verbose); - } - debug('CDK toolkit version:', version.DISPLAY_VERSION); - debug('Command line arguments:', argv); - - const configuration = new Configuration({ - commandLineArguments: { - ...argv, - _: argv._ as [Command, ...string[]], // TypeScript at its best - }, - }); - await configuration.load(); - - const sdkProvider = await SdkProvider.withAwsCliCompatibleDefaults({ - profile: configuration.settings.get(['profile']), - ec2creds: argv.ec2creds, - httpOptions: { - proxyAddress: argv.proxy, - caBundlePath: argv['ca-bundle-path'], - }, - }); - - const cloudFormation = new CloudFormationDeployments({ sdkProvider }); - - const cloudExecutable = new CloudExecutable({ - configuration, - sdkProvider, - synthesizer: execProgram, - }); - - /** Function to load plug-ins, using configurations additively. */ - function loadPlugins(...settings: Settings[]) { - const loaded = new Set(); - for (const source of settings) { - const plugins: string[] = source.get(['plugin']) || []; - for (const plugin of plugins) { - const resolved = tryResolve(plugin); - if (loaded.has(resolved)) { continue; } - debug(`Loading plug-in: ${colors.green(plugin)} from ${colors.blue(resolved)}`); - PluginHost.instance.load(plugin); - loaded.add(resolved); - } - } - - function tryResolve(plugin: string): string { - try { - return require.resolve(plugin); - } catch (e) { - error(`Unable to resolve plugin ${colors.green(plugin)}: ${e.stack}`); - throw new Error(`Unable to resolve plug-in: ${plugin}`); - } - } - } - - loadPlugins(configuration.settings); - - const cmd = argv._[0]; - - if (typeof(cmd) !== 'string') { - throw new Error(`First argument should be a string. Got: ${cmd} (${typeof(cmd)})`); - } - - // Bundle up global objects so the commands have access to them - const commandOptions = { args: argv, configuration, aws: sdkProvider }; - - try { - const returnValue = argv.commandHandler - ? await (argv.commandHandler as (opts: typeof commandOptions) => any)(commandOptions) - : await main(cmd, argv); - if (typeof returnValue === 'object') { - return toJsonOrYaml(returnValue); - } else if (typeof returnValue === 'string') { - return returnValue; - } else { - return returnValue; - } - } finally { - await version.displayVersionMessage(); - } - - async function main(command: string, args: any): Promise { - const toolkitStackName: string = ToolkitInfo.determineName(configuration.settings.get(['toolkitStackName'])); - debug(`Toolkit stack: ${colors.bold(toolkitStackName)}`); - - if (args.all && args.STACKS) { - throw new Error('You must either specify a list of Stacks or the `--all` argument'); - } - - args.STACKS = args.STACKS || []; - args.ENVIRONMENTS = args.ENVIRONMENTS || []; - - const selector: StackSelector = { - allTopLevel: args.all, - patterns: args.STACKS, - }; - - const cli = new CdkToolkit({ - cloudExecutable, - cloudFormation, - verbose: argv.trace || argv.verbose > 0, - ignoreErrors: argv['ignore-errors'], - strict: argv.strict, - configuration, - sdkProvider, - }); - - switch (command) { - case 'ls': - case 'list': - return cli.list(args.STACKS, { long: args.long }); - - case 'diff': - const enableDiffNoFail = isFeatureEnabled(configuration, cxapi.ENABLE_DIFF_NO_FAIL); - return cli.diff({ - stackNames: args.STACKS, - exclusively: args.exclusively, - templatePath: args.template, - strict: args.strict, - contextLines: args.contextLines, - securityOnly: args.securityOnly, - fail: args.fail || !enableDiffNoFail, - }); - - case 'bootstrap': - const source: BootstrapSource = determineBootsrapVersion(args, configuration); - - const bootstrapper = new Bootstrapper(source); - - if (args.showTemplate) { - return bootstrapper.showTemplate(); - } - - return cli.bootstrap(args.ENVIRONMENTS, bootstrapper, { - roleArn: args.roleArn, - force: argv.force, - toolkitStackName: toolkitStackName, - execute: args.execute, - tags: configuration.settings.get(['tags']), - terminationProtection: args.terminationProtection, - parameters: { - bucketName: configuration.settings.get(['toolkitBucket', 'bucketName']), - kmsKeyId: configuration.settings.get(['toolkitBucket', 'kmsKeyId']), - createCustomerMasterKey: args.bootstrapCustomerKey, - qualifier: args.qualifier, - publicAccessBlockConfiguration: args.publicAccessBlockConfiguration, - trustedAccounts: arrayFromYargs(args.trust), - trustedAccountsForLookup: arrayFromYargs(args.trustForLookup), - cloudFormationExecutionPolicies: arrayFromYargs(args.cloudformationExecutionPolicies), - }, - }); - - case 'deploy': - const parameterMap: { [name: string]: string | undefined } = {}; - for (const parameter of args.parameters) { - if (typeof parameter === 'string') { - const keyValue = (parameter as string).split('='); - parameterMap[keyValue[0]] = keyValue.slice(1).join('='); - } - } - return cli.deploy({ - selector, - exclusively: args.exclusively, - toolkitStackName, - roleArn: args.roleArn, - notificationArns: args.notificationArns, - requireApproval: configuration.settings.get(['requireApproval']), - reuseAssets: args['build-exclude'], - tags: configuration.settings.get(['tags']), - execute: args.execute, - changeSetName: args.changeSetName, - force: args.force, - parameters: parameterMap, - usePreviousParameters: args['previous-parameters'], - outputsFile: configuration.settings.get(['outputsFile']), - progress: configuration.settings.get(['progress']), - ci: args.ci, - rollback: configuration.settings.get(['rollback']), - hotswap: args.hotswap, - watch: args.watch, - }); - - case 'watch': - return cli.watch({ - selector, - // parameters: parameterMap, - // usePreviousParameters: args['previous-parameters'], - // outputsFile: configuration.settings.get(['outputsFile']), - // requireApproval: configuration.settings.get(['requireApproval']), - // notificationArns: args.notificationArns, - exclusively: args.exclusively, - toolkitStackName, - roleArn: args.roleArn, - reuseAssets: args['build-exclude'], - changeSetName: args.changeSetName, - force: args.force, - progress: configuration.settings.get(['progress']), - rollback: configuration.settings.get(['rollback']), - hotswap: args.hotswap, - }); - - case 'destroy': - return cli.destroy({ - selector, - exclusively: args.exclusively, - force: args.force, - roleArn: args.roleArn, - }); - - case 'synthesize': - case 'synth': - if (args.exclusively) { - return cli.synth(args.STACKS, args.exclusively, args.quiet, args.validation); - } else { - return cli.synth(args.STACKS, true, args.quiet, args.validation); - } - - - case 'metadata': - return cli.metadata(args.STACK); - - case 'init': - const language = configuration.settings.get(['language']); - if (args.list) { - return printAvailableTemplates(language); - } else { - return cliInit(args.TEMPLATE, language, undefined, args.generateOnly); - } - case 'version': - return data(version.DISPLAY_VERSION); - - default: - throw new Error('Unknown command: ' + command); - } - } - - function toJsonOrYaml(object: any): string { - return serializeStructure(object, argv.json); - } -} - -/** - * Determine which version of bootstrapping - * (legacy, or "new") should be used. - */ -function determineBootsrapVersion(args: { template?: string }, configuration: Configuration): BootstrapSource { - const isV1 = version.DISPLAY_VERSION.startsWith('1.'); - return isV1 ? determineV1BootstrapSource(args, configuration) : determineV2BootstrapSource(args); -} - -function determineV1BootstrapSource(args: { template?: string }, configuration: Configuration): BootstrapSource { - let source: BootstrapSource; - if (args.template) { - print(`Using bootstrapping template from ${args.template}`); - source = { source: 'custom', templateFile: args.template }; - } else if (process.env.CDK_NEW_BOOTSTRAP) { - print('CDK_NEW_BOOTSTRAP set, using new-style bootstrapping'); - source = { source: 'default' }; - } else if (isFeatureEnabled(configuration, cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT)) { - print(`'${cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT}' context set, using new-style bootstrapping`); - source = { source: 'default' }; - } else { - // in V1, the "legacy" bootstrapping is the default - source = { source: 'legacy' }; - } - return source; -} - -function determineV2BootstrapSource(args: { template?: string }): BootstrapSource { - let source: BootstrapSource; - if (args.template) { - print(`Using bootstrapping template from ${args.template}`); - source = { source: 'custom', templateFile: args.template }; - } else if (process.env.CDK_LEGACY_BOOTSTRAP) { - print('CDK_LEGACY_BOOTSTRAP set, using legacy-style bootstrapping'); - source = { source: 'legacy' }; - } else { - // in V2, the "new" bootstrapping is the default - source = { source: 'default' }; - } - return source; -} - -function isFeatureEnabled(configuration: Configuration, featureFlag: string) { - return configuration.context.get(featureFlag) ?? cxapi.futureFlagDefault(featureFlag); -} - -/** - * Translate a Yargs input array to something that makes more sense in a programming language - * model (telling the difference between absence and an empty array) - * - * - An empty array is the default case, meaning the user didn't pass any arguments. We return - * undefined. - * - If the user passed a single empty string, they did something like `--array=`, which we'll - * take to mean they passed an empty array. - */ -function arrayFromYargs(xs: string[]): string[] | undefined { - if (xs.length === 0) { return 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; } - if (typeof value === 'string') { - data(value); - } else if (typeof value === 'number') { - process.exitCode = value; - } - }) - .catch(err => { - error(err.message); - if (err.stack) { - debug(err.stack); - } - process.exitCode = 1; - }); +cli(); diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts b/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts index 0da0b027bbc65..ad2af9f62ef61 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts @@ -77,6 +77,33 @@ export interface SdkHttpOptions { const CACHED_ACCOUNT = Symbol('cached_account'); const CACHED_DEFAULT_CREDENTIALS = Symbol('cached_default_credentials'); +/** + * SDK configuration for a given environment + * 'forEnvironment' will attempt to assume a role and if it + * is not successful, then it will either: + * 1. Check to see if the default credentials (local credentials the CLI was executed with) + * are for the given environment. If they are then return those. + * 2. If the default credentials are not for the given environment then + * throw an error + * + * 'didAssumeRole' allows callers to whether they are receiving the assume role + * credentials or the default credentials. + */ +export interface SdkForEnvironment { + /** + * The SDK for the given environment + */ + readonly sdk: ISDK; + + /** + * Whether or not the assume role was successful. + * If the assume role was not successful (false) + * then that means that the 'sdk' returned contains + * the default credentials (not the assume role credentials) + */ + readonly didAssumeRole: boolean; +} + /** * Creates instances of the AWS SDK appropriate for a given account/region. * @@ -140,7 +167,11 @@ export class SdkProvider { * * The `environment` parameter is resolved first (see `resolveEnvironment()`). */ - public async forEnvironment(environment: cxapi.Environment, mode: Mode, options?: CredentialsOptions): Promise { + public async forEnvironment( + environment: cxapi.Environment, + mode: Mode, + options?: CredentialsOptions, + ): Promise { const env = await this.resolveEnvironment(environment); const baseCreds = await this.obtainBaseCredentials(env.account, mode); @@ -151,7 +182,7 @@ export class SdkProvider { // account. if (options?.assumeRoleArn === undefined) { if (baseCreds.source === 'incorrectDefault') { throw new Error(fmtObtainCredentialsError(env.account, baseCreds)); } - return new SDK(baseCreds.credentials, env.region, this.sdkOptions); + return { sdk: new SDK(baseCreds.credentials, env.region, this.sdkOptions), didAssumeRole: false }; } // We will proceed to AssumeRole using whatever we've been given. @@ -161,7 +192,7 @@ export class SdkProvider { // we can determine whether the AssumeRole call succeeds or not. try { await sdk.forceCredentialRetrieval(); - return sdk; + return { sdk, didAssumeRole: true }; } catch (e) { // AssumeRole failed. Proceed and warn *if and only if* the baseCredentials were already for the right account // or returned from a plugin. This is to cover some current setups for people using plugins or preferring to @@ -170,7 +201,7 @@ export class SdkProvider { if (baseCreds.source === 'correctDefault' || baseCreds.source === 'plugin') { debug(e.message); warning(`${fmtObtainedCredentials(baseCreds)} could not be used to assume '${options.assumeRoleArn}', but are for the right account. Proceeding anyway.`); - return new SDK(baseCreds.credentials, env.region, this.sdkOptions); + return { sdk: new SDK(baseCreds.credentials, env.region, this.sdkOptions), didAssumeRole: false }; } throw e; diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index c45098277fbf1..b703bb6130cc5 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -62,6 +62,8 @@ export interface ISDK { kms(): AWS.KMS; stepFunctions(): AWS.StepFunctions; codeBuild(): AWS.CodeBuild + cloudWatchLogs(): AWS.CloudWatchLogs; + appsync(): AWS.AppSync; } /** @@ -185,6 +187,14 @@ export class SDK implements ISDK { return this.wrapServiceErrorHandling(new AWS.CodeBuild(this.config)); } + public cloudWatchLogs(): AWS.CloudWatchLogs { + return this.wrapServiceErrorHandling(new AWS.CloudWatchLogs(this.config)); + } + + public appsync(): AWS.AppSync { + return this.wrapServiceErrorHandling(new AWS.AppSync(this.config)); + } + public async currentAccount(): Promise { // Get/refresh if necessary before we can access `accessKeyId` await this.forceCredentialRetrieval(); diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts index 7877368710c5f..a7b63214a3c80 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts @@ -3,6 +3,7 @@ import * as path from 'path'; import * as cxapi from '@aws-cdk/cx-api'; import { warning } from '../../logging'; import { loadStructuredFile, toYAML } from '../../serialize'; +import { rootDir } from '../../util/directories'; import { SdkProvider } from '../aws-auth'; import { DeployStackResult } from '../deploy-stack'; import { BootstrapEnvironmentOptions, BootstrappingParameters } from './bootstrap-props'; @@ -170,7 +171,7 @@ export class Bootstrapper { case 'custom': return loadStructuredFile(this.source.templateFile); case 'default': - return loadStructuredFile(path.join(__dirname, 'bootstrap-template.yaml')); + return loadStructuredFile(path.join(rootDir(), 'lib', 'api', 'bootstrap', 'bootstrap-template.yaml')); case 'legacy': return legacyBootstrapTemplate(params); } diff --git a/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts b/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts index 49f97e71332c3..df20bf0f62b63 100644 --- a/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts +++ b/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts @@ -3,6 +3,7 @@ import * as path from 'path'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; +import * as logging from '../../logging'; import { Mode, SdkProvider, ISDK } from '../aws-auth'; import { deployStack, DeployStackResult } from '../deploy-stack'; import { DEFAULT_TOOLKIT_STACK_NAME, ToolkitInfo } from '../toolkit-info'; @@ -27,7 +28,7 @@ export class BootstrapStack { toolkitStackName = toolkitStackName ?? DEFAULT_TOOLKIT_STACK_NAME; const resolvedEnvironment = await sdkProvider.resolveEnvironment(environment); - const sdk = await sdkProvider.forEnvironment(resolvedEnvironment, Mode.ForWriting); + const sdk = (await sdkProvider.forEnvironment(resolvedEnvironment, Mode.ForWriting)).sdk; const currentToolkitInfo = await ToolkitInfo.lookup(resolvedEnvironment, sdk, toolkitStackName); @@ -65,7 +66,18 @@ export class BootstrapStack { const newVersion = bootstrapVersionFromTemplate(template); if (this.currentToolkitInfo.found && newVersion < this.currentToolkitInfo.version && !options.force) { - throw new Error(`Not downgrading existing bootstrap stack from version '${this.currentToolkitInfo.version}' to version '${newVersion}'. Use --force to force or set the '@aws-cdk/core:newStyleStackSynthesis' feature flag in cdk.json to use the latest bootstrap version.`); + logging.warning(`Bootstrap stack already at version '${this.currentToolkitInfo.version}'. Not downgrading it to version '${newVersion}' (use --force if you intend to downgrade)`); + if (newVersion === 0) { + // A downgrade with 0 as target version means we probably have a new-style bootstrap in the account, + // and an old-style bootstrap as current target, which means the user probably forgot to put this flag in. + logging.warning('(Did you set the \'@aws-cdk/core:newStyleStackSynthesis\' feature flag in cdk.json?)'); + } + + return { + noOp: true, + outputs: {}, + stackArn: this.currentToolkitInfo.bootstrapStack.stackId, + }; } const outdir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk-bootstrap')); diff --git a/packages/aws-cdk/lib/api/cloudformation-deployments.ts b/packages/aws-cdk/lib/api/cloudformation-deployments.ts index fb7c5410faf3d..5b2c120ce8d3d 100644 --- a/packages/aws-cdk/lib/api/cloudformation-deployments.ts +++ b/packages/aws-cdk/lib/api/cloudformation-deployments.ts @@ -1,32 +1,106 @@ +import * as path from 'path'; import * as cxapi from '@aws-cdk/cx-api'; import { AssetManifest } from 'cdk-assets'; +import * as fs from 'fs-extra'; import { Tag } from '../cdk-toolkit'; -import { debug } from '../logging'; +import { debug, warning } from '../logging'; import { publishAssets } from '../util/asset-publishing'; -import { Mode, SdkProvider } from './aws-auth'; +import { Mode } from './aws-auth/credentials'; +import { ISDK } from './aws-auth/sdk'; +import { SdkProvider } from './aws-auth/sdk-provider'; import { deployStack, DeployStackResult, destroyStack } from './deploy-stack'; +import { LazyListStackResources, ListStackResources } from './evaluate-cloudformation-template'; import { ToolkitInfo } from './toolkit-info'; import { CloudFormationStack, Template } from './util/cloudformation'; import { StackActivityProgress } from './util/cloudformation/stack-activity-monitor'; +import { replaceEnvPlaceholders } from './util/placeholders'; /** - * Replace the {ACCOUNT} and {REGION} placeholders in all strings found in a complex object. + * SDK obtained by assuming the lookup role + * for a given environment */ -export async function replaceEnvPlaceholders(object: A, env: cxapi.Environment, sdkProvider: SdkProvider): Promise { - return cxapi.EnvironmentPlaceholders.replaceAsync(object, { - accountId: () => Promise.resolve(env.account), - region: () => Promise.resolve(env.region), - partition: async () => { - // There's no good way to get the partition! - // We should have had it already, except we don't. - // - // Best we can do is ask the "base credentials" for this environment for their partition. Cross-partition - // AssumeRole'ing will never work anyway, so this answer won't be wrong (it will just be slow!) - return (await sdkProvider.baseCredentialsPartition(env, Mode.ForReading)) ?? 'aws'; - }, - }); +export interface PreparedSdkWithLookupRoleForEnvironment { + /** + * The SDK for the given environment + */ + readonly sdk: ISDK; + + /** + * The resolved environment for the stack + * (no more 'unknown-account/unknown-region') + */ + readonly resolvedEnvironment: cxapi.Environment; + + /** + * Whether or not the assume role was successful. + * If the assume role was not successful (false) + * then that means that the 'sdk' returned contains + * the default credentials (not the assume role credentials) + */ + readonly didAssumeRole: boolean; } +/** + * Try to use the bootstrap lookupRole. There are two scenarios that are handled here + * 1. The lookup role may not exist (it was added in bootstrap stack version 7) + * 2. The lookup role may not have the correct permissions (ReadOnlyAccess was added in + * bootstrap stack version 8) + * + * In the case of 1 (lookup role doesn't exist) `forEnvironment` will either: + * 1. Return the default credentials if the default credentials are for the stack account + * 2. Throw an error if the default credentials are not for the stack account. + * + * If we successfully assume the lookup role we then proceed to 2 and check whether the bootstrap + * stack version is valid. If it is not we throw an error which should be handled in the calling + * function (and fallback to use a different role, etc) + * + * If we do not successfully assume the lookup role, but do get back the default credentials + * then return those and note that we are returning the default credentials. The calling + * function can then decide to use them or fallback to another role. + */ +export async function prepareSdkWithLookupRoleFor( + sdkProvider: SdkProvider, + stack: cxapi.CloudFormationStackArtifact, +): Promise { + const resolvedEnvironment = await sdkProvider.resolveEnvironment(stack.environment); + + // Substitute any placeholders with information about the current environment + const arns = await replaceEnvPlaceholders({ + lookupRoleArn: stack.lookupRole?.arn, + }, resolvedEnvironment, sdkProvider); + + // try to assume the lookup role + const warningMessage = `Could not assume ${arns.lookupRoleArn}, proceeding anyway.`; + const upgradeMessage = `(To get rid of this warning, please upgrade to bootstrap version >= ${stack.lookupRole?.requiresBootstrapStackVersion})`; + try { + const stackSdk = await sdkProvider.forEnvironment(resolvedEnvironment, Mode.ForReading, { + assumeRoleArn: arns.lookupRoleArn, + assumeRoleExternalId: stack.lookupRole?.assumeRoleExternalId, + }); + + // if we succeed in assuming the lookup role, make sure we have the correct bootstrap stack version + if (stackSdk.didAssumeRole && stack.lookupRole?.bootstrapStackVersionSsmParameter && stack.lookupRole.requiresBootstrapStackVersion) { + const version = await ToolkitInfo.versionFromSsmParameter(stackSdk.sdk, stack.lookupRole.bootstrapStackVersionSsmParameter); + if (version < stack.lookupRole.requiresBootstrapStackVersion) { + throw new Error(`Bootstrap stack version '${stack.lookupRole.requiresBootstrapStackVersion}' is required, found version '${version}'.`); + } + // we may not have assumed the lookup role because one was not provided + // if that is the case then don't print the upgrade warning + } else if (!stackSdk.didAssumeRole && stack.lookupRole?.requiresBootstrapStackVersion) { + warning(upgradeMessage); + } + return { ...stackSdk, resolvedEnvironment }; + } catch (e) { + debug(e); + // only print out the warnings if the lookupRole exists AND there is a required + // bootstrap version, otherwise the warnings will print `undefined` + if (stack.lookupRole && stack.lookupRole.requiresBootstrapStackVersion) { + warning(warningMessage); + warning(upgradeMessage); + } + throw (e); + } +} export interface DeployStackOptions { /** @@ -171,6 +245,29 @@ export interface ProvisionerProps { sdkProvider: SdkProvider; } +/** + * SDK obtained by assuming the deploy role + * for a given environment + */ +export interface PreparedSdkForEnvironment { + /** + * The SDK for the given environment + */ + readonly stackSdk: ISDK; + + /** + * The resolved environment for the stack + * (no more 'unknown-account/unknown-region') + */ + readonly resolvedEnvironment: cxapi.Environment; + /** + * The Execution Role that should be passed to CloudFormation. + * + * @default - no execution role is used + */ + readonly cloudFormationRoleArn?: string; +} + /** * Helper class for CloudFormation deployments * @@ -184,13 +281,23 @@ export class CloudFormationDeployments { this.sdkProvider = props.sdkProvider; } - public async readCurrentTemplate(stackArtifact: cxapi.CloudFormationStackArtifact): Promise