diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index d263a2a35e27e..cd536e7a33898 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -18,16 +18,17 @@ description of the bug: minimal amount of code that causes the bug (if possible) or a reference: --> - - - -### Error Log +### What did you expect to happen? +### What actually happened? + ### Environment diff --git a/.github/workflows/issue-label-assign.yml b/.github/workflows/issue-label-assign.yml index 864b2e872a45e..8233cd7837cb8 100644 --- a/.github/workflows/issue-label-assign.yml +++ b/.github/workflows/issue-label-assign.yml @@ -16,14 +16,14 @@ jobs: {"keywords":["[cli]","[command line]"],"labels":["package/tools"],"assignees":["shivlaks"]}, {"keywords":["[@aws-cdk/alexa-ask]","[alexa-ask]","[alexa ask]"],"labels":["@aws-cdk/alexa-ask"],"assignees":["MrArnoldPalmer"]}, {"keywords":["[@aws-cdk/app-delivery]","[app-delivery]","[app delivery]"],"labels":["@aws-cdk/app-delivery"],"assignees":["skinny85"]}, - {"keywords":["[@aws-cdk/assert]","[assert]"],"labels":["@aws-cdk/assert"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/assert]","[assert]"],"labels":["@aws-cdk/assert"],"assignees":["nija-at"]}, {"keywords":["[@aws-cdk/assets]","[assets]"],"labels":["@aws-cdk/assets"],"assignees":["eladb"]}, - {"keywords":["[@aws-cdk/aws-accessanalyzer]","[aws-accessanalyzer]","[accessanalyzer]","[access analyzer]"],"labels":["@aws-cdk/aws-accessanalyzer"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-accessanalyzer]","[aws-accessanalyzer]","[accessanalyzer]","[access analyzer]"],"labels":["@aws-cdk/aws-accessanalyzer"],"assignees":["rix0rrr"]}, {"keywords":["[@aws-cdk/aws-acmpca]","[aws-acmpca]","[acmpca]"],"labels":["@aws-cdk/aws-acmpca"],"assignees":["skinny85"]}, {"keywords":["[@aws-cdk/aws-amazonmq]","[aws-amazonmq]","[amazonmq]","[amazon mq]","[amazon-mq]"],"labels":["@aws-cdk/aws-amazonmq"],"assignees":["MrArnoldPalmer"]}, {"keywords":["[@aws-cdk/aws-amplify]","[aws-amplify]","[amplify]"],"labels":["@aws-cdk/aws-amplify"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["[@aws-cdk/aws-apigateway]","[aws-apigateway]","[apigateway]","[api gateway]","[api-gateway]"],"labels":["@aws-cdk/aws-apigateway"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-apigatewayv2]","[aws-apigatewayv2]","[apigatewayv2]","[apigateway v2]","[api-gateway-v2]"],"labels":["@aws-cdk/aws-apigatewayv2"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-apigateway]","[aws-apigateway]","[apigateway]","[api gateway]","[api-gateway]"],"labels":["@aws-cdk/aws-apigateway"],"assignees":["nija-at"]}, + {"keywords":["[@aws-cdk/aws-apigatewayv2]","[aws-apigatewayv2]","[apigatewayv2]","[apigateway v2]","[api-gateway-v2]"],"labels":["@aws-cdk/aws-apigatewayv2"],"assignees":["nija-at"]}, {"keywords":["[@aws-cdk/aws-appconfig]","[aws-appconfig]","[appconfig]","[app config]","[app-config]"],"labels":["@aws-cdk/aws-appconfig"],"assignees":["MrArnoldPalmer"]}, {"keywords":["[@aws-cdk/aws-applicationautoscaling]","[aws-applicationautoscaling]","[applicationautoscaling]","[application autoscaling]","[application-autoscaling]"],"labels":["@aws-cdk/aws-applicationautoscaling"],"assignees":["NetaNir"]}, {"keywords":["[@aws-cdk/aws-appmesh]","[aws-appmesh]","[appmesh]","[app mesh]","[app-mesh]"],"labels":["@aws-cdk/aws-appmesh"],"assignees":["MrArnoldPalmer"]}, @@ -45,9 +45,9 @@ jobs: {"keywords":["[@aws-cdk/aws-cloud9]","[aws-cloud9]","[cloud9]","[cloud 9]"],"labels":["@aws-cdk/aws-cloud9"],"assignees":["skinny85"]}, {"keywords":["[@aws-cdk/aws-cloudformation]","[aws-cloudformation]","[cloudformation]","[cloud formation]"],"labels":["@aws-cdk/aws-cloudformation"],"assignees":["eladb"]}, {"keywords":["[@aws-cdk/aws-cloudfront]","[aws-cloudfront]","[cloudfront]","[cloud front]"],"labels":["@aws-cdk/aws-cloudfront"],"assignees":["iliapolo"]}, - {"keywords":["[@aws-cdk/aws-cloudtrail]","[aws-cloudtrail]","[cloudtrail]","[cloud trail]"],"labels":["@aws-cdk/aws-cloudtrail"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-cloudwatch]","[aws-cloudwatch]","[cloudwatch]","[cloud watch]"],"labels":["@aws-cdk/aws-cloudwatch"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-cloudwatch-actions]","[aws-cloudwatch-actions]","[cloudwatch-actions]","[cloudwatch actions]"],"labels":["@aws-cdk/aws-cloudwatch-actions"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-cloudtrail]","[aws-cloudtrail]","[cloudtrail]","[cloud trail]"],"labels":["@aws-cdk/aws-cloudtrail"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-cloudwatch]","[aws-cloudwatch]","[cloudwatch]","[cloud watch]"],"labels":["@aws-cdk/aws-cloudwatch"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-cloudwatch-actions]","[aws-cloudwatch-actions]","[cloudwatch-actions]","[cloudwatch actions]"],"labels":["@aws-cdk/aws-cloudwatch-actions"],"assignees":["rix0rrr"]}, {"keywords":["[@aws-cdk/aws-codebuild]","[aws-codebuild]","[codebuild]","[code build]","[code-build]"],"labels":["@aws-cdk/aws-codebuild"],"assignees":["skinny85"]}, {"keywords":["[@aws-cdk/aws-codecommit]","[aws-codecommit]","[codecommit]","[code commit]", "[code-commit]"],"labels":["@aws-cdk/aws-codecommit"],"assignees":["skinny85"]}, {"keywords":["[@aws-cdk/aws-codedeploy]","[aws-codedeploy]","[codedeploy]","[code deploy]","[code-deploy]"],"labels":["@aws-cdk/aws-codedeploy"],"assignees":["skinny85"]}, @@ -57,42 +57,42 @@ jobs: {"keywords":["[@aws-cdk/aws-codestar]","[aws-codestar]","[codestar]"],"labels":["@aws-cdk/aws-codestar"],"assignees":["skinny85"]}, {"keywords":["[@aws-cdk/aws-codestarconnections]","[aws-codestarconnections]","[codestarconnections]","[codestar connections]","[codestar-connections]"],"labels":["@aws-cdk/aws-codestarconnections"],"assignees":["skinny85"]}, {"keywords":["[@aws-cdk/aws-codestarnotifications]","[aws-codestarnotifications]","[codestarnotifications]","[codestar notifications]","[codestar-notifications]"],"labels":["@aws-cdk/aws-codestarnotifications"],"assignees":["skinny85"]}, - {"keywords":["[@aws-cdk/aws-cognito]","[aws-cognito]","[cognito]"],"labels":["@aws-cdk/aws-cognito"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-cognito]","[aws-cognito]","[cognito]"],"labels":["@aws-cdk/aws-cognito"],"assignees":["nija-at"]}, {"keywords":["[@aws-cdk/aws-config]","[aws-config]","[config]"],"labels":["@aws-cdk/aws-config"],"assignees":["MrArnoldPalmer"]}, {"keywords":["[@aws-cdk/aws-datapipeline]","[aws-datapipeline]","[datapipeline]","[data pipeline]","[data-pipeline]"],"labels":["@aws-cdk/aws-datapipeline"],"assignees":["iliapolo"]}, {"keywords":["[@aws-cdk/aws-dax]","[aws-dax]","[dax]"],"labels":["@aws-cdk/aws-dax"],"assignees":["RomainMuller"]}, - {"keywords":["[@aws-cdk/aws-detective]","[aws-detective]","[detective]"],"labels":["@aws-cdk/aws-detective"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-detective]","[aws-detective]","[detective]"],"labels":["@aws-cdk/aws-detective"],"assignees":["rix0rrr"]}, {"keywords":["[@aws-cdk/aws-directoryservice]","[aws-directoryservice]","[directoryservice]","[directory service]","[directory-service]"],"labels":["@aws-cdk/aws-directoryservice"],"assignees":["NetaNir"]}, - {"keywords":["[@aws-cdk/aws-dlm]","[aws-dlm]","[dlm]"],"labels":["@aws-cdk/aws-dlm"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-dms]","[aws-dms]","[dms]"],"labels":["@aws-cdk/aws-dms"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-dlm]","[aws-dlm]","[dlm]"],"labels":["@aws-cdk/aws-dlm"],"assignees":["nija-at"]}, + {"keywords":["[@aws-cdk/aws-dms]","[aws-dms]","[dms]"],"labels":["@aws-cdk/aws-dms"],"assignees":["nija-at"]}, {"keywords":["[@aws-cdk/aws-docdb]","[aws-docdb]","[docdb]","[doc db]","[doc-db]"],"labels":["@aws-cdk/aws-docdb"],"assignees":["iliapolo"]}, {"keywords":["[@aws-cdk/aws-dynamodb]","[aws-dynamodb]","[dynamodb]","[dynamo db]","[dynamo-db]"],"labels":["@aws-cdk/aws-dynamodb"],"assignees":["RomainMuller"]}, {"keywords":["[@aws-cdk/aws-dynamodb-global]","[aws-dynamodb-global]","[dynamodb-global]","[dynamodb global]"],"labels":["@aws-cdk/aws-dynamodb-global"],"assignees":["RomainMuller"]}, - {"keywords":["[@aws-cdk/aws-ec2]","[aws-ec2]","[ec2]","[vpc]"],"labels":["@aws-cdk/aws-ec2"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-ec2]","[aws-ec2]","[ec2]","[vpc]"],"labels":["@aws-cdk/aws-ec2"],"assignees":["rix0rrr"]}, {"keywords":["[@aws-cdk/aws-ecr]","[aws-ecr]","[ecr]"],"labels":["@aws-cdk/aws-ecr"],"assignees":["MrArnoldPalmer"]}, {"keywords":["[@aws-cdk/aws-ecr-assets]","[aws-ecr-assets]","[ecr-assets]","[ecr assets]","[ecrassets]"],"labels":["@aws-cdk/aws-ecr-assets"],"assignees":["eladb"]}, - {"keywords":["[@aws-cdk/aws-efs]","[aws-efs]","[efs]"],"labels":["@aws-cdk/aws-efs"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-efs]","[aws-efs]","[efs]"],"labels":["@aws-cdk/aws-efs"],"assignees":["rix0rrr"]}, {"keywords":["[@aws-cdk/aws-eks]","[aws-eks]","[eks]"],"labels":["@aws-cdk/aws-eks"],"assignees":["eladb"]}, {"keywords":["[@aws-cdk/aws-elasticache]","[aws-elasticache]","[elasticache]","[elastic cache]","[elastic-cache]"],"labels":["@aws-cdk/aws-elasticache"],"assignees":["iliapolo"]}, {"keywords":["[@aws-cdk/aws-elasticbeanstalk]","[aws-elasticbeanstalk]","[elasticbeanstalk]","[elastic beanstalk]","[elastic-beanstalk]"],"labels":["@aws-cdk/aws-elasticbeanstalk"],"assignees":["skinny85"]}, - {"keywords":["[@aws-cdk/aws-elasticloadbalancing]","[aws-elasticloadbalancing]","[elasticloadbalancing]","[elastic loadbalancing]","[elastic-loadbalancing]","[elb]"],"labels":["@aws-cdk/aws-elasticloadbalancing"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-elasticloadbalancingv2]","[aws-elasticloadbalancingv2]","[elasticloadbalancingv2]","[elastic-loadbalancing-v2]","[elbv2]","[elb v2]"],"labels":["@aws-cdk/aws-elasticloadbalancingv2"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-elasticloadbalancingv2-targets]","[aws-elasticloadbalancingv2-targets]","[elasticloadbalancingv2-targets]","[elasticloadbalancingv2 targets]","[elbv2 targets]"],"labels":["@aws-cdk/aws-elasticloadbalancingv2-targets"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-elasticloadbalancing]","[aws-elasticloadbalancing]","[elasticloadbalancing]","[elastic loadbalancing]","[elastic-loadbalancing]","[elb]"],"labels":["@aws-cdk/aws-elasticloadbalancing"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-elasticloadbalancingv2]","[aws-elasticloadbalancingv2]","[elasticloadbalancingv2]","[elastic-loadbalancing-v2]","[elbv2]","[elb v2]"],"labels":["@aws-cdk/aws-elasticloadbalancingv2"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-elasticloadbalancingv2-targets]","[aws-elasticloadbalancingv2-targets]","[elasticloadbalancingv2-targets]","[elasticloadbalancingv2 targets]","[elbv2 targets]"],"labels":["@aws-cdk/aws-elasticloadbalancingv2-targets"],"assignees":["rix0rrr"]}, {"keywords":["[@aws-cdk/aws-elasticsearch]","[aws-elasticsearch]","[elasticsearch]","[elastic search]","[elastic-search]"],"labels":["@aws-cdk/aws-elasticsearch"],"assignees":["iliapolo"]}, {"keywords":["[@aws-cdk/aws-emr]","[aws-emr]","[emr]"],"labels":["@aws-cdk/aws-emr"],"assignees":["iliapolo"]}, - {"keywords":["[@aws-cdk/aws-events]","[aws-events]","[events]","[eventbridge]","pevent-bridge]"],"labels":["@aws-cdk/aws-events"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-events-targets]","[aws-events-targets]","[events-targets]","[events targets]"],"labels":["@aws-cdk/aws-events-targets"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-eventschemas]","[aws-eventschemas]","[eventschemas]","[event schemas]"],"labels":["@aws-cdk/aws-eventschemas"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-fms]","[aws-fms]","[fms]"],"labels":["@aws-cdk/aws-fms"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-fsx]","[aws-fsx]","[fsx]"],"labels":["@aws-cdk/aws-fsx"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-events]","[aws-events]","[events]","[eventbridge]","pevent-bridge]"],"labels":["@aws-cdk/aws-events"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-events-targets]","[aws-events-targets]","[events-targets]","[events targets]"],"labels":["@aws-cdk/aws-events-targets"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-eventschemas]","[aws-eventschemas]","[eventschemas]","[event schemas]"],"labels":["@aws-cdk/aws-eventschemas"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-fms]","[aws-fms]","[fms]"],"labels":["@aws-cdk/aws-fms"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-fsx]","[aws-fsx]","[fsx]"],"labels":["@aws-cdk/aws-fsx"],"assignees":["rix0rrr"]}, {"keywords":["[@aws-cdk/aws-gamelift]","[aws-gamelift]","[gamelift]","[game lift]"],"labels":["@aws-cdk/aws-gamelift"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["[@aws-cdk/aws-globalaccelerator]","[aws-globalaccelerator]","[globalaccelerator]","[global accelerator]","[global-accelerator]"],"labels":["@aws-cdk/aws-globalaccelerator"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-globalaccelerator]","[aws-globalaccelerator]","[globalaccelerator]","[global accelerator]","[global-accelerator]"],"labels":["@aws-cdk/aws-globalaccelerator"],"assignees":["rix0rrr"]}, {"keywords":["[@aws-cdk/aws-glue]","[aws-glue]","[glue]"],"labels":["@aws-cdk/aws-glue"],"assignees":["iliapolo"]}, {"keywords":["[@aws-cdk/aws-greengrass]","[aws-greengrass]","[greengrass]","[green grass]","[green-grass]"],"labels":["@aws-cdk/aws-greengrass"],"assignees":["shivlaks"]}, - {"keywords":["[@aws-cdk/aws-guardduty]","[aws-guardduty]","[guardduty]","[guard duty]","[guard-duty]"],"labels":["@aws-cdk/aws-guardduty"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-iam]","[aws-iam]","[iam]"],"labels":["@aws-cdk/aws-iam"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-imagebuilder]","[aws-imagebuilder]","[imagebuilder]","[image builder]","[image-builder]"],"labels":["@aws-cdk/aws-imagebuilder"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-inspector]","[aws-inspector]","[inspector]"],"labels":["@aws-cdk/aws-inspector"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-guardduty]","[aws-guardduty]","[guardduty]","[guard duty]","[guard-duty]"],"labels":["@aws-cdk/aws-guardduty"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-iam]","[aws-iam]","[iam]"],"labels":["@aws-cdk/aws-iam"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-imagebuilder]","[aws-imagebuilder]","[imagebuilder]","[image builder]","[image-builder]"],"labels":["@aws-cdk/aws-imagebuilder"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-inspector]","[aws-inspector]","[inspector]"],"labels":["@aws-cdk/aws-inspector"],"assignees":["rix0rrr"]}, {"keywords":["[@aws-cdk/aws-iot]","[aws-iot]","[iot]"],"labels":["@aws-cdk/aws-iot"],"assignees":["shivlaks"]}, {"keywords":["[@aws-cdk/aws-iot1click]","[aws-iot1click]","[iot1click]","[iot 1click]"],"labels":["@aws-cdk/aws-iot1click"],"assignees":["shivlaks"]}, {"keywords":["[@aws-cdk/aws-iotanalytics]","[aws-iotanalytics]","[iotanalytics]","[iot analytics]","[iot-analytics]"],"labels":["@aws-cdk/aws-iotanalytics"],"assignees":["shivlaks"]}, @@ -103,18 +103,19 @@ jobs: {"keywords":["[@aws-cdk/aws-kinesisfirehose]","[aws-kinesisfirehose]","[kinesisfirehose]","[kinesis firehose]","[kinesis-firehose]"],"labels":["@aws-cdk/aws-kinesisfirehose"],"assignees":["iliapolo"]}, {"keywords":["[@aws-cdk/aws-kms]","[aws-kms]","[kms]"],"labels":["@aws-cdk/aws-kms"],"assignees":["njlynch"]}, {"keywords":["[@aws-cdk/aws-lakeformation]","[aws-lakeformation]","[lakeformation]","[lake formation]","[lake-formation]"],"labels":["@aws-cdk/aws-lakeformation"],"assignees":["iliapolo"]}, - {"keywords":["[@aws-cdk/aws-lambda]","[aws-lambda]","[lambda]"],"labels":["@aws-cdk/aws-lambda"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-lambda-event-sources]","[aws-lambda-event-sources]","[lambda-event-sources]","[lambda event sources]"],"labels":["@aws-cdk/aws-lambda-event-sources"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-lambda]","[aws-lambda]","[lambda]"],"labels":["@aws-cdk/aws-lambda"],"assignees":["nija-at"]}, + {"keywords":["[@aws-cdk/aws-lambda-event-sources]","[aws-lambda-event-sources]","[lambda-event-sources]","[lambda event sources]"],"labels":["@aws-cdk/aws-lambda-event-sources"],"assignees":["nija-at"]}, {"keywords":["[@aws-cdk/aws-lambda-nodejs]","[aws-lambda-nodejs]","[lambda-nodejs]","[lambda nodejs]"],"labels":["@aws-cdk/aws-lambda-nodejs"],"assignees":["eladb"]}, - {"keywords":["[@aws-cdk/aws-logs]","[aws-logs]","[logs]"],"labels":["@aws-cdk/aws-logs"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-logs-destinations]","[aws-logs-destinations]","[logs-destinations]","[logs destinations]"],"labels":["@aws-cdk/aws-logs-destinations"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-lambda-python]","[aws-lambda-python]","[lambda-python]","[lambda python]"],"labels":["@aws-cdk/aws-lambda-python"],"assignees":["eladb"]}, + {"keywords":["[@aws-cdk/aws-logs]","[aws-logs]","[logs]"],"labels":["@aws-cdk/aws-logs"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-logs-destinations]","[aws-logs-destinations]","[logs-destinations]","[logs destinations]"],"labels":["@aws-cdk/aws-logs-destinations"],"assignees":["rix0rrr"]}, {"keywords":["[@aws-cdk/aws-managedblockchain]","[aws-managedblockchain]","[managedblockchain]","[managed blockchain]","[managed-blockchain]"],"labels":["@aws-cdk/aws-managedblockchain"],"assignees":["shivlaks"]}, - {"keywords":["[@aws-cdk/aws-mediaconvert]","[aws-mediaconvert]","[mediaconvert]","[media convert]","[media-convert]"],"labels":["@aws-cdk/aws-mediaconvert"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-medialive]","[aws-medialive]","[medialive]","[media live]","[media-live]"],"labels":["@aws-cdk/aws-medialive"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-mediastore]","[aws-mediastore]","[mediastore]","[media store]","[media-store]"],"labels":["@aws-cdk/aws-mediastore"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-mediaconvert]","[aws-mediaconvert]","[mediaconvert]","[media convert]","[media-convert]"],"labels":["@aws-cdk/aws-mediaconvert"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-medialive]","[aws-medialive]","[medialive]","[media live]","[media-live]"],"labels":["@aws-cdk/aws-medialive"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-mediastore]","[aws-mediastore]","[mediastore]","[media store]","[media-store]"],"labels":["@aws-cdk/aws-mediastore"],"assignees":["rix0rrr"]}, {"keywords":["[@aws-cdk/aws-msk]","[aws-msk]","[msk]"],"labels":["@aws-cdk/aws-msk"],"assignees":["iliapolo"]}, - {"keywords":["[@aws-cdk/aws-neptune]","[aws-neptune]","[neptune]"],"labels":["@aws-cdk/aws-neptune"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-networkmanager]","[aws-networkmanager]","[networkmanager]","[network manager]","[network-manager]"],"labels":["@aws-cdk/aws-networkmanager"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-neptune]","[aws-neptune]","[neptune]"],"labels":["@aws-cdk/aws-neptune"],"assignees":["nija-at"]}, + {"keywords":["[@aws-cdk/aws-networkmanager]","[aws-networkmanager]","[networkmanager]","[network manager]","[network-manager]"],"labels":["@aws-cdk/aws-networkmanager"],"assignees":["rix0rrr"]}, {"keywords":["[@aws-cdk/aws-opsworks]","[aws-opsworks]","[opsworks]","[ops works]","[ops-works]"],"labels":["@aws-cdk/aws-opsworks"],"assignees":["MrArnoldPalmer"]}, {"keywords":["[@aws-cdk/aws-opsworkscm]","[aws-opsworkscm]","[opsworkscm]","[opsworks cm]","[opsworks-cm]"],"labels":["@aws-cdk/aws-opsworkscm"],"assignees":["MrArnoldPalmer"]}, {"keywords":["[@aws-cdk/aws-personalize]","[aws-personalize]","[personalize]"],"labels":["@aws-cdk/aws-personalize"],"assignees":["NetaNir"]}, @@ -123,7 +124,7 @@ jobs: {"keywords":["[@aws-cdk/aws-qldb]","[aws-qldb]","[qldb]"],"labels":["@aws-cdk/aws-qldb"],"assignees":["shivlaks"]}, {"keywords":["[@aws-cdk/aws-ram]","[aws-ram]","[ram]"],"labels":["@aws-cdk/aws-ram"],"assignees":["MrArnoldPalmer"]}, {"keywords":["[@aws-cdk/aws-rds]","[aws-rds]","[rds]"],"labels":["@aws-cdk/aws-rds"],"assignees":["skinny85"]}, - {"keywords":["[@aws-cdk/aws-redshift]","[aws-redshift]","[redshift]","[red shift]","[red-shift]"],"labels":["@aws-cdk/aws-redshift"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-redshift]","[aws-redshift]","[redshift]","[red shift]","[red-shift]"],"labels":["@aws-cdk/aws-redshift"],"assignees":["nija-at"]}, {"keywords":["[@aws-cdk/aws-resourcegroups]","[aws-resourcegroups]","[resourcegroups]","[resource groups]","[resource-groups]"],"labels":["@aws-cdk/aws-resourcegroups"],"assignees":["MrArnoldPalmer"]}, {"keywords":["[@aws-cdk/aws-robomaker]","[aws-robomaker]","[robomaker]","[robo maker]","[robo-maker]"],"labels":["@aws-cdk/aws-robomaker"],"assignees":["NetaNir"]}, {"keywords":["[@aws-cdk/aws-route53]","[aws-route53]","[route53]","[route 53]","[route-53]"],"labels":["@aws-cdk/aws-route53"],"assignees":["shivlaks"]}, @@ -135,10 +136,10 @@ jobs: {"keywords":["[@aws-cdk/aws-s3-deployment]","[aws-s3-deployment]","[s3-deployment]","[s3 deployment]"],"labels":["@aws-cdk/aws-s3-deployment"],"assignees":["iliapolo"]}, {"keywords":["[@aws-cdk/aws-s3-notifications]","[aws-s3-notifications]","[s3-notifications]","[s3 notifications]"],"labels":["@aws-cdk/aws-s3-notifications"],"assignees":["iliapolo"]}, {"keywords":["[@aws-cdk/aws-sagemaker]","[aws-sagemaker]","[sagemaker]","[sage maker]","[sage-maker]"],"labels":["@aws-cdk/aws-sagemaker"],"assignees":["NetaNir"]}, - {"keywords":["[@aws-cdk/aws-sam]","[aws-sam]","[sam]"],"labels":["@aws-cdk/aws-sam"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-sdb]","[aws-sdb]","[sdb]"],"labels":["@aws-cdk/aws-sdb"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-sam]","[aws-sam]","[sam]"],"labels":["@aws-cdk/aws-sam"],"assignees":["nija-at"]}, + {"keywords":["[@aws-cdk/aws-sdb]","[aws-sdb]","[sdb]"],"labels":["@aws-cdk/aws-sdb"],"assignees":["nija-at"]}, {"keywords":["[@aws-cdk/aws-secretsmanager]","[aws-secretsmanager]","[secretsmanager]","[secrets manager]","[secrets-manager]"],"labels":["@aws-cdk/aws-secretsmanager"],"assignees":["njlynch"]}, - {"keywords":["[@aws-cdk/aws-securityhub]","[aws-securityhub]","[securityhub]","[security hub]","[security-hub]"],"labels":["@aws-cdk/aws-securityhub"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-securityhub]","[aws-securityhub]","[securityhub]","[security hub]","[security-hub]"],"labels":["@aws-cdk/aws-securityhub"],"assignees":["rix0rrr"]}, {"keywords":["[@aws-cdk/aws-servicecatalog]","[aws-servicecatalog]","[servicecatalog]","[service catalog]","[service-catalog]"],"labels":["@aws-cdk/aws-servicecatalog"],"assignees":["MrArnoldPalmer"]}, {"keywords":["[@aws-cdk/aws-servicediscovery]","[aws-servicediscovery]","[servicediscovery]","[service discovery]","[service-discovery]"],"labels":["@aws-cdk/aws-servicediscovery"],"assignees":["MrArnoldPalmer"]}, {"keywords":["[@aws-cdk/aws-ses]","[aws-ses]","[ses]"],"labels":["@aws-cdk/aws-ses"],"assignees":["iliapolo"]}, @@ -151,9 +152,9 @@ jobs: {"keywords":["[@aws-cdk/aws-stepfunctions-tasks]","[aws-stepfunctions-tasks]","[stepfunctions-tasks]","[stepfunctions tasks]"],"labels":["@aws-cdk/aws-stepfunctions-tasks"],"assignees":["shivlaks"]}, {"keywords":["[@aws-cdk/aws-synthetics]","[aws-synthetics]","[synthetics]"],"labels":["@aws-cdk/aws-synthetics"],"assignees":["NetaNir"]}, {"keywords":["[@aws-cdk/aws-transfer]","[aws-transfer]","[transfer]"],"labels":["@aws-cdk/aws-transfer"],"assignees":["iliapolo"]}, - {"keywords":["[@aws-cdk/aws-waf]","[aws-waf]","[waf]"],"labels":["@aws-cdk/aws-waf"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-wafregional]","[aws-wafregional]","[wafregional]","[waf regional]","[waf-regional]"],"labels":["@aws-cdk/aws-wafregional"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/aws-wafv2]","[aws-wafv2]","[wafv2]","[waf v2]","[waf-v2]"],"labels":["@aws-cdk/aws-wafv2"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-waf]","[aws-waf]","[waf]"],"labels":["@aws-cdk/aws-waf"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-wafregional]","[aws-wafregional]","[wafregional]","[waf regional]","[waf-regional]"],"labels":["@aws-cdk/aws-wafregional"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-wafv2]","[aws-wafv2]","[wafv2]","[waf v2]","[waf-v2]"],"labels":["@aws-cdk/aws-wafv2"],"assignees":["rix0rrr"]}, {"keywords":["[@aws-cdk/aws-workspaces]","[aws-workspaces]","[workspaces]"],"labels":["@aws-cdk/aws-workspaces"],"assignees":["NetaNir"]}, {"keywords":["[@aws-cdk/cfnspec]","[cfnspec]","[cfn spec]","[cfn-spec]"],"labels":["@aws-cdk/cfnspec"],"assignees":["eladb"]}, {"keywords":["[@aws-cdk/cloud-assembly-schema]","[cloud-assembly-schema]","[cloud assembly schema]"],"labels":["@aws-cdk/cloud-assembly-schema"],"assignees":["eladb"]}, @@ -163,7 +164,7 @@ jobs: {"keywords":["[@aws-cdk/custom-resources]","[custom-resources]","[custom resources]"],"labels":["@aws-cdk/custom-resources"],"assignees":["eladb"]}, {"keywords":["[@aws-cdk/cx-api]","[cx-api]","[cx api]"],"labels":["@aws-cdk/cx-api"],"assignees":["eladb"]}, {"keywords":["[@aws-cdk/region-info]","[region-info]","[region info]"],"labels":["@aws-cdk/region-info"],"assignees":["RomainMuller"]}, - {"keywords":["[@aws-cdk/aws-macie]","[aws-macie]","[macie]"],"labels":["@aws-cdk/aws-macie"],"assignees":["ericzbeard"]}, - {"keywords":["[@aws-cdk/pipelines]","[pipelines]","[cdk pipelines]","[cdk-pipelines]"],"labels":["@aws-cdk/pipelines"],"assignees":["ericzbeard"]} + {"keywords":["[@aws-cdk/aws-macie]","[aws-macie]","[macie]"],"labels":["@aws-cdk/aws-macie"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/pipelines]","[pipelines]","[cdk pipelines]","[cdk-pipelines]"],"labels":["@aws-cdk/pipelines"],"assignees":["rix0rrr"]} ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b54895aee553..4e69649d4564b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,59 @@ 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.57.0](https://github.com/aws/aws-cdk/compare/v1.56.0...v1.57.0) (2020-08-07) + + +### ⚠ BREAKING CHANGES + +* **apigatewayv2:** The parameter for the method `bind()` on +`IHttpRouteIntegration` has changed to accept one of type +`HttpRouteIntegrationBindOptions`. The previous parameter +`IHttpRoute` is now a property inside the new parameter under +the key `route`. +* **eks:** The experimental `eks.Cluster` construct no longer supports setting `kubectlEnabled: false`. A temporary drop-in alternative is `eks.LegacyCluster`, but we have plans to completely remove support for it in an upcoming release since `eks.Cluster` has matured and should provide all the needed capabilities. Please comment on https://github.com/aws/aws-cdk/issues/9332 if there are use cases that are not supported by `eks.Cluster`. +* **eks:** endpoint access is configured to private and public by default instead of just public +* `lambda.Version` and `apigateway.Deployment` resources with auto-generated IDs will be replaced as we fixed a bug which ignored resource dependencies when generating these logical IDs. +* **core:** in unit tests, the `node.path` of constructs within stacks created the root of the tree via `new Stack()` will now have a prefix `Default/` which represents an implicit `App` root. + +Related: https://github.com/aws/aws-cdk-rfcs/issues/192 +* **cloudfront:** the property OriginBase.originId has been removed + +### Features + +* **apigateway:** additionalProperties in RestApi Model supports JsonSchema type ([#8848](https://github.com/aws/aws-cdk/issues/8848)) ([5e087e5](https://github.com/aws/aws-cdk/commit/5e087e5f3d59f931ceabebb290536a93b170522c)), closes [#8069](https://github.com/aws/aws-cdk/issues/8069) +* **apigateway:** configure endpoint types on SpecRestApi ([#9068](https://github.com/aws/aws-cdk/issues/9068)) ([7673e48](https://github.com/aws/aws-cdk/commit/7673e487e6358d1b345a138f016ac38b33315e4b)), closes [#9060](https://github.com/aws/aws-cdk/issues/9060) +* **apigateway:** import API keys ([#9155](https://github.com/aws/aws-cdk/issues/9155)) ([e3f6ae3](https://github.com/aws/aws-cdk/commit/e3f6ae3078799d3ff1c3a2f4a4ec19a82652b3e2)), closes [#8367](https://github.com/aws/aws-cdk/issues/8367) +* **appsync:** add x-ray parameter to AppSync ([#9389](https://github.com/aws/aws-cdk/issues/9389)) ([51921ad](https://github.com/aws/aws-cdk/commit/51921ade45840737f554dad066abfbbfc3b822b6)) +* **cloudfront:** add support for Origin Groups ([#9360](https://github.com/aws/aws-cdk/issues/9360)) ([11e146c](https://github.com/aws/aws-cdk/commit/11e146cb330ae036920c5cc1ab74225c0775a695)), closes [#9109](https://github.com/aws/aws-cdk/issues/9109) +* **cloudfront:** Behaviors support cached methods, compression, viewer protocol, and smooth streaming ([#9411](https://github.com/aws/aws-cdk/issues/9411)) ([2451fa9](https://github.com/aws/aws-cdk/commit/2451fa96f6a623b0634ba249bf6cc2a38da1dbbf)), closes [#7086](https://github.com/aws/aws-cdk/issues/7086) [#9107](https://github.com/aws/aws-cdk/issues/9107) +* **core:** implicit app for root stacks ([#9342](https://github.com/aws/aws-cdk/issues/9342)) ([1d85a9f](https://github.com/aws/aws-cdk/commit/1d85a9f16c87f51440ffbddd854aa5410b69fac7)) +* **core:** warn if an aspect was added via another aspect ([#8639](https://github.com/aws/aws-cdk/issues/8639)) ([9d7bef7](https://github.com/aws/aws-cdk/commit/9d7bef797f296c3e9f6f5dac6a4edf3139c2dfe2)) +* **eks:** default masters role ([#9464](https://github.com/aws/aws-cdk/issues/9464)) ([b80c271](https://github.com/aws/aws-cdk/commit/b80c2718055a19a72955e457397d6e812a21e53e)), closes [#9463](https://github.com/aws/aws-cdk/issues/9463) +* **eks:** deprecate "kubectlEnabled: false" ([#9454](https://github.com/aws/aws-cdk/issues/9454)) ([2791017](https://github.com/aws/aws-cdk/commit/27910175560f4e354aebab86e338b6a9190db4a5)), closes [#9332](https://github.com/aws/aws-cdk/issues/9332) +* **eks:** endpoint access customization ([#9095](https://github.com/aws/aws-cdk/issues/9095)) ([692864c](https://github.com/aws/aws-cdk/commit/692864cf4659ba84fdec9d8a298c185679076d38)), closes [#5220](https://github.com/aws/aws-cdk/issues/5220) [/github.com/aws/aws-cdk/pull/9095#issuecomment-665621701](https://github.com/aws//github.com/aws/aws-cdk/pull/9095/issues/issuecomment-665621701) +* **s3:** Introduce S3 Inventory ([#9102](https://github.com/aws/aws-cdk/issues/9102)) ([b0f359e](https://github.com/aws/aws-cdk/commit/b0f359eee99c100e6d33e00388c1a4bffe7baa6c)) + + +### Bug Fixes + +* **apigatewayv2:** cyclic dependency between HttpApi and the lambda function ([#9100](https://github.com/aws/aws-cdk/issues/9100)) ([7b29774](https://github.com/aws/aws-cdk/commit/7b297749bbe5d75f29f1aeb2652d095e3f2630e1)), closes [#9075](https://github.com/aws/aws-cdk/issues/9075) +* **athena:** WorkGroup tags corruption ([#9085](https://github.com/aws/aws-cdk/issues/9085)) ([b688913](https://github.com/aws/aws-cdk/commit/b688913b7534867c4cb584e491bf6e89437b48d9)), closes [#6936](https://github.com/aws/aws-cdk/issues/6936) +* **aws-lambda-python:** use cp instead of rsync ([#9355](https://github.com/aws/aws-cdk/issues/9355)) ([056bcaf](https://github.com/aws/aws-cdk/commit/056bcafa99aa4b741bf1e1d075fe8ab188c99c34)), closes [#9349](https://github.com/aws/aws-cdk/issues/9349) +* **cfn-include:** fails to load SAM resources ([#9442](https://github.com/aws/aws-cdk/issues/9442)) ([1de9dc8](https://github.com/aws/aws-cdk/commit/1de9dc86a7990e8bd7c026bde59a02ecf0582616)) +* **cfn-include:** no longer concatenate elements of Fn::Join without tokens ([#9476](https://github.com/aws/aws-cdk/issues/9476)) ([d038b61](https://github.com/aws/aws-cdk/commit/d038b61cd9b015b231911d4aaac131080b8b7b7c)) +* **core:** can't have multiple CfnRules in a Stack ([#9500](https://github.com/aws/aws-cdk/issues/9500)) ([76a7bfd](https://github.com/aws/aws-cdk/commit/76a7bfdf95c48a8d924d9363da2913240a5326f9)), closes [#8251](https://github.com/aws/aws-cdk/issues/8251) [#9485](https://github.com/aws/aws-cdk/issues/9485) +* **core:** docs for CfnMapping are not clear ([#9451](https://github.com/aws/aws-cdk/issues/9451)) ([c1e3c57](https://github.com/aws/aws-cdk/commit/c1e3c575ba67c0bf6d9fbea443fb1c80bcce7d67)), closes [#9432](https://github.com/aws/aws-cdk/issues/9432) +* **dynamodb:** allow using PhysicalName.GENERATE_IF_NEEDED as the Table name ([#9377](https://github.com/aws/aws-cdk/issues/9377)) ([8ab7b10](https://github.com/aws/aws-cdk/commit/8ab7b1062416adce1f2423c558bd3bfd714c5590)), closes [#9374](https://github.com/aws/aws-cdk/issues/9374) +* **ecs:** Scope-down IAM permissions for ECS drain ([#9502](https://github.com/aws/aws-cdk/issues/9502)) ([9fbeec3](https://github.com/aws/aws-cdk/commit/9fbeec3d7fe73ec870fe2de0e122b7714165f70e)) +* **ecs:** Scope-down IAM permissions on Cluster ASG ([#9493](https://github.com/aws/aws-cdk/issues/9493)) ([1670289](https://github.com/aws/aws-cdk/commit/16702898feacfe4f8c5ec323205362d6a0e36a97)) +* **ecs-patterns:** Adds missing option to secure ingress of ALB in Ap… ([#9434](https://github.com/aws/aws-cdk/issues/9434)) ([ba1427f](https://github.com/aws/aws-cdk/commit/ba1427f8510bc5c123012f6cfa1ca55d456efba7)) +* **lambda:** bundling docker image does not exist for Go runtime ([#9465](https://github.com/aws/aws-cdk/issues/9465)) ([7666d9b](https://github.com/aws/aws-cdk/commit/7666d9ba6b9a1212796636840fb7a1dffe41e4f3)), closes [#9435](https://github.com/aws/aws-cdk/issues/9435) + + +* **cloudfront:** remove the originId property from OriginBase ([#9380](https://github.com/aws/aws-cdk/issues/9380)) ([70b9f63](https://github.com/aws/aws-cdk/commit/70b9f63fa979c8c1d74ecdbd1f3c5bd248c5715f)) +* do not use "synthesize" and "prepare" in the cdk ([#9410](https://github.com/aws/aws-cdk/issues/9410)) ([e3ae645](https://github.com/aws/aws-cdk/commit/e3ae645f636a9f08566435799b7f55d50f5298bb)), closes [/github.com/aws/aws-cdk/pull/9410#issuecomment-668552361](https://github.com/aws//github.com/aws/aws-cdk/pull/9410/issues/issuecomment-668552361) + ## [1.56.0](https://github.com/aws/aws-cdk/compare/v1.55.0...v1.56.0) (2020-07-31) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bcf045935ed0e..ef73dba94c39d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -364,7 +364,7 @@ part of the build of all AWS modules in the project and enforces the [AWS Construct Library Design Guidelines](./DESIGN_GUIDELINES.md). For more information about this tool, see the [awslint -README](./tools/awslint/README.md). +README](./packages/awslint/README.md). Generally speaking, if you make any changes which violate an awslint rule, build will fail with appropriate messages. All rules are documented and explained in @@ -377,7 +377,7 @@ Here are a few useful commands: * `scripts/foreach.sh yarn awslint` will start linting the entire repo, progressively. Rerun `scripts/foreach.sh` after fixing to continue. * `lerna run awslint --no-bail --stream 2> awslint.txt` will run __awslint__ in all modules and collect all results into awslint.txt * `lerna run awslint -- -i ` will run awslint throughout the repo and - evaluate only the rule specified [awslint README](./tools/awslint/README.md) + evaluate only the rule specified [awslint README](./packages/awslint/README.md) for details on include/exclude rule patterns. ### cfn2ts diff --git a/lerna.json b/lerna.json index 855e51ed977ca..96e8ea36e52af 100644 --- a/lerna.json +++ b/lerna.json @@ -10,5 +10,5 @@ "tools/*" ], "rejectCycles": "true", - "version": "1.56.0" + "version": "1.57.0" } diff --git a/packages/@aws-cdk/assert/lib/synth-utils.ts b/packages/@aws-cdk/assert/lib/synth-utils.ts index b66216362d363..bb8d9a437afd9 100644 --- a/packages/@aws-cdk/assert/lib/synth-utils.ts +++ b/packages/@aws-cdk/assert/lib/synth-utils.ts @@ -4,13 +4,12 @@ import * as core from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; export class SynthUtils { + /** + * Returns the cloud assembly template artifact for a stack. + */ public static synthesize(stack: core.Stack, options: core.SynthesisOptions = { }): cxapi.CloudFormationStackArtifact { // always synthesize against the root (be it an App or whatever) so all artifacts will be included - const root = stack.node.root; - - // if the root is an app, invoke "synth" to avoid double synthesis - const assembly = root instanceof core.App ? root.synth() : core.ConstructNode.synth(root.node, options); - + const assembly = synthesizeApp(stack, options); return assembly.getStackArtifact(stack.artifactId); } @@ -51,10 +50,7 @@ export class SynthUtils { */ public static _synthesizeWithNested(stack: core.Stack, options: core.SynthesisOptions = { }): cxapi.CloudFormationStackArtifact | object { // always synthesize against the root (be it an App or whatever) so all artifacts will be included - const root = stack.node.root; - - // if the root is an app, invoke "synth" to avoid double synthesis - const assembly = root instanceof core.App ? root.synth() : core.ConstructNode.synth(root.node, options); + const assembly = synthesizeApp(stack, options); // if this is a nested stack (it has a parent), then just read the template as a string if (stack.nestedStackParent) { @@ -65,6 +61,24 @@ export class SynthUtils { } } +/** + * Synthesizes the app in which a stack resides and returns the cloud assembly object. + */ +function synthesizeApp(stack: core.Stack, options: core.SynthesisOptions) { + const root = stack.node.root; + if (!core.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; + + return root.synth({ + force, + ...options, + }); +} + export interface SubsetOptions { /** * Match all resources of the given type diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 89d2d721dc849..852801b79aa5d 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -38,6 +38,7 @@ running on AWS Lambda, or any web application. - [Private Integrations](#private-integrations) - [Gateway Response](#gateway-response) - [OpenAPI Definition](#openapi-definition) + - [Endpoint configuration](#endpoint-configuration) - [APIGateway v2](#apigateway-v2) ## Defining APIs @@ -200,6 +201,12 @@ const key = api.addApiKey('ApiKey', { }); ``` +Existing API keys can also be imported into a CDK app using its id. + +```ts +const importedKey = ApiKey.fromApiKeyId(this, 'imported-key', ''); +``` + In scenarios where you need to create a single api key and configure rate limiting for it, you can use `RateLimitedApiKey`. This construct lets you specify rate limiting properties which should be applied only to the api key being created. The API key created has the specified rate limits, such as quota and throttles, applied. @@ -986,11 +993,29 @@ to configure these. There are a number of limitations in using OpenAPI definitions in API Gateway. Read the [Amazon API Gateway important notes for REST APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html#api-gateway-known-issues-rest-apis) for more details. - + **Note:** When starting off with an OpenAPI definition using `SpecRestApi`, it is not possible to configure some properties that can be configured directly in the OpenAPI specification file. This is to prevent people duplication of these properties and potential confusion. +### Endpoint configuration + +By default, `SpecRestApi` will create an edge optimized endpoint. + +This can be modified as shown below: + +```ts +const api = new apigateway.SpecRestApi(this, 'ExampleRestApi', { + // ... + endpointTypes: [apigateway.EndpointType.PRIVATE] +}); +``` + +**Note:** For private endpoints you will still need to provide the +[`x-amazon-apigateway-policy`](https://docs.aws.amazon.com/apigateway/latest/developerguide/openapi-extensions-policy.html) and +[`x-amazon-apigateway-endpoint-configuration`](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-endpoint-configuration.html) +in your openApi file. + ## APIGateway v2 APIGateway v2 APIs are now moved to its own package named `aws-apigatewayv2`. For backwards compatibility, existing diff --git a/packages/@aws-cdk/aws-apigateway/lib/api-key.ts b/packages/@aws-cdk/aws-apigateway/lib/api-key.ts index 172b77aa40309..dc705e73c939e 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/api-key.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/api-key.ts @@ -81,6 +81,18 @@ export interface ApiKeyProps extends ApiKeyOptions { * for Method resources that require an Api Key. */ export class ApiKey extends Resource implements IApiKey { + + /** + * Import an ApiKey by its Id + */ + public static fromApiKeyId(scope: Construct, id: string, apiKeyId: string): IApiKey { + class Import extends Resource implements IApiKey { + public keyId = apiKeyId; + } + + return new Import(scope, id); + } + public readonly keyId: string; constructor(scope: Construct, id: string, props: ApiKeyProps = { }) { diff --git a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts index c72dba724f878..404ba1855bd68 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -1,7 +1,8 @@ -import { CfnResource, Construct, Lazy, RemovalPolicy, Resource, Stack } from '@aws-cdk/core'; import * as crypto from 'crypto'; +import { Construct, Lazy, RemovalPolicy, Resource, CfnResource } from '@aws-cdk/core'; import { CfnDeployment } from './apigateway.generated'; -import { IRestApi, RestApi, SpecRestApi } from './restapi'; +import { IRestApi, RestApi, SpecRestApi, RestApiBase } from './restapi'; +import { Method } from './method'; export interface DeploymentProps { /** @@ -77,6 +78,10 @@ export class Deployment extends Resource { this.api = props.api; this.deploymentId = Lazy.stringValue({ produce: () => this.resource.ref }); + + if (props.api instanceof RestApiBase) { + props.api._attachDeployment(this); + } } /** @@ -92,27 +97,28 @@ export class Deployment extends Resource { } /** - * Hook into synthesis before it occurs and make any final adjustments. + * Quoting from CloudFormation's docs: + * + * If you create an AWS::ApiGateway::RestApi resource and its methods (using + * AWS::ApiGateway::Method) in the same template as your deployment, the + * deployment must depend on the RestApi's methods. To create a dependency, + * add a DependsOn attribute to the deployment. If you don't, AWS + * CloudFormation creates the deployment right after it creates the RestApi + * resource that doesn't contain any methods, and AWS CloudFormation + * encounters the following error: The REST API doesn't contain any methods. + * + * @param method The method to add as a dependency of the deployment + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-deployment.html + * @see https://github.com/aws/aws-cdk/pull/6165 + * @internal */ - protected prepare() { - if (this.api instanceof RestApi) { - // Ignore IRestApi that are imported - - /* - * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-deployment.html - * Quoting from CloudFormation's docs - "If you create an AWS::ApiGateway::RestApi resource and its methods (using AWS::ApiGateway::Method) in - * the same template as your deployment, the deployment must depend on the RestApi's methods. To create a dependency, add a DependsOn attribute - * to the deployment. If you don't, AWS CloudFormation creates the deployment right after it creates the RestApi resource that doesn't contain - * any methods, and AWS CloudFormation encounters the following error: The REST API doesn't contain any methods." - */ - - /* - * Adding a dependency between LatestDeployment and Method construct, using ConstructNode.addDependencies(), creates additional dependencies - * between AWS::ApiGateway::Deployment and the AWS::Lambda::Permission nodes (children under Method), causing cyclic dependency errors. Hence, - * falling back to declaring dependencies between the underlying CfnResources. - */ - this.api.methods.map(m => m.node.defaultChild as CfnResource).forEach(m => this.resource.addDependsOn(m)); - } + public _addMethodDependency(method: Method) { + // adding a dependency between the constructs using `node.addDependency()` + // will create additional dependencies between `AWS::ApiGateway::Deployment` + // and the `AWS::Lambda::Permission` resources (children under Method), + // causing cyclic dependency errors. Hence, falling back to declaring + // dependencies between the underlying CfnResources. + this.node.addDependency(method.node.defaultChild as CfnResource); } } @@ -122,9 +128,9 @@ interface LatestDeploymentResourceProps { } class LatestDeploymentResource extends CfnDeployment { - private hashComponents = new Array(); - private originalLogicalId: string; - private api: IRestApi; + private readonly hashComponents = new Array(); + private readonly originalLogicalId: string; + private readonly api: IRestApi; constructor(scope: Construct, id: string, props: LatestDeploymentResourceProps) { super(scope, id, { @@ -133,7 +139,8 @@ class LatestDeploymentResource extends CfnDeployment { }); this.api = props.restApi; - this.originalLogicalId = Stack.of(this).getLogicalId(this); + this.originalLogicalId = this.stack.getLogicalId(this); + this.overrideLogicalId(Lazy.stringValue({ produce: () => this.calculateLogicalId() })); } /** @@ -150,27 +157,26 @@ class LatestDeploymentResource extends CfnDeployment { this.hashComponents.push(data); } - /** - * Hooks into synthesis to calculate a logical ID that hashes all the components - * add via `addToLogicalId`. - */ - protected prepare() { + private calculateLogicalId() { + const hash = [ ...this.hashComponents ]; + if (this.api instanceof RestApi || this.api instanceof SpecRestApi) { // Ignore IRestApi that are imported // Add CfnRestApi to the logical id so a new deployment is triggered when any of its properties change. const cfnRestApiCF = (this.api.node.defaultChild as any)._toCloudFormation(); - this.addToLogicalId(Stack.of(this).resolve(cfnRestApiCF)); + hash.push(this.stack.resolve(cfnRestApiCF)); } - const stack = Stack.of(this); + let lid = this.originalLogicalId; // if hash components were added to the deployment, we use them to calculate // a logical ID for the deployment resource. - if (this.hashComponents.length > 0) { + if (hash.length > 0) { const md5 = crypto.createHash('md5'); - this.hashComponents.map(x => stack.resolve(x)).forEach(c => md5.update(JSON.stringify(c))); - this.overrideLogicalId(this.originalLogicalId + md5.digest('hex')); + hash.map(x => this.stack.resolve(x)).forEach(c => md5.update(JSON.stringify(c))); + lid += md5.digest('hex'); } - super.prepare(); + + return lid; } } diff --git a/packages/@aws-cdk/aws-apigateway/lib/json-schema.ts b/packages/@aws-cdk/aws-apigateway/lib/json-schema.ts index 7ef0577566a29..b320031edf499 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/json-schema.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/json-schema.ts @@ -63,7 +63,7 @@ export interface JsonSchema { readonly minProperties?: number; readonly required?: string[]; readonly properties?: { [name: string]: JsonSchema }; - readonly additionalProperties?: boolean; + readonly additionalProperties?: JsonSchema | boolean; readonly patternProperties?: { [name: string]: JsonSchema }; readonly dependencies?: { [name: string]: JsonSchema | string[] }; readonly propertyNames?: JsonSchema; diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index cedf43099856e..f027a2f7424be 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -159,6 +159,14 @@ export interface RestApiBaseProps { * @default - when no export name is given, output will be created without export */ readonly endpointExportName?: string; + + /** + * A list of the endpoint types of the API. Use this property when creating + * an API. + * + * @default EndpointType.EDGE + */ + readonly endpointTypes?: EndpointType[]; } /** @@ -218,18 +226,9 @@ export interface RestApiProps extends RestApiOptions { * The EndpointConfiguration property type specifies the endpoint types of a REST API * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-restapi-endpointconfiguration.html * - * @default - No endpoint configuration + * @default EndpointType.EDGE */ readonly endpointConfiguration?: EndpointConfiguration; - - /** - * A list of the endpoint types of the API. Use this property when creating - * an API. - * - * @default - No endpoint types. - * @deprecated this property is deprecated, use endpointConfiguration instead - */ - readonly endpointTypes?: EndpointType[]; } /** @@ -248,7 +247,6 @@ export interface SpecRestApiProps extends RestApiBaseProps { * Base implementation that are common to various implementations of IRestApi */ export abstract class RestApiBase extends Resource implements IRestApi { - /** * Checks if the given object is an instance of RestApiBase. * @internal @@ -384,6 +382,15 @@ export abstract class RestApiBase extends Resource implements IRestApi { ignore(method); } + /** + * Associates a Deployment resource with this REST API. + * + * @internal + */ + public _attachDeployment(deployment: Deployment) { + ignore(deployment); + } + protected configureCloudWatchRole(apiResource: CfnRestApi) { const role = new iam.Role(this, 'CloudWatchRole', { assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), @@ -423,6 +430,25 @@ export abstract class RestApiBase extends Resource implements IRestApi { } } } + + /** + * @internal + */ + protected _configureEndpoints(props: RestApiProps): CfnRestApi.EndpointConfigurationProperty | undefined { + if (props.endpointTypes && props.endpointConfiguration) { + throw new Error('Only one of the RestApi props, endpointTypes or endpointConfiguration, is allowed'); + } + if (props.endpointConfiguration) { + return { + types: props.endpointConfiguration.types, + vpcEndpointIds: props.endpointConfiguration?.vpcEndpoints?.map(vpcEndpoint => vpcEndpoint.vpcEndpointId), + }; + } + if (props.endpointTypes) { + return { types: props.endpointTypes }; + } + return undefined; + } } /** @@ -463,6 +489,7 @@ export class SpecRestApi extends RestApiBase { failOnWarnings: props.failOnWarnings, body: apiDefConfig.inlineDefinition ? apiDefConfig.inlineDefinition : undefined, bodyS3Location: apiDefConfig.inlineDefinition ? undefined : apiDefConfig.s3Location, + endpointConfiguration: this._configureEndpoints(props), parameters: props.parameters, }); this.node.defaultChild = resource; @@ -550,6 +577,11 @@ export class RestApi extends RestApiBase { */ public readonly methods = new Array(); + /** + * This list of deployments bound to this RestApi + */ + private readonly deployments = new Array(); + constructor(scope: Construct, id: string, props: RestApiProps = { }) { super(scope, id, props); @@ -560,7 +592,7 @@ export class RestApi extends RestApiBase { failOnWarnings: props.failOnWarnings, minimumCompressionSize: props.minimumCompressionSize, binaryMediaTypes: props.binaryMediaTypes, - endpointConfiguration: this.configureEndpoints(props), + endpointConfiguration: this._configureEndpoints(props), apiKeySourceType: props.apiKeySourceType, cloneFrom: props.cloneFrom ? props.cloneFrom.restApiId : undefined, parameters: props.parameters, @@ -627,6 +659,29 @@ export class RestApi extends RestApiBase { */ public _attachMethod(method: Method) { this.methods.push(method); + + // add this method as a dependency to all deployments defined for this api + // when additional deployments are added, _attachDeployment is called and + // this method will be added there. + for (const dep of this.deployments) { + dep._addMethodDependency(method); + } + } + + /** + * Attaches a deployment to this REST API. + * + * @internal + */ + public _attachDeployment(deployment: Deployment) { + this.deployments.push(deployment); + + // add all methods that were already defined as dependencies of this + // deployment when additional methods are added, _attachMethod is called and + // it will be added as a dependency to this deployment. + for (const method of this.methods) { + deployment._addMethodDependency(method); + } } /** @@ -639,22 +694,6 @@ export class RestApi extends RestApiBase { return []; } - - private configureEndpoints(props: RestApiProps): CfnRestApi.EndpointConfigurationProperty | undefined { - if (props.endpointTypes && props.endpointConfiguration) { - throw new Error('Only one of the RestApi props, endpointTypes or endpointConfiguration, is allowed'); - } - if (props.endpointConfiguration) { - return { - types: props.endpointConfiguration.types, - vpcEndpointIds: props.endpointConfiguration?.vpcEndpoints?.map(vpcEndpoint => vpcEndpoint.vpcEndpointId), - }; - } - if (props.endpointTypes) { - return { types: props.endpointTypes }; - } - return undefined; - } } /** @@ -666,7 +705,7 @@ export interface EndpointConfiguration { /** * A list of endpoint types of an API or its custom domain name. * - * @default - no endpoint types. + * @default EndpointType.EDGE */ readonly types: EndpointType[]; @@ -752,4 +791,4 @@ class RootResource extends ResourceBase { function ignore(_x: any) { return; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index 6816f6cc02ab7..668185ab89589 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -115,7 +115,6 @@ "from-method:@aws-cdk/aws-apigateway.Resource", "duration-prop-type:@aws-cdk/aws-apigateway.QuotaSettings.period", "duration-prop-type:@aws-cdk/aws-apigateway.ResponseType.INTEGRATION_TIMEOUT", - "from-method:@aws-cdk/aws-apigateway.ApiKey", "ref-via-interface:@aws-cdk/aws-apigateway.ApiKeyProps.resources", "props-physical-name:@aws-cdk/aws-apigateway.DeploymentProps", "props-physical-name:@aws-cdk/aws-apigateway.MethodProps", diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.expected.json index 53cfbe6135685..7379b8f5ab47b 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameterse7d4044be8659ef3bb40a53a69846d7ca1b2d8e4e4bd36ad8c9d8e69fe3b68a0S3Bucket316B0B52" + "Ref": "AssetParameters3dc8c5549b88fef617feef923524902b3650973ae1159c9489ee8405344dd5a0S3BucketD7637C1B" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse7d4044be8659ef3bb40a53a69846d7ca1b2d8e4e4bd36ad8c9d8e69fe3b68a0S3VersionKey4A4C6C19" + "Ref": "AssetParameters3dc8c5549b88fef617feef923524902b3650973ae1159c9489ee8405344dd5a0S3VersionKeyC19FD924" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse7d4044be8659ef3bb40a53a69846d7ca1b2d8e4e4bd36ad8c9d8e69fe3b68a0S3VersionKey4A4C6C19" + "Ref": "AssetParameters3dc8c5549b88fef617feef923524902b3650973ae1159c9489ee8405344dd5a0S3VersionKeyC19FD924" } ] } @@ -272,17 +272,17 @@ } }, "Parameters": { - "AssetParameterse7d4044be8659ef3bb40a53a69846d7ca1b2d8e4e4bd36ad8c9d8e69fe3b68a0S3Bucket316B0B52": { + "AssetParameters3dc8c5549b88fef617feef923524902b3650973ae1159c9489ee8405344dd5a0S3BucketD7637C1B": { "Type": "String", - "Description": "S3 bucket for asset \"e7d4044be8659ef3bb40a53a69846d7ca1b2d8e4e4bd36ad8c9d8e69fe3b68a0\"" + "Description": "S3 bucket for asset \"3dc8c5549b88fef617feef923524902b3650973ae1159c9489ee8405344dd5a0\"" }, - "AssetParameterse7d4044be8659ef3bb40a53a69846d7ca1b2d8e4e4bd36ad8c9d8e69fe3b68a0S3VersionKey4A4C6C19": { + "AssetParameters3dc8c5549b88fef617feef923524902b3650973ae1159c9489ee8405344dd5a0S3VersionKeyC19FD924": { "Type": "String", - "Description": "S3 key for asset version \"e7d4044be8659ef3bb40a53a69846d7ca1b2d8e4e4bd36ad8c9d8e69fe3b68a0\"" + "Description": "S3 key for asset version \"3dc8c5549b88fef617feef923524902b3650973ae1159c9489ee8405344dd5a0\"" }, - "AssetParameterse7d4044be8659ef3bb40a53a69846d7ca1b2d8e4e4bd36ad8c9d8e69fe3b68a0ArtifactHash2FE6C4D8": { + "AssetParameters3dc8c5549b88fef617feef923524902b3650973ae1159c9489ee8405344dd5a0ArtifactHash9DF43F02": { "Type": "String", - "Description": "Artifact hash for asset \"e7d4044be8659ef3bb40a53a69846d7ca1b2d8e4e4bd36ad8c9d8e69fe3b68a0\"" + "Description": "Artifact hash for asset \"3dc8c5549b88fef617feef923524902b3650973ae1159c9489ee8405344dd5a0\"" } }, "Outputs": { @@ -313,4 +313,4 @@ } } } -} +} \ No newline at end of file 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 4850214d48c82..1f3a157fc36b9 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 @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters4e547bc3a1a10467cf6503c7845fe1a857e509746b927699a9e3bea8b802181eS3Bucket4E51347F" + "Ref": "AssetParametersfec8e8354e12687c5a4b843b4e269741f53dec634946869b276f7fd1017845c3S3Bucket2E551A38" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters4e547bc3a1a10467cf6503c7845fe1a857e509746b927699a9e3bea8b802181eS3VersionKey707D1166" + "Ref": "AssetParametersfec8e8354e12687c5a4b843b4e269741f53dec634946869b276f7fd1017845c3S3VersionKeyE54FD621" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters4e547bc3a1a10467cf6503c7845fe1a857e509746b927699a9e3bea8b802181eS3VersionKey707D1166" + "Ref": "AssetParametersfec8e8354e12687c5a4b843b4e269741f53dec634946869b276f7fd1017845c3S3VersionKeyE54FD621" } ] } @@ -281,17 +281,17 @@ } }, "Parameters": { - "AssetParameters4e547bc3a1a10467cf6503c7845fe1a857e509746b927699a9e3bea8b802181eS3Bucket4E51347F": { + "AssetParametersfec8e8354e12687c5a4b843b4e269741f53dec634946869b276f7fd1017845c3S3Bucket2E551A38": { "Type": "String", - "Description": "S3 bucket for asset \"4e547bc3a1a10467cf6503c7845fe1a857e509746b927699a9e3bea8b802181e\"" + "Description": "S3 bucket for asset \"fec8e8354e12687c5a4b843b4e269741f53dec634946869b276f7fd1017845c3\"" }, - "AssetParameters4e547bc3a1a10467cf6503c7845fe1a857e509746b927699a9e3bea8b802181eS3VersionKey707D1166": { + "AssetParametersfec8e8354e12687c5a4b843b4e269741f53dec634946869b276f7fd1017845c3S3VersionKeyE54FD621": { "Type": "String", - "Description": "S3 key for asset version \"4e547bc3a1a10467cf6503c7845fe1a857e509746b927699a9e3bea8b802181e\"" + "Description": "S3 key for asset version \"fec8e8354e12687c5a4b843b4e269741f53dec634946869b276f7fd1017845c3\"" }, - "AssetParameters4e547bc3a1a10467cf6503c7845fe1a857e509746b927699a9e3bea8b802181eArtifactHash267391ED": { + "AssetParametersfec8e8354e12687c5a4b843b4e269741f53dec634946869b276f7fd1017845c3ArtifactHashD7A29DA9": { "Type": "String", - "Description": "Artifact hash for asset \"4e547bc3a1a10467cf6503c7845fe1a857e509746b927699a9e3bea8b802181e\"" + "Description": "Artifact hash for asset \"fec8e8354e12687c5a4b843b4e269741f53dec634946869b276f7fd1017845c3\"" } }, "Outputs": { @@ -322,4 +322,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json index 4a18847f20311..da8f3e8d934fa 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters4e547bc3a1a10467cf6503c7845fe1a857e509746b927699a9e3bea8b802181eS3Bucket4E51347F" + "Ref": "AssetParametersfec8e8354e12687c5a4b843b4e269741f53dec634946869b276f7fd1017845c3S3Bucket2E551A38" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters4e547bc3a1a10467cf6503c7845fe1a857e509746b927699a9e3bea8b802181eS3VersionKey707D1166" + "Ref": "AssetParametersfec8e8354e12687c5a4b843b4e269741f53dec634946869b276f7fd1017845c3S3VersionKeyE54FD621" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters4e547bc3a1a10467cf6503c7845fe1a857e509746b927699a9e3bea8b802181eS3VersionKey707D1166" + "Ref": "AssetParametersfec8e8354e12687c5a4b843b4e269741f53dec634946869b276f7fd1017845c3S3VersionKeyE54FD621" } ] } @@ -272,17 +272,17 @@ } }, "Parameters": { - "AssetParameters4e547bc3a1a10467cf6503c7845fe1a857e509746b927699a9e3bea8b802181eS3Bucket4E51347F": { + "AssetParametersfec8e8354e12687c5a4b843b4e269741f53dec634946869b276f7fd1017845c3S3Bucket2E551A38": { "Type": "String", - "Description": "S3 bucket for asset \"4e547bc3a1a10467cf6503c7845fe1a857e509746b927699a9e3bea8b802181e\"" + "Description": "S3 bucket for asset \"fec8e8354e12687c5a4b843b4e269741f53dec634946869b276f7fd1017845c3\"" }, - "AssetParameters4e547bc3a1a10467cf6503c7845fe1a857e509746b927699a9e3bea8b802181eS3VersionKey707D1166": { + "AssetParametersfec8e8354e12687c5a4b843b4e269741f53dec634946869b276f7fd1017845c3S3VersionKeyE54FD621": { "Type": "String", - "Description": "S3 key for asset version \"4e547bc3a1a10467cf6503c7845fe1a857e509746b927699a9e3bea8b802181e\"" + "Description": "S3 key for asset version \"fec8e8354e12687c5a4b843b4e269741f53dec634946869b276f7fd1017845c3\"" }, - "AssetParameters4e547bc3a1a10467cf6503c7845fe1a857e509746b927699a9e3bea8b802181eArtifactHash267391ED": { + "AssetParametersfec8e8354e12687c5a4b843b4e269741f53dec634946869b276f7fd1017845c3ArtifactHashD7A29DA9": { "Type": "String", - "Description": "Artifact hash for asset \"4e547bc3a1a10467cf6503c7845fe1a857e509746b927699a9e3bea8b802181e\"" + "Description": "Artifact hash for asset \"fec8e8354e12687c5a4b843b4e269741f53dec634946869b276f7fd1017845c3\"" } }, "Outputs": { @@ -313,4 +313,4 @@ } } } -} +} \ No newline at end of file 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 2cbc9c1ebbbb8..6d17b2e53232e 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json @@ -528,7 +528,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters41bb2d4a81eb782e91694a93a5e13f3e4a4ed3148bdb066856d1e5275b593cd7S3Bucket763974CB" + "Ref": "AssetParametersc7bba0d9d477c86c6dc2adb0eb95842634a1c040dd3a66b42eec2bb604644d4fS3BucketE85F411C" }, "S3Key": { "Fn::Join": [ @@ -541,7 +541,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters41bb2d4a81eb782e91694a93a5e13f3e4a4ed3148bdb066856d1e5275b593cd7S3VersionKey57F05589" + "Ref": "AssetParametersc7bba0d9d477c86c6dc2adb0eb95842634a1c040dd3a66b42eec2bb604644d4fS3VersionKeyF02404BC" } ] } @@ -554,7 +554,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters41bb2d4a81eb782e91694a93a5e13f3e4a4ed3148bdb066856d1e5275b593cd7S3VersionKey57F05589" + "Ref": "AssetParametersc7bba0d9d477c86c6dc2adb0eb95842634a1c040dd3a66b42eec2bb604644d4fS3VersionKeyF02404BC" } ] } @@ -607,17 +607,17 @@ } }, "Parameters": { - "AssetParameters41bb2d4a81eb782e91694a93a5e13f3e4a4ed3148bdb066856d1e5275b593cd7S3Bucket763974CB": { + "AssetParametersc7bba0d9d477c86c6dc2adb0eb95842634a1c040dd3a66b42eec2bb604644d4fS3BucketE85F411C": { "Type": "String", - "Description": "S3 bucket for asset \"41bb2d4a81eb782e91694a93a5e13f3e4a4ed3148bdb066856d1e5275b593cd7\"" + "Description": "S3 bucket for asset \"c7bba0d9d477c86c6dc2adb0eb95842634a1c040dd3a66b42eec2bb604644d4f\"" }, - "AssetParameters41bb2d4a81eb782e91694a93a5e13f3e4a4ed3148bdb066856d1e5275b593cd7S3VersionKey57F05589": { + "AssetParametersc7bba0d9d477c86c6dc2adb0eb95842634a1c040dd3a66b42eec2bb604644d4fS3VersionKeyF02404BC": { "Type": "String", - "Description": "S3 key for asset version \"41bb2d4a81eb782e91694a93a5e13f3e4a4ed3148bdb066856d1e5275b593cd7\"" + "Description": "S3 key for asset version \"c7bba0d9d477c86c6dc2adb0eb95842634a1c040dd3a66b42eec2bb604644d4f\"" }, - "AssetParameters41bb2d4a81eb782e91694a93a5e13f3e4a4ed3148bdb066856d1e5275b593cd7ArtifactHash9CD65872": { + "AssetParametersc7bba0d9d477c86c6dc2adb0eb95842634a1c040dd3a66b42eec2bb604644d4fArtifactHash6742F1C9": { "Type": "String", - "Description": "Artifact hash for asset \"41bb2d4a81eb782e91694a93a5e13f3e4a4ed3148bdb066856d1e5275b593cd7\"" + "Description": "Artifact hash for asset \"c7bba0d9d477c86c6dc2adb0eb95842634a1c040dd3a66b42eec2bb604644d4f\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json index 92ca8ad632bfc..5aa57732955bc 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json @@ -120,7 +120,7 @@ "BooksApi60AC975F" ] }, - "BooksApiDeployment86CA39AFc1570c78b1ea90526c0309cd74b7b8d0": { + "BooksApiDeployment86CA39AF4ff82f86c127f53c9de94d266b1906be": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { @@ -141,7 +141,7 @@ "Ref": "BooksApi60AC975F" }, "DeploymentId": { - "Ref": "BooksApiDeployment86CA39AFc1570c78b1ea90526c0309cd74b7b8d0" + "Ref": "BooksApiDeployment86CA39AF4ff82f86c127f53c9de94d266b1906be" }, "StageName": "prod" } diff --git a/packages/@aws-cdk/aws-apigateway/test/test.api-key.ts b/packages/@aws-cdk/aws-apigateway/test/test.api-key.ts index e32f7be5149d5..e7db715036bc6 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.api-key.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.api-key.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as apigateway from '../lib'; @@ -43,4 +43,27 @@ export = { test.done(); }, + + 'use an imported api key'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + 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 + const importedKey = apigateway.ApiKey.fromApiKeyId(stack, 'imported', 'KeyIdabc'); + api.addUsagePlan('plan', { + apiKey: importedKey, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ApiGateway::UsagePlanKey', { + KeyId: 'KeyIdabc', + KeyType: 'API_KEY', + UsagePlanId: { + Ref: 'testapiplan1B111AFF', + }, + })); + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts b/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts index 47118594132a5..0f5ce4a37732d 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts @@ -150,16 +150,16 @@ export = { // the logical ID changed const template = synthesize(); test.ok(!template.Resources.deployment33381975bba46c5132329b81e7befcbbba5a0e75, 'old resource id is not deleted'); - test.ok(template.Resources.deployment33381975075f46a4503208d69fcffed2f263c48c, - `new resource deployment33381975075f46a4503208d69fcffed2f263c48c is not created, instead found ${Object.keys(template.Resources)}`); + test.ok(template.Resources.deployment333819758aa4cdb9d204502b959c4903f4d5d29f, + `new resource deployment333819758aa4cdb9d204502b959c4903f4d5d29f is not created, instead found ${Object.keys(template.Resources)}`); // tokens supported, and are resolved upon synthesis const value = 'hello hello'; deployment.addToLogicalId({ foo: Lazy.stringValue({ produce: () => value }) }); const template2 = synthesize(); - test.ok(template2.Resources.deployment33381975b6d7672e4c9afd0b741e41d07739786b, - `resource deployment33381975b6d7672e4c9afd0b741e41d07739786b not found, instead found ${Object.keys(template2.Resources)}`); + test.ok(template2.Resources.deployment333819758d91bed959c6bd6268ba84f6d33e888e, + `resource deployment333819758d91bed959c6bd6268ba84f6d33e888e not found, instead found ${Object.keys(template2.Resources)}`); test.done(); diff --git a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts index 3b51f66a611e3..9736dc2e188b5 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts @@ -1018,5 +1018,29 @@ export = { })); test.done(); }, + + '"endpointTypes" can be used to specify endpoint configuration for SpecRestApi'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.SpecRestApi(stack, 'api', { + apiDefinition: apigw.ApiDefinition.fromInline({ foo: 'bar' }), + endpointTypes: [ apigw.EndpointType.EDGE, apigw.EndpointType.PRIVATE ], + }); + + api.root.addMethod('GET'); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::RestApi', { + EndpointConfiguration: { + Types: [ + 'EDGE', + 'PRIVATE', + ], + }, + })); + test.done(); + }, }, }; diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index f64255743f11d..9d94f4ed3f8da 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -106,7 +106,6 @@ The `corsPreflight` option lets you specify a CORS configuration for an API. ```ts new HttpApi(stack, 'HttpProxyApi', { corsPreflight: { - allowCredentials: true, allowHeaders: ['Authorization'], allowMethods: [HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS, HttpMethod.POST], allowOrigins: ['*'], @@ -136,8 +135,6 @@ the API's URL - `https://{api_id}.execute-api.{region}.amazonaws.com/`. Note that, `HttpApi` will always creates a `$default` stage, unless the `createDefaultStage` property is unset. - - ### Custom Domain Custom domain names are simpler and more intuitive URLs that you can provide to your API users. Custom domain name are associated to API stages. diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts index ef6e7cf80a637..d868b25f9eea1 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts @@ -143,6 +143,10 @@ export class HttpApi extends Resource implements IHttpApi { let corsConfiguration: CfnApi.CorsProperty | undefined; if (props?.corsPreflight) { + const cors = props.corsPreflight; + if (cors.allowOrigins && cors.allowOrigins.includes('*') && cors.allowCredentials) { + throw new Error("CORS preflight - allowCredentials is not supported when allowOrigin is '*'"); + } const { allowCredentials, allowHeaders, diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts index 7e9672ed5a786..236f533ee746e 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts @@ -112,6 +112,23 @@ export class HttpIntegration extends Resource implements IHttpIntegration { } } +/** + * Options to the HttpRouteIntegration during its bind operation. + */ +export interface HttpRouteIntegrationBindOptions { + /** + * The route to which this is being bound. + */ + readonly route: IHttpRoute; + + /** + * The current scope in which the bind is occurring. + * If the `HttpRouteIntegration` being bound creates additional constructs, + * this will be used as their parent scope. + */ + readonly scope: Construct; +} + /** * The interface that various route integration classes will inherit. */ @@ -119,7 +136,7 @@ export interface IHttpRouteIntegration { /** * Bind this integration to the route. */ - bind(route: IHttpRoute): HttpRouteIntegrationConfig; + bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig; } /** @@ -149,4 +166,4 @@ export interface HttpRouteIntegrationConfig { * @default - undefined */ readonly payloadFormatVersion: PayloadFormatVersion; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/http.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/http.ts index b514b63f8056f..d5020d2d908ba 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/http.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/http.ts @@ -1,5 +1,5 @@ -import { HttpIntegrationType, HttpRouteIntegrationConfig, IHttpRouteIntegration, PayloadFormatVersion } from '../integration'; -import { HttpMethod, IHttpRoute } from '../route'; +import { HttpIntegrationType, HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig, IHttpRouteIntegration, PayloadFormatVersion } from '../integration'; +import { HttpMethod } from '../route'; /** * Properties to initialize a new `HttpProxyIntegration`. @@ -24,7 +24,7 @@ export class HttpProxyIntegration implements IHttpRouteIntegration { constructor(private readonly props: HttpProxyIntegrationProps) { } - public bind(_: IHttpRoute): HttpRouteIntegrationConfig { + public bind(_: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { return { method: this.props.method ?? HttpMethod.ANY, payloadFormatVersion: PayloadFormatVersion.VERSION_1_0, // 1.0 is required and is the only supported format diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/lambda.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/lambda.ts index 155b671fa79c2..30973a10b2ca0 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/lambda.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/lambda.ts @@ -1,8 +1,7 @@ import { ServicePrincipal } from '@aws-cdk/aws-iam'; import { IFunction } from '@aws-cdk/aws-lambda'; import { Stack } from '@aws-cdk/core'; -import { HttpIntegrationType, HttpRouteIntegrationConfig, IHttpRouteIntegration, PayloadFormatVersion } from '../integration'; -import { IHttpRoute } from '../route'; +import { HttpIntegrationType, HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig, IHttpRouteIntegration, PayloadFormatVersion } from '../integration'; /** * Lambda Proxy integration properties @@ -29,8 +28,10 @@ export class LambdaProxyIntegration implements IHttpRouteIntegration { constructor(private readonly props: LambdaProxyIntegrationProps) { } - public bind(route: IHttpRoute): HttpRouteIntegrationConfig { + public bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + const route = options.route; this.props.handler.addPermission(`${route.node.uniqueId}-Permission`, { + scope: options.scope, principal: new ServicePrincipal('apigateway.amazonaws.com'), sourceArn: Stack.of(route).formatArn({ service: 'execute-api', diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts index 574b2c82275e8..2f65902a6aaee 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts @@ -120,7 +120,10 @@ export class HttpRoute extends Resource implements IHttpRoute { this.path = props.routeKey.path; let integration: HttpIntegration | undefined; - const config = props.integration.bind(this); + const config = props.integration.bind({ + route: this, + scope: this, + }); integration = new HttpIntegration(this, `${this.node.id}-Integration`, { httpApi: props.httpApi, integrationType: config.type, diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts index 5e88dbfe3cbf5..1924976115891 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts @@ -1,6 +1,7 @@ import '@aws-cdk/assert/jest'; +import { ABSENT } from '@aws-cdk/assert'; import { Code, Function, Runtime } from '@aws-cdk/aws-lambda'; -import { Stack } from '@aws-cdk/core'; +import { Duration, Stack } from '@aws-cdk/core'; import { HttpApi, HttpMethod, LambdaProxyIntegration } from '../../lib'; describe('HttpApi', () => { @@ -113,4 +114,46 @@ describe('HttpApi', () => { RouteKey: 'ANY /pets', }); }); + + describe('cors', () => { + test('cors is correctly configured.', () => { + const stack = new Stack(); + new HttpApi(stack, 'HttpApi', { + corsPreflight: { + allowHeaders: ['Authorization'], + allowMethods: [HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS, HttpMethod.POST], + allowOrigins: ['*'], + maxAge: Duration.seconds(36400), + }, + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Api', { + CorsConfiguration: { + AllowHeaders: ['Authorization'], + AllowMethods: ['GET', 'HEAD', 'OPTIONS', 'POST'], + AllowOrigins: ['*'], + MaxAge: 36400, + }, + }); + }); + + test('cors is absent when not specified.', () => { + const stack = new Stack(); + new HttpApi(stack, 'HttpApi'); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Api', { + CorsConfiguration: ABSENT, + }); + }); + + test('errors when allowConfiguration is specified along with origin "*"', () => { + const stack = new Stack(); + expect(() => new HttpApi(stack, 'HttpApi', { + corsPreflight: { + allowCredentials: true, + allowOrigins: ['*'], + }, + })).toThrowError(/allowCredentials is not supported/); + }); + }); }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/integ.custom-domain.expected.json b/packages/@aws-cdk/aws-apigatewayv2/test/http/integ.custom-domain.expected.json index 4c9ada01d006c..2273bb18b02b4 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/integ.custom-domain.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/integ.custom-domain.expected.json @@ -4,27 +4,31 @@ "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { - "Statement": [{ - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "lambda.amazonaws.com" + "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" + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] ] - ] - }] + } + ] } }, "echohandler8F648AB2": { @@ -46,44 +50,26 @@ "echohandlerServiceRole833A8F7A" ] }, - "echohandlerinteghttpproxyHttpProxyProdApiDefaultRoute20082F68PermissionBE86C6B3": { - "Type": "AWS::Lambda::Permission", + "DNFDC76583": { + "Type": "AWS::ApiGatewayV2::DomainName", "Properties": { - "Action": "lambda:InvokeFunction", - "FunctionName": { - "Fn::GetAtt": [ - "echohandler8F648AB2", - "Arn" - ] - }, - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":execute-api:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":", - { - "Ref": "HttpProxyProdApi368B6161" - }, - "/*/*" - ] - ] - } + "DomainName": "apigv2.demo.com", + "DomainNameConfigurations": [ + { + "CertificateArn": "arn:aws:acm:us-east-1:111111111111:certificate", + "EndpointType": "REGIONAL" + } + ] } }, - "echohandlerinteghttpproxyDemoApiDefaultRoute050CFFE6PermissionD503E35E": { + "HttpProxyProdApi368B6161": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "HttpProxyProdApi", + "ProtocolType": "HTTP" + } + }, + "HttpProxyProdApiDefaultRouteinteghttpproxyHttpProxyProdApiDefaultRoute20082F68PermissionB29E0EB7": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -112,7 +98,7 @@ }, ":", { - "Ref": "DemoApiE67238F8" + "Ref": "HttpProxyProdApi368B6161" }, "/*/*" ] @@ -120,23 +106,6 @@ } } }, - "DNFDC76583": { - "Type": "AWS::ApiGatewayV2::DomainName", - "Properties": { - "DomainName": "apigv2.demo.com", - "DomainNameConfigurations": [{ - "CertificateArn": "arn:aws:acm:us-east-1:111111111111:certificate", - "EndpointType": "REGIONAL" - }] - } - }, - "HttpProxyProdApi368B6161": { - "Type": "AWS::ApiGatewayV2::Api", - "Properties": { - "Name": "HttpProxyProdApi", - "ProtocolType": "HTTP" - } - }, "HttpProxyProdApiDefaultRouteDefaultRouteIntegration702F0DF7": { "Type": "AWS::ApiGatewayV2::Integration", "Properties": { @@ -236,6 +205,43 @@ "ProtocolType": "HTTP" } }, + "DemoApiDefaultRouteinteghttpproxyDemoApiDefaultRoute050CFFE6PermissionB1FE82E7": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "echohandler8F648AB2", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "DemoApiE67238F8" + }, + "/*/*" + ] + ] + } + } + }, "DemoApiDefaultRouteDefaultRouteIntegration714B9B03": { "Type": "AWS::ApiGatewayV2::Integration", "Properties": { @@ -326,4 +332,4 @@ "Value": "https://apigv2.demo.com/demo" } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/http.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/http.test.ts index 8f187d0ab9c78..c8d30e292b46c 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/http.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/http.test.ts @@ -1,6 +1,5 @@ -import { ABSENT } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; -import { Duration, Stack } from '@aws-cdk/core'; +import { Stack } from '@aws-cdk/core'; import { HttpApi, HttpIntegration, HttpIntegrationType, HttpMethod, HttpProxyIntegration, HttpRoute, HttpRouteKey, PayloadFormatVersion } from '../../../lib'; describe('HttpProxyIntegration', () => { @@ -72,37 +71,3 @@ describe('HttpProxyIntegration', () => { }); }); }); - -describe('CORS', () => { - test('CORS Configuration is correctly configured.', () => { - const stack = new Stack(); - new HttpApi(stack, 'HttpApi', { - corsPreflight: { - allowCredentials: true, - allowHeaders: ['Authorization'], - allowMethods: [HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS, HttpMethod.POST], - allowOrigins: ['*'], - maxAge: Duration.seconds(36400), - }, - }); - - expect(stack).toHaveResource('AWS::ApiGatewayV2::Api', { - CorsConfiguration: { - AllowCredentials: true, - AllowHeaders: ['Authorization'], - AllowMethods: ['GET', 'HEAD', 'OPTIONS', 'POST'], - AllowOrigins: ['*'], - MaxAge: 36400, - }, - }); - }); - - test('CorsConfiguration is ABSENT when not specified.', () => { - const stack = new Stack(); - new HttpApi(stack, 'HttpApi'); - - expect(stack).toHaveResource('AWS::ApiGatewayV2::Api', { - CorsConfiguration: ABSENT, - }); - }); -}); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/integ.http-proxy.expected.json b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/integ.http-proxy.expected.json index 475ef14f158ef..d91a751447c01 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/integ.http-proxy.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/integ.http-proxy.expected.json @@ -50,7 +50,14 @@ "AlwaysSuccessServiceRole6DB8C2F6" ] }, - "AlwaysSuccessinteghttpproxyLambdaProxyApiDefaultRoute17D52FE1Permission3B39FF57": { + "LambdaProxyApi67594471": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "LambdaProxyApi", + "ProtocolType": "HTTP" + } + }, + "LambdaProxyApiDefaultRouteinteghttpproxyLambdaProxyApiDefaultRoute17D52FE1Permission4875FF59": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -87,13 +94,6 @@ } } }, - "LambdaProxyApi67594471": { - "Type": "AWS::ApiGatewayV2::Api", - "Properties": { - "Name": "LambdaProxyApi", - "ProtocolType": "HTTP" - } - }, "LambdaProxyApiDefaultRouteDefaultRouteIntegration97D5250B": { "Type": "AWS::ApiGatewayV2::Integration", "Properties": { diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/integ.lambda-proxy.expected.json b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/integ.lambda-proxy.expected.json index ff35db0f3c0d5..38a6929757755 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/integ.lambda-proxy.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/integ.lambda-proxy.expected.json @@ -50,7 +50,14 @@ "AlwaysSuccessServiceRole6DB8C2F6" ] }, - "AlwaysSuccessinteglambdaproxyLambdaProxyApiDefaultRoute59CA2390Permission90573BC0": { + "LambdaProxyApi67594471": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "LambdaProxyApi", + "ProtocolType": "HTTP" + } + }, + "LambdaProxyApiDefaultRouteinteglambdaproxyLambdaProxyApiDefaultRoute59CA2390Permission07F93503": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -87,13 +94,6 @@ } } }, - "LambdaProxyApi67594471": { - "Type": "AWS::ApiGatewayV2::Api", - "Properties": { - "Name": "LambdaProxyApi", - "ProtocolType": "HTTP" - } - }, "LambdaProxyApiDefaultRouteDefaultRouteIntegration97D5250B": { "Type": "AWS::ApiGatewayV2::Integration", "Properties": { diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/lambda.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/lambda.test.ts index 9984b8fdd9bff..8cdea2a3748c5 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/lambda.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/lambda.test.ts @@ -1,6 +1,6 @@ import '@aws-cdk/assert/jest'; import { Code, Function, Runtime } from '@aws-cdk/aws-lambda'; -import { Stack } from '@aws-cdk/core'; +import { App, Stack } from '@aws-cdk/core'; import { HttpApi, HttpRoute, HttpRouteKey, LambdaProxyIntegration, PayloadFormatVersion } from '../../../lib'; describe('LambdaProxyIntegration', () => { @@ -39,6 +39,21 @@ describe('LambdaProxyIntegration', () => { PayloadFormatVersion: '1.0', }); }); + + test('no dependency cycles', () => { + const app = new App(); + const lambdaStack = new Stack(app, 'lambdaStack'); + const fooFn = fooFunction(lambdaStack, 'Fn'); + + const apigwStack = new Stack(app, 'apigwStack'); + new HttpApi(apigwStack, 'httpApi', { + defaultIntegration: new LambdaProxyIntegration({ + handler: fooFn, + }), + }); + + expect(() => app.synth()).not.toThrow(); + }); }); function fooFunction(stack: Stack, id: string) { diff --git a/packages/@aws-cdk/aws-appsync/README.md b/packages/@aws-cdk/aws-appsync/README.md index 40064e0822e02..f086184760132 100644 --- a/packages/@aws-cdk/aws-appsync/README.md +++ b/packages/@aws-cdk/aws-appsync/README.md @@ -54,6 +54,7 @@ const api = new appsync.GraphQLApi(stack, 'Api', { authorizationType: appsync.AuthorizationType.IAM }, }, + xrayEnabled: true, }); const demoTable = new db.Table(stack, 'DemoTable', { @@ -159,4 +160,4 @@ api.grantMutation(role, 'updateExample'); // For custom types and granular design api.grant(role, appsync.IamResource.ofType('Mutation', 'updateExample'), 'appsync:GraphQL'); -``` \ No newline at end of file +``` diff --git a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts index ce9c380d3707a..c2c1e768e2d9b 100644 --- a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts +++ b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts @@ -256,6 +256,12 @@ export interface GraphQLApiProps { * @default - Use schemaDefinition */ readonly schemaDefinitionFile?: string; + /** + * A flag indicating whether or not X-Ray tracing is enabled for the GraphQL API. + * + * @default - false + */ + readonly xrayEnabled?: boolean; } @@ -402,6 +408,7 @@ export class GraphQLApi extends Construct { ) : undefined, additionalAuthenticationProviders: this.formatAdditionalAuthenticationProviders(props), + xrayEnabled: props.xrayEnabled, }); this.apiId = this.api.attrApiId; diff --git a/packages/@aws-cdk/aws-appsync/test/appsync.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync.test.ts index 70e8ef0082b89..3c1ee1a71f1f2 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync.test.ts @@ -94,4 +94,23 @@ test('appsync should configure resolver as unit when pipelineConfig is empty arr expect(stack).toHaveResourceLike('AWS::AppSync::Resolver', { Kind: 'UNIT', }); -}); \ No newline at end of file +}); + +test('when xray is enabled should not throw an Error', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new appsync.GraphQLApi(stack, 'api', { + authorizationConfig: {}, + name: 'api', + schemaDefinition: appsync.SchemaDefinition.FILE, + schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + xrayEnabled: true, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLApi', { + XrayEnabled: true, + }); +}); diff --git a/packages/@aws-cdk/aws-athena/package.json b/packages/@aws-cdk/aws-athena/package.json index 51bc934c9fdac..337a2890ef389 100644 --- a/packages/@aws-cdk/aws-athena/package.json +++ b/packages/@aws-cdk/aws-athena/package.json @@ -65,6 +65,8 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", + "nodeunit-shim": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-athena/test/athena.test.ts b/packages/@aws-cdk/aws-athena/test/athena.test.ts index e394ef336bfb4..57a08032e1e59 100644 --- a/packages/@aws-cdk/aws-athena/test/athena.test.ts +++ b/packages/@aws-cdk/aws-athena/test/athena.test.ts @@ -1,6 +1,72 @@ import '@aws-cdk/assert/jest'; -import {} from '../lib'; +import { expect, haveResource } from '@aws-cdk/assert'; +import * as cdk from '@aws-cdk/core'; +import { CfnWorkGroup } from '../lib'; -test('No tests are specified for this package', () => { - expect(true).toBe(true); +describe('Athena Workgroup Tags', () => { + test('test tag spec correction', () => { + const stack = new cdk.Stack(); + new CfnWorkGroup(stack, 'Athena-Workgroup', { + name: 'HelloWorld', + description: 'A WorkGroup', + recursiveDeleteOption: true, + state: 'ENABLED', + tags: [ + { + key: 'key1', + value: 'value1', + }, + { + key: 'key2', + value: 'value2', + }], + workGroupConfiguration: { + requesterPaysEnabled: true, + resultConfiguration: { + outputLocation: 's3://fakebucketme/athena/results/', + }, + }, + }); + expect(stack).to(haveResource('AWS::Athena::WorkGroup', { + Tags: [ + { + Key: 'key1', + Value: 'value1', + }, + { + Key: 'key2', + Value: 'value2', + }, + ], + })); + }); + test('test tag aspect spec correction', () => { + const stack = new cdk.Stack(); + cdk.Tag.add(stack, 'key1', 'value1'); + cdk.Tag.add(stack, 'key2', 'value2'); + new CfnWorkGroup(stack, 'Athena-Workgroup', { + name: 'HelloWorld', + description: 'A WorkGroup', + recursiveDeleteOption: true, + state: 'ENABLED', + workGroupConfiguration: { + requesterPaysEnabled: true, + resultConfiguration: { + outputLocation: 's3://fakebucketme/athena/results/', + }, + }, + }); + expect(stack).to(haveResource('AWS::Athena::WorkGroup', { + Tags: [ + { + Key: 'key1', + Value: 'value1', + }, + { + Key: 'key2', + Value: 'value2', + }, + ], + })); + }); }); diff --git a/packages/@aws-cdk/aws-athena/test/integ.workgroup.expected.json b/packages/@aws-cdk/aws-athena/test/integ.workgroup.expected.json new file mode 100644 index 0000000000000..bfc35b8efc2f3 --- /dev/null +++ b/packages/@aws-cdk/aws-athena/test/integ.workgroup.expected.json @@ -0,0 +1,23 @@ +{ + "Resources": { + "AthenaWorkgroup": { + "Type": "AWS::Athena::WorkGroup", + "Properties": { + "Name": "HelloWorld", + "Description": "A WorkGroup", + "RecursiveDeleteOption": true, + "State": "ENABLED", + "Tags": [ + { + "Key": "key1", + "Value": "value1" + }, + { + "Key": "key2", + "Value": "value2" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-athena/test/integ.workgroup.ts b/packages/@aws-cdk/aws-athena/test/integ.workgroup.ts new file mode 100644 index 0000000000000..068ec1f81dbdd --- /dev/null +++ b/packages/@aws-cdk/aws-athena/test/integ.workgroup.ts @@ -0,0 +1,23 @@ +import * as cdk from '@aws-cdk/core'; +import { CfnWorkGroup }from '../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-athena-workgroup-tags'); + +new CfnWorkGroup(stack, 'AthenaWorkgroup', { + name: 'HelloWorld', + description: 'A WorkGroup', + recursiveDeleteOption: true, + state: 'ENABLED', + tags: [ + { + key: 'key1', + value: 'value1', + }, + { + key: 'key2', + value: 'value2', + }], +}); + +app.synth(); 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 626efdf1c7ce0..52242e44bda29 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 @@ -32,7 +32,7 @@ nodeunitShim({ 'MyFleetInstanceSecurityGroup774E8234': { 'Type': 'AWS::EC2::SecurityGroup', 'Properties': { - 'GroupDescription': 'MyFleet/InstanceSecurityGroup', + 'GroupDescription': 'TestStack/MyFleet/InstanceSecurityGroup', 'SecurityGroupEgress': [ { 'CidrIp': '0.0.0.0/0', @@ -43,7 +43,7 @@ nodeunitShim({ 'Tags': [ { 'Key': 'Name', - 'Value': 'MyFleet', + 'Value': 'TestStack/MyFleet', }, ], @@ -68,7 +68,7 @@ nodeunitShim({ 'Tags': [ { 'Key': 'Name', - 'Value': 'MyFleet', + 'Value': 'TestStack/MyFleet', }, ], }, @@ -122,7 +122,7 @@ nodeunitShim({ { 'Key': 'Name', 'PropagateAtLaunch': true, - 'Value': 'MyFleet', + 'Value': 'TestStack/MyFleet', }, ], @@ -520,7 +520,7 @@ nodeunitShim({ { Key: 'Name', PropagateAtLaunch: true, - Value: 'MyFleet', + Value: 'TestStack/MyFleet', }, { Key: 'notsuper', 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 73684d88a5320..9475421d5d4c6 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/scheduled-action.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/scheduled-action.test.ts @@ -69,7 +69,7 @@ nodeunitShim({ { Key: 'Name', PropagateAtLaunch: true, - Value: 'ASG', + Value: 'Default/ASG', }, ], VPCZoneIdentifier: [ diff --git a/packages/@aws-cdk/aws-batch/README.md b/packages/@aws-cdk/aws-batch/README.md index 83589b65a12f8..314d647dec76e 100644 --- a/packages/@aws-cdk/aws-batch/README.md +++ b/packages/@aws-cdk/aws-batch/README.md @@ -138,6 +138,36 @@ Below is an example: const computeEnv = batch.ComputeEnvironment.fromComputeEnvironmentArn(this, 'imported-compute-env', 'arn:aws:batch:us-east-1:555555555555:compute-environment/My-Compute-Env'); ``` +### Change the baseline AMI of the compute resources + +Ocassionally, you will need to deviate from the default processing AMI. + +ECS Optimized Amazon Linux 2 example: + +```ts +const myComputeEnv = new batch.ComputeEnvironment(this, 'ComputeEnv', { + computeResources: { + image: new ecs.EcsOptimizedAmi({ + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, + }), + vpc, + } +}); +``` + +Custom based AMI example: + +```ts +const myComputeEnv = new batch.ComputeEnvironment(this, 'ComputeEnv', { + computeResources: { + image: ec2.MachineImage.genericLinux({ + "[aws-region]": "[ami-ID]", + }) + vpc, + } +}); +``` + ## Job Queue Jobs are always submitted to a specific queue. This means that you have to create a queue before you can start submitting jobs. Each queue is mapped to at least one (and no more than three) compute environment. When the job is scheduled for execution, AWS Batch will select the compute environment based on ordinal priority and available capacity in each environment. diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.deps.ts b/packages/@aws-cdk/aws-cloudformation/test/test.deps.ts index ed87cf1f6c3b3..0b39949203fe0 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.deps.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.deps.ts @@ -209,9 +209,9 @@ export = { const nested2 = new NestedStack(nested1, 'Nested2'); // THEN - test.throws(() => nested1.addDependency(root), /Nested stack 'Nested1' cannot depend on a parent stack ''/); - test.throws(() => nested2.addDependency(nested1), /Nested stack 'Nested1\/Nested2' cannot depend on a parent stack 'Nested1'/); - test.throws(() => nested2.addDependency(root), /Nested stack 'Nested1\/Nested2' cannot depend on a parent stack ''/); + test.throws(() => nested1.addDependency(root), /Nested stack 'Default\/Nested1' cannot depend on a parent stack 'Default'/); + test.throws(() => nested2.addDependency(nested1), /Nested stack 'Default\/Nested1\/Nested2' cannot depend on a parent stack 'Default\/Nested1'/); + test.throws(() => nested2.addDependency(root), /Nested stack 'Default\/Nested1\/Nested2' cannot depend on a parent stack 'Default'/); test.done(); }, diff --git a/packages/@aws-cdk/aws-cloudfront-origins/README.md b/packages/@aws-cdk/aws-cloudfront-origins/README.md index b1dd1de905709..614a089247459 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/README.md +++ b/packages/@aws-cdk/aws-cloudfront-origins/README.md @@ -76,3 +76,24 @@ new cloudfront.Distribution(this, 'myDist', { ``` See the documentation of `@aws-cdk/aws-cloudfront` for more information. + +## Failover Origins (Origin Groups) + +You can set up CloudFront with origin failover for scenarios that require high availability. +To get started, you create an origin group with two origins: a primary and a secondary. +If the primary origin is unavailable, or returns specific HTTP response status codes that indicate a failure, +CloudFront automatically switches to the secondary origin. +You achieve that behavior in the CDK using the `OriginGroup` class: + +```ts +new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { + origin: new origins.OriginGroup({ + primaryOrigin: new origins.S3Origin(myBucket), + fallbackOrigin: new origins.HttpOrigin('www.example.com'), + // optional, defaults to: 500, 502, 503 and 504 + fallbackStatusCodes: [404], + }), + }, +}); +``` diff --git a/packages/@aws-cdk/aws-cloudfront-origins/lib/index.ts b/packages/@aws-cdk/aws-cloudfront-origins/lib/index.ts index 5f41b97f3dfde..6fc3bf1750637 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudfront-origins/lib/index.ts @@ -1,3 +1,4 @@ export * from './http-origin'; export * from './load-balancer-origin'; export * from './s3-origin'; +export * from './origin-group'; diff --git a/packages/@aws-cdk/aws-cloudfront-origins/lib/origin-group.ts b/packages/@aws-cdk/aws-cloudfront-origins/lib/origin-group.ts new file mode 100644 index 0000000000000..8fa8284e62ea9 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/lib/origin-group.ts @@ -0,0 +1,49 @@ +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import { Construct } from '@aws-cdk/core'; + +/** Construction properties for {@link OriginGroup}. */ +export interface OriginGroupProps { + /** + * The primary origin that should serve requests for this group. + */ + readonly primaryOrigin: cloudfront.IOrigin; + + /** + * The fallback origin that should serve requests when the primary fails. + */ + readonly fallbackOrigin: cloudfront.IOrigin; + + /** + * The list of HTTP status codes that, + * when returned from the primary origin, + * would cause querying the fallback origin. + * + * @default - 500, 502, 503 and 504 + */ + readonly fallbackStatusCodes?: number[]; +} + +/** + * An Origin that represents a group. + * Consists of a primary Origin, + * and a fallback Origin called when the primary returns one of the provided HTTP status codes. + */ +export class OriginGroup implements cloudfront.IOrigin { + public constructor(private readonly props: OriginGroupProps) { + } + + public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { + const primaryOriginConfig = this.props.primaryOrigin.bind(scope, options); + if (primaryOriginConfig.failoverConfig) { + throw new Error('An OriginGroup cannot use an Origin with its own failover configuration as its primary origin!'); + } + + return { + originProperty: primaryOriginConfig.originProperty, + failoverConfig: { + failoverOrigin: this.props.fallbackOrigin, + statusCodes: this.props.fallbackStatusCodes, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts b/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts index 5c74b2b59382e..ea679a88a9aa7 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts +++ b/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts @@ -42,7 +42,6 @@ export class S3Origin implements cloudfront.IOrigin { public bind(scope: cdk.Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { return this.origin.bind(scope, options); } - } /** diff --git a/packages/@aws-cdk/aws-cloudfront-origins/test/integ.origin-group.expected.json b/packages/@aws-cdk/aws-cloudfront-origins/test/integ.origin-group.expected.json new file mode 100644 index 0000000000000..6ed3596c52eba --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/test/integ.origin-group.expected.json @@ -0,0 +1,144 @@ +{ + "Resources": { + "Bucket83908E77": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "BucketPolicyE9A3008A": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "Bucket83908E77" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Principal": { + "CanonicalUser": { + "Fn::GetAtt": [ + "DistributionOrigin1S3Origin5F5C0696", + "S3CanonicalUserId" + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "DistributionOrigin1S3Origin5F5C0696": { + "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", + "Properties": { + "CloudFrontOriginAccessIdentityConfig": { + "Comment": "Allows CloudFront to reach the bucket" + } + } + }, + "DistributionCFDistribution882A7313": { + "Type": "AWS::CloudFront::Distribution", + "Properties": { + "DistributionConfig": { + "DefaultCacheBehavior": { + "ForwardedValues": { + "QueryString": false + }, + "TargetOriginId": "cloudfrontorigingroupDistributionOrigin137659A54", + "ViewerProtocolPolicy": "allow-all" + }, + "Enabled": true, + "OriginGroups": { + "Items": [ + { + "FailoverCriteria": { + "StatusCodes": { + "Items": [ + 500, + 502, + 503, + 504 + ], + "Quantity": 4 + } + }, + "Id": "cloudfrontorigingroupDistributionOriginGroup10B57F1D1", + "Members": { + "Items": [ + { + "OriginId": "cloudfrontorigingroupDistributionOrigin137659A54" + }, + { + "OriginId": "cloudfrontorigingroupDistributionOrigin2CCE5D500" + } + ], + "Quantity": 2 + } + } + ], + "Quantity": 1 + }, + "Origins": [ + { + "DomainName": { + "Fn::GetAtt": [ + "Bucket83908E77", + "RegionalDomainName" + ] + }, + "Id": "cloudfrontorigingroupDistributionOrigin137659A54", + "S3OriginConfig": { + "OriginAccessIdentity": { + "Fn::Join": [ + "", + [ + "origin-access-identity/cloudfront/", + { + "Ref": "DistributionOrigin1S3Origin5F5C0696" + } + ] + ] + } + } + }, + { + "CustomOriginConfig": { + "OriginProtocolPolicy": "https-only" + }, + "DomainName": "www.example.com", + "Id": "cloudfrontorigingroupDistributionOrigin2CCE5D500" + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront-origins/test/integ.origin-group.ts b/packages/@aws-cdk/aws-cloudfront-origins/test/integ.origin-group.ts new file mode 100644 index 0000000000000..31557317a9b40 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/test/integ.origin-group.ts @@ -0,0 +1,22 @@ +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import * as origins from '../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'cloudfront-origin-group'); + +const bucket = new s3.Bucket(stack, 'Bucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, +}); + +const originGroup = new origins.OriginGroup({ + primaryOrigin: new origins.S3Origin(bucket), + fallbackOrigin: new origins.HttpOrigin('www.example.com'), +}); + +new cloudfront.Distribution(stack, 'Distribution', { + defaultBehavior: { origin: originGroup }, +}); + +app.synth(); 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 new file mode 100644 index 0000000000000..00dce3cc8c39e --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/test/origin-group.test.ts @@ -0,0 +1,142 @@ +import '@aws-cdk/assert/jest'; +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as s3 from '@aws-cdk/aws-s3'; +import { Stack } from '@aws-cdk/core'; +import * as origins from '../lib'; + +let stack: Stack; +let bucket: s3.IBucket; +let primaryOrigin: cloudfront.IOrigin; +beforeEach(() => { + stack = new Stack(); + bucket = new s3.Bucket(stack, 'Bucket'); + primaryOrigin = new origins.S3Origin(bucket); +}); + +describe('Origin Groups', () => { + test('correctly render the OriginGroups property of DistributionConfig', () => { + const failoverOrigin = new origins.S3Origin(s3.Bucket.fromBucketName(stack, 'ImportedBucket', 'imported-bucket')); + const originGroup = new origins.OriginGroup({ + primaryOrigin, + fallbackOrigin: failoverOrigin, + fallbackStatusCodes: [500], + }); + + new cloudfront.Distribution(stack, 'Distribution', { + defaultBehavior: { origin: originGroup }, + }); + + const primaryOriginId = 'DistributionOrigin13547B94F'; + const failoverOriginId = 'DistributionOrigin2C85CC43B'; + expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + DistributionConfig: { + Origins: [ + { + Id: primaryOriginId, + DomainName: { + 'Fn::GetAtt': ['Bucket83908E77', 'RegionalDomainName'], + }, + S3OriginConfig: { + OriginAccessIdentity: { + 'Fn::Join': ['', [ + 'origin-access-identity/cloudfront/', + { Ref: 'DistributionOrigin1S3Origin5F5C0696' }, + ]], + }, + }, + }, + { + Id: failoverOriginId, + DomainName: { + 'Fn::Join': ['', [ + 'imported-bucket.s3.', + { Ref: 'AWS::Region' }, + '.', + { Ref: 'AWS::URLSuffix' }, + ]], + }, + S3OriginConfig: { + OriginAccessIdentity: { + 'Fn::Join': ['', [ + 'origin-access-identity/cloudfront/', + { Ref: 'DistributionOrigin2S3OriginE484D4BF' }, + ]], + }, + }, + }, + ], + OriginGroups: { + Items: [ + { + FailoverCriteria: { + StatusCodes: { + Items: [500], + Quantity: 1, + }, + }, + Id: 'DistributionOriginGroup1A1A31B49', + Members: { + Items: [ + { OriginId: primaryOriginId }, + { OriginId: failoverOriginId }, + ], + Quantity: 2, + }, + }, + ], + Quantity: 1, + }, + }, + }); + }); + + test('cannot have an Origin with their own failover configuration as the primary Origin', () => { + const failoverOrigin = new origins.S3Origin(s3.Bucket.fromBucketName(stack, 'ImportedBucket', 'imported-bucket')); + const originGroup = new origins.OriginGroup({ + primaryOrigin, + fallbackOrigin: failoverOrigin, + }); + const groupOfGroups = new origins.OriginGroup({ + primaryOrigin: originGroup, + fallbackOrigin: failoverOrigin, + }); + + expect(() => { + new cloudfront.Distribution(stack, 'Distribution', { + defaultBehavior: { origin: groupOfGroups }, + }); + }).toThrow(/An OriginGroup cannot use an Origin with its own failover configuration as its primary origin!/); + }); + + test('cannot have an Origin with their own failover configuration as the fallback Origin', () => { + const originGroup = new origins.OriginGroup({ + primaryOrigin, + fallbackOrigin: new origins.S3Origin(s3.Bucket.fromBucketName(stack, 'ImportedBucket', 'imported-bucket')), + }); + const groupOfGroups = new origins.OriginGroup({ + primaryOrigin, + fallbackOrigin: originGroup, + }); + + expect(() => { + new cloudfront.Distribution(stack, 'Distribution', { + defaultBehavior: { origin: groupOfGroups }, + }); + }).toThrow(/An Origin cannot use an Origin with its own failover configuration as its fallback origin!/); + }); + + test('cannot have an empty array of fallbackStatusCodes', () => { + const failoverOrigin = new origins.S3Origin(s3.Bucket.fromBucketName(stack, 'ImportedBucket', 'imported-bucket')); + const originGroup = new origins.OriginGroup({ + primaryOrigin, + fallbackOrigin: failoverOrigin, + fallbackStatusCodes: [], + }); + + expect(() => { + new cloudfront.Distribution(stack, 'Distribution', { + defaultBehavior: { origin: originGroup }, + }); + }).toThrow(/fallbackStatusCodes cannot be empty/); + }); +}); diff --git a/packages/@aws-cdk/aws-cloudfront/README.md b/packages/@aws-cdk/aws-cloudfront/README.md index 44bfe03e680eb..f71cf9f971391 100644 --- a/packages/@aws-cdk/aws-cloudfront/README.md +++ b/packages/@aws-cdk/aws-cloudfront/README.md @@ -133,12 +133,11 @@ const myWebDistribution = new cloudfront.Distribution(this, 'myDist', { Additional behaviors can be specified at creation, or added after the initial creation. Each additional behavior is associated with an origin, and enable customization for a specific set of resources based on a URL path pattern. For example, we can add a behavior to `myWebDistribution` to -override the default time-to-live (TTL) for all of the images. +override the default viewer protocol policy for all of the images. ```ts myWebDistribution.addBehavior('/images/*.jpg', new origins.S3Origin(myBucket), { viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, - defaultTtl: cdk.Duration.days(7), }); ``` @@ -156,7 +155,6 @@ new cloudfront.Distribution(this, 'myDist', { '/images/*.jpg': { origin: bucketOrigin, viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, - defaultTtl: cdk.Duration.days(7), }, }, }); diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index 19516b8a59ae8..81cb0e7470ec4 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -132,6 +132,7 @@ export class Distribution extends Resource implements IDistribution { private readonly defaultBehavior: CacheBehavior; private readonly additionalBehaviors: CacheBehavior[] = []; private readonly boundOrigins: BoundOrigin[] = []; + private readonly originGroups: CfnDistribution.OriginGroupProperty[] = []; private readonly errorResponses: ErrorResponse[]; private readonly certificate?: acm.ICertificate; @@ -160,6 +161,7 @@ export class Distribution extends Resource implements IDistribution { const distribution = new CfnDistribution(this, 'CFDistribution', { distributionConfig: { enabled: true, origins: Lazy.anyValue({ produce: () => this.renderOrigins() }), + originGroups: Lazy.anyValue({ produce: () => this.renderOriginGroups() }), defaultCacheBehavior: this.defaultBehavior._renderBehavior(), cacheBehaviors: Lazy.anyValue({ produce: () => this.renderCacheBehaviors() }), viewerCertificate: this.certificate ? this.renderViewerCertificate(this.certificate) : undefined, @@ -187,7 +189,7 @@ export class Distribution extends Resource implements IDistribution { this.additionalBehaviors.push(new CacheBehavior(originId, { pathPattern, ...behaviorOptions })); } - private addOrigin(origin: IOrigin): string { + private addOrigin(origin: IOrigin, isFailoverOrigin: boolean = false): string { const existingOrigin = this.boundOrigins.find(boundOrigin => boundOrigin.origin === origin); if (existingOrigin) { return existingOrigin.originId; @@ -197,10 +199,41 @@ export class Distribution extends Resource implements IDistribution { const originId = scope.node.uniqueId; const originBindConfig = origin.bind(scope, { originId }); this.boundOrigins.push({ origin, originId, ...originBindConfig }); + if (originBindConfig.failoverConfig) { + if (isFailoverOrigin) { + throw new Error('An Origin cannot use an Origin with its own failover configuration as its fallback origin!'); + } + const failoverOriginId = this.addOrigin(originBindConfig.failoverConfig.failoverOrigin, true); + this.addOriginGroup(originBindConfig.failoverConfig.statusCodes, originId, failoverOriginId); + } return originId; } } + private addOriginGroup(statusCodes: number[] | undefined, originId: string, failoverOriginId: string): void { + statusCodes = statusCodes ?? [500, 502, 503, 504]; + if (statusCodes.length === 0) { + throw new Error('fallbackStatusCodes cannot be empty'); + } + const groupIndex = this.originGroups.length + 1; + this.originGroups.push({ + failoverCriteria: { + statusCodes: { + items: statusCodes, + quantity: statusCodes.length, + }, + }, + id: new Construct(this, `OriginGroup${groupIndex}`).node.uniqueId, + members: { + items: [ + { originId }, + { originId: failoverOriginId }, + ], + quantity: 2, + }, + }); + } + private renderOrigins(): CfnDistribution.OriginProperty[] { const renderedOrigins: CfnDistribution.OriginProperty[] = []; this.boundOrigins.forEach(boundOrigin => { @@ -211,6 +244,15 @@ export class Distribution extends Resource implements IDistribution { return renderedOrigins; } + private renderOriginGroups(): CfnDistribution.OriginGroupsProperty | undefined { + return this.originGroups.length === 0 + ? undefined + : { + items: this.originGroups, + quantity: this.originGroups.length, + }; + } + private renderCacheBehaviors(): CfnDistribution.CacheBehaviorProperty[] | undefined { if (this.additionalBehaviors.length === 0) { return undefined; } return this.additionalBehaviors.map(behavior => behavior._renderBehavior()); @@ -334,6 +376,21 @@ export class AllowedMethods { private constructor(methods: string[]) { this.methods = methods; } } +/** + * The HTTP methods that the Behavior will cache requests on. + */ +export class CachedMethods { + /** HEAD and GET */ + public static readonly CACHE_GET_HEAD = new CachedMethods(['GET', 'HEAD']); + /** HEAD, GET, and OPTIONS */ + public static readonly CACHE_GET_HEAD_OPTIONS = new CachedMethods(['GET', 'HEAD', 'OPTIONS']); + + /** HTTP methods supported */ + public readonly methods: string[]; + + private constructor(methods: string[]) { this.methods = methods; } +} + /** * Options for configuring custom error responses. * @@ -419,10 +476,26 @@ export interface AddBehaviorOptions { /** * HTTP methods to allow for this behavior. * - * @default - GET and HEAD + * @default AllowedMethods.ALLOW_GET_HEAD */ readonly allowedMethods?: AllowedMethods; + /** + * HTTP methods to cache for this behavior. + * + * @default CachedMethods.CACHE_GET_HEAD + */ + readonly cachedMethods?: CachedMethods; + + /** + * Whether you want CloudFront to automatically compress certain files for this cache behavior. + * See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/ServingCompressedFiles.html#compressed-content-cloudfront-file-types + * for file types CloudFront will compress. + * + * @default false + */ + readonly compress?: boolean; + /** * Whether CloudFront will forward query strings to the origin. * If this is set to true, CloudFront will forward all query parameters to the origin, and cache @@ -440,6 +513,20 @@ export interface AddBehaviorOptions { */ readonly forwardQueryStringCacheKeys?: string[]; + /** + * Set this to true to indicate you want to distribute media files in the Microsoft Smooth Streaming format using this behavior. + * + * @default false + */ + readonly smoothStreaming?: boolean; + + /** + * The protocol that viewers can use to access the files controlled by this behavior. + * + * @default ViewerProtocolPolicy.ALLOW_ALL + */ + readonly viewerProtocolPolicy?: ViewerProtocolPolicy; + /** * The Lambda@Edge functions to invoke before serving the contents. * diff --git a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts index 416b9f5bcc179..cee40aa4e149b 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts @@ -1,6 +1,22 @@ import { Construct, Duration, Token } from '@aws-cdk/core'; import { CfnDistribution } from './cloudfront.generated'; +/** + * The failover configuration used for Origin Groups, + * returned in {@link OriginBindConfig.failoverConfig}. + */ +export interface OriginFailoverConfig { + /** The origin to use as the fallback origin. */ + readonly failoverOrigin: IOrigin; + + /** + * The HTTP status codes of the response that trigger querying the failover Origin. + * + * @default - 500, 502, 503 and 504 + */ + readonly statusCodes?: number[]; +} + /** The struct returned from {@link IOrigin.bind}. */ export interface OriginBindConfig { /** @@ -9,6 +25,13 @@ export interface OriginBindConfig { * @default - nothing is returned */ readonly originProperty?: CfnDistribution.OriginProperty; + + /** + * The failover configuration for this Origin. + * + * @default - nothing is returned + */ + readonly failoverConfig?: OriginFailoverConfig; } /** @@ -86,8 +109,6 @@ export abstract class OriginBase implements IOrigin { private readonly connectionAttempts?: number; private readonly customHeaders?: Record; - private originId?: string; - protected constructor(domainName: string, props: OriginProps = {}) { validateIntInRangeOrUndefined('connectionTimeout', 1, 10, props.connectionTimeout?.toSeconds()); validateIntInRangeOrUndefined('connectionAttempts', 1, 3, props.connectionAttempts, false); @@ -99,22 +120,10 @@ export abstract class OriginBase implements IOrigin { this.customHeaders = props.customHeaders; } - /** - * The unique id for this origin. - * - * Cannot be accesed until bind() is called. - */ - public get id(): string { - if (!this.originId) { throw new Error('Cannot access originId until `bind` is called.'); } - return this.originId; - } - /** * Binds the origin to the associated Distribution. Can be used to grant permissions, create dependent resources, etc. */ public bind(_scope: Construct, options: OriginBindOptions): OriginBindConfig { - this.originId = options.originId; - const s3OriginConfig = this.renderS3OriginConfig(); const customOriginConfig = this.renderCustomOriginConfig(); @@ -124,7 +133,7 @@ export abstract class OriginBase implements IOrigin { return { originProperty: { domainName: this.domainName, - id: this.id, + id: options.originId, originPath: this.originPath, connectionAttempts: this.connectionAttempts, connectionTimeout: this.connectionTimeout?.toSeconds(), diff --git a/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts b/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts index 2e44abcfc05c0..6461d0fd54b0a 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts @@ -38,12 +38,15 @@ export class CacheBehavior { return { pathPattern: this.props.pathPattern, targetOriginId: this.originId, - allowedMethods: this.props.allowedMethods?.methods ?? undefined, + allowedMethods: this.props.allowedMethods?.methods, + cachedMethods: this.props.cachedMethods?.methods, + compress: this.props.compress, forwardedValues: { queryString: this.props.forwardQueryString ?? false, queryStringCacheKeys: this.props.forwardQueryStringCacheKeys, }, - viewerProtocolPolicy: ViewerProtocolPolicy.ALLOW_ALL, + smoothStreaming: this.props.smoothStreaming, + viewerProtocolPolicy: this.props.viewerProtocolPolicy ?? ViewerProtocolPolicy.ALLOW_ALL, lambdaFunctionAssociations: this.props.edgeLambdas ? this.props.edgeLambdas.map(edgeLambda => { if (edgeLambda.functionVersion.version === '$LATEST') { diff --git a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts index e4f91e7bcd58d..29942711ff781 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts @@ -307,7 +307,7 @@ describe('with Lambda@Edge functions', () => { { EventType: 'origin-request', LambdaFunctionARN: { - Ref: 'FunctionCurrentVersion4E2B22619c0305f954e58f25575548280c0a3629', + Ref: 'FunctionCurrentVersion4E2B2261477a5ae8059bbaa7813f752292c0f65e', }, }, ], @@ -341,7 +341,7 @@ describe('with Lambda@Edge functions', () => { { EventType: 'viewer-request', LambdaFunctionARN: { - Ref: 'FunctionCurrentVersion4E2B22619c0305f954e58f25575548280c0a3629', + Ref: 'FunctionCurrentVersion4E2B2261477a5ae8059bbaa7813f752292c0f65e', }, }, ], 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 4477a411cf790..2570e889d8587 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,6 +1,6 @@ import '@aws-cdk/assert/jest'; import { App, Stack } from '@aws-cdk/core'; -import { AllowedMethods } from '../../lib'; +import { AllowedMethods, CachedMethods, ViewerProtocolPolicy } from '../../lib'; import { CacheBehavior } from '../../lib/private/cache-behavior'; let app: App; @@ -29,18 +29,25 @@ test('renders with all properties specified', () => { const behavior = new CacheBehavior('origin_id', { pathPattern: '*', allowedMethods: AllowedMethods.ALLOW_ALL, + cachedMethods: CachedMethods.CACHE_GET_HEAD_OPTIONS, + compress: true, forwardQueryString: true, forwardQueryStringCacheKeys: ['user_id', 'auth'], + smoothStreaming: true, + viewerProtocolPolicy: ViewerProtocolPolicy.HTTPS_ONLY, }); expect(behavior._renderBehavior()).toEqual({ targetOriginId: 'origin_id', pathPattern: '*', allowedMethods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'PATCH', 'POST', 'DELETE'], + cachedMethods: ['GET', 'HEAD', 'OPTIONS'], + compress: true, forwardedValues: { queryString: true, queryStringCacheKeys: ['user_id', 'auth'], }, - viewerProtocolPolicy: 'allow-all', + smoothStreaming: true, + viewerProtocolPolicy: 'https-only', }); }); diff --git a/packages/@aws-cdk/aws-codecommit/test/test.codecommit.ts b/packages/@aws-cdk/aws-codecommit/test/test.codecommit.ts index bd7a6ac54ec0c..f920844f072a8 100644 --- a/packages/@aws-cdk/aws-codecommit/test/test.codecommit.ts +++ b/packages/@aws-cdk/aws-codecommit/test/test.codecommit.ts @@ -29,7 +29,7 @@ export = { 'all', ], DestinationArn: 'arn:aws:sns:*:123456789012:my_topic', - Name: 'MyRepository/arn:aws:sns:*:123456789012:my_topic', + Name: 'Default/MyRepository/arn:aws:sns:*:123456789012:my_topic', }, ], }, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.pipeline-actions.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.pipeline-actions.ts index 9a6b99351ee7b..043e42952e7c0 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.pipeline-actions.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.pipeline-actions.ts @@ -26,7 +26,7 @@ export = nodeunit.testCase({ actions: [action], }); - cdk.ConstructNode.prepare(stack.node); + app.synth(); _assertPermissionGranted(test, stack, pipelineRole.statements, 'iam:PassRole', action.deploymentRole.roleArn); diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index f5cf9df385869..2cb849d669f72 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -913,8 +913,6 @@ export class Table extends TableBase { this.encryptionKey = encryptionKey; - if (props.tableName) { this.node.addMetadata('aws:cdk:hasPhysicalName', props.tableName); } - this.tableArn = this.getResourceArnAttribute(this.table.attrArn, { service: 'dynamodb', resource: 'table', @@ -922,6 +920,8 @@ export class Table extends TableBase { }); this.tableName = this.getResourceNameAttribute(this.table.ref); + if (props.tableName) { this.node.addMetadata('aws:cdk:hasPhysicalName', this.tableName); } + this.tableStreamArn = streamSpecification ? this.table.attrStreamArn : undefined; this.scalingRole = this.makeScalingRole(); diff --git a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts index 05287f09adaa5..754a30150ace5 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts @@ -1,9 +1,9 @@ -import { ResourcePart, SynthUtils } from '@aws-cdk/assert'; +import { ABSENT, ResourcePart, SynthUtils } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; -import { App, CfnDeletionPolicy, ConstructNode, Duration, RemovalPolicy, Stack, Tag } from '@aws-cdk/core'; +import { App, CfnDeletionPolicy, ConstructNode, Duration, PhysicalName, RemovalPolicy, Stack, Tag } from '@aws-cdk/core'; import { Attribute, AttributeType, @@ -67,8 +67,12 @@ function* LSI_GENERATOR(): Generator { } describe('default properties', () => { + let stack: Stack; + beforeEach(() => { + stack = new Stack(); + }); + test('hash key only', () => { - const stack = new Stack(); new Table(stack, CONSTRUCT_NAME, { partitionKey: TABLE_PARTITION_KEY }); expect(stack).toHaveResource('AWS::DynamoDB::Table', { @@ -82,7 +86,6 @@ describe('default properties', () => { }); test('removalPolicy is DESTROY', () => { - const stack = new Stack(); new Table(stack, CONSTRUCT_NAME, { partitionKey: TABLE_PARTITION_KEY, removalPolicy: RemovalPolicy.DESTROY }); expect(stack).toHaveResource('AWS::DynamoDB::Table', { DeletionPolicy: CfnDeletionPolicy.DELETE }, ResourcePart.CompleteDefinition); @@ -90,7 +93,6 @@ describe('default properties', () => { }); test('hash + range key', () => { - const stack = new Stack(); new Table(stack, CONSTRUCT_NAME, { partitionKey: TABLE_PARTITION_KEY, sortKey: TABLE_SORT_KEY, @@ -110,8 +112,6 @@ describe('default properties', () => { }); test('hash + range key can also be specified in props', () => { - const stack = new Stack(); - new Table(stack, CONSTRUCT_NAME, { partitionKey: TABLE_PARTITION_KEY, sortKey: TABLE_SORT_KEY, @@ -132,7 +132,6 @@ describe('default properties', () => { }); test('point-in-time recovery is not enabled', () => { - const stack = new Stack(); new Table(stack, CONSTRUCT_NAME, { partitionKey: TABLE_PARTITION_KEY, sortKey: TABLE_SORT_KEY, @@ -154,7 +153,6 @@ describe('default properties', () => { }); test('server-side encryption is not enabled', () => { - const stack = new Stack(); new Table(stack, CONSTRUCT_NAME, { partitionKey: TABLE_PARTITION_KEY, sortKey: TABLE_SORT_KEY, @@ -176,7 +174,6 @@ describe('default properties', () => { }); test('stream is not enabled', () => { - const stack = new Stack(); new Table(stack, CONSTRUCT_NAME, { partitionKey: TABLE_PARTITION_KEY, sortKey: TABLE_SORT_KEY, @@ -198,7 +195,6 @@ describe('default properties', () => { }); test('ttl is not enabled', () => { - const stack = new Stack(); new Table(stack, CONSTRUCT_NAME, { partitionKey: TABLE_PARTITION_KEY, sortKey: TABLE_SORT_KEY, @@ -220,8 +216,6 @@ describe('default properties', () => { }); test('can specify new and old images', () => { - const stack = new Stack(); - new Table(stack, CONSTRUCT_NAME, { tableName: TABLE_NAME, readCapacity: 42, @@ -249,8 +243,6 @@ describe('default properties', () => { }); test('can specify new images only', () => { - const stack = new Stack(); - new Table(stack, CONSTRUCT_NAME, { tableName: TABLE_NAME, readCapacity: 42, @@ -278,8 +270,6 @@ describe('default properties', () => { }); test('can specify old images only', () => { - const stack = new Stack(); - new Table(stack, CONSTRUCT_NAME, { tableName: TABLE_NAME, readCapacity: 42, @@ -305,6 +295,19 @@ describe('default properties', () => { }, ); }); + + test('can use PhysicalName.GENERATE_IF_NEEDED as the Table name', () => { + new Table(stack, CONSTRUCT_NAME, { + tableName: PhysicalName.GENERATE_IF_NEEDED, + partitionKey: TABLE_PARTITION_KEY, + }); + + // 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, + }); + }); }); test('when specifying every property', () => { @@ -718,7 +721,7 @@ test('if an encryption key is included, encrypt/decrypt permissions are also add ], 'Version': '2012-10-17', }, - 'Description': 'Customer-managed key auto-created for encrypting DynamoDB table at Table A', + 'Description': 'Customer-managed key auto-created for encrypting DynamoDB table at Default/Table A', 'EnableKeyRotation': true, }, 'UpdateReplacePolicy': 'Retain', @@ -1755,7 +1758,7 @@ describe('grants', () => { const user = new iam.User(stack, 'user'); // WHEN - expect(() => table.grantTableListStreams(user)).toThrow(/DynamoDB Streams must be enabled on the table my-table/); + expect(() => table.grantTableListStreams(user)).toThrow(/DynamoDB Streams must be enabled on the table Default\/my-table/); }); test('"grantTableListStreams" allows principal to list all streams for this table', () => { @@ -1801,7 +1804,7 @@ describe('grants', () => { const user = new iam.User(stack, 'user'); // WHEN - expect(() => table.grantStreamRead(user)).toThrow(/DynamoDB Streams must be enabled on the table my-table/); + expect(() => table.grantStreamRead(user)).toThrow(/DynamoDB Streams must be enabled on the table Default\/my-table/); }); test('"grantStreamRead" allows principal to read and describe the table stream"', () => { diff --git a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts index 7f7862fce3539..c00326888faf8 100644 --- a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts +++ b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts @@ -151,6 +151,21 @@ export class WindowsImage implements IMachineImage { } } +/** + * CPU type + */ +export enum AmazonLinuxCpuType { + /** + * arm64 CPU type + */ + ARM_64 = 'arm64', + + /** + * x86_64 CPU type + */ + X86_64 = 'x86_64', +} + /** * Amazon Linux image properties */ @@ -189,6 +204,13 @@ export interface AmazonLinuxImageProps { * @default - Empty UserData for Linux machines */ readonly userData?: UserData; + + /** + * CPU Type + * + * @default X86_64 + */ + readonly cpuType?: AmazonLinuxCpuType; } /** @@ -206,12 +228,14 @@ export class AmazonLinuxImage implements IMachineImage { private readonly edition: AmazonLinuxEdition; private readonly virtualization: AmazonLinuxVirt; private readonly storage: AmazonLinuxStorage; + private readonly cpu: AmazonLinuxCpuType; constructor(private readonly props: AmazonLinuxImageProps = {}) { this.generation = (props && props.generation) || AmazonLinuxGeneration.AMAZON_LINUX; this.edition = (props && props.edition) || AmazonLinuxEdition.STANDARD; this.virtualization = (props && props.virtualization) || AmazonLinuxVirt.HVM; this.storage = (props && props.storage) || AmazonLinuxStorage.GENERAL_PURPOSE; + this.cpu = (props && props.cpuType) || AmazonLinuxCpuType.X86_64; } /** @@ -223,7 +247,7 @@ export class AmazonLinuxImage implements IMachineImage { 'ami', this.edition !== AmazonLinuxEdition.STANDARD ? this.edition : undefined, this.virtualization, - 'x86_64', // No 32-bits images vended through this + this.cpu, this.storage, ].filter(x => x !== undefined); // Get rid of undefineds diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index 8408073e94a1e..daaba4cd8a05f 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -1107,6 +1107,16 @@ export class Vpc extends VpcBase { public readonly internetConnectivityEstablished: IDependable; + /** + * Indicates if instances launched in this VPC will have public DNS hostnames. + */ + public readonly dnsHostnamesEnabled: boolean; + + /** + * Indicates if DNS support is enabled for this VPC. + */ + public readonly dnsSupportEnabled: boolean; + /** * The VPC resource */ @@ -1147,16 +1157,16 @@ export class Vpc extends VpcBase { this.networkBuilder = new NetworkBuilder(cidrBlock); - const enableDnsHostnames = props.enableDnsHostnames == null ? true : props.enableDnsHostnames; - const enableDnsSupport = props.enableDnsSupport == null ? true : props.enableDnsSupport; + this.dnsHostnamesEnabled = props.enableDnsHostnames == null ? true : props.enableDnsHostnames; + this.dnsSupportEnabled = props.enableDnsSupport == null ? true : props.enableDnsSupport; const instanceTenancy = props.defaultInstanceTenancy || 'default'; this.internetConnectivityEstablished = this._internetConnectivityEstablished; // Define a VPC using the provided CIDR range this.resource = new CfnVPC(this, 'Resource', { cidrBlock, - enableDnsHostnames, - enableDnsSupport, + enableDnsHostnames: this.dnsHostnamesEnabled, + enableDnsSupport: this.dnsSupportEnabled, instanceTenancy, }); diff --git a/packages/@aws-cdk/aws-ec2/test/connections.test.ts b/packages/@aws-cdk/aws-ec2/test/connections.test.ts index 2721f073cf6b0..591d543827084 100644 --- a/packages/@aws-cdk/aws-ec2/test/connections.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/connections.test.ts @@ -1,5 +1,5 @@ import { expect, haveResource } from '@aws-cdk/assert'; -import { App, ConstructNode, Stack } from '@aws-cdk/core'; +import { App, Stack } from '@aws-cdk/core'; import { nodeunitShim, Test } from 'nodeunit-shim'; import { @@ -78,7 +78,7 @@ nodeunitShim({ // THEN expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { - GroupDescription: 'SecurityGroup1', + GroupDescription: 'Default/SecurityGroup1', SecurityGroupIngress: [ { Description: 'from 0.0.0.0/0:88', @@ -91,7 +91,7 @@ nodeunitShim({ })); expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { - GroupDescription: 'SecurityGroup2', + GroupDescription: 'Default/SecurityGroup2', SecurityGroupIngress: [ { Description: 'from 0.0.0.0/0:88', @@ -185,7 +185,7 @@ nodeunitShim({ sg2.connections.allowFrom(sg1, Port.tcp(100)); // THEN -- both rules are in Stack2 - ConstructNode.prepare(app.node); + app.synth(); expect(stack2).to(haveResource('AWS::EC2::SecurityGroupIngress', { GroupId: { 'Fn::GetAtt': [ 'SecurityGroupDD263621', 'GroupId' ] }, @@ -216,7 +216,7 @@ nodeunitShim({ sg2.connections.allowTo(sg1, Port.tcp(100)); // THEN -- both rules are in Stack2 - ConstructNode.prepare(app.node); + app.synth(); expect(stack2).to(haveResource('AWS::EC2::SecurityGroupIngress', { GroupId: { 'Fn::ImportValue': 'Stack1:ExportsOutputFnGetAttSecurityGroupDD263621GroupIdDF6F8B09' }, @@ -249,7 +249,7 @@ nodeunitShim({ sg2.connections.allowFrom(sg1b, Port.tcp(100)); // THEN -- both egress rules are in Stack2 - ConstructNode.prepare(app.node); + app.synth(); expect(stack2).to(haveResource('AWS::EC2::SecurityGroupEgress', { GroupId: { 'Fn::ImportValue': 'Stack1:ExportsOutputFnGetAttSecurityGroupAED40ADC5GroupId1D10C76A' }, 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 29f5c26de4c26..7e5e4924097f7 100644 --- a/packages/@aws-cdk/aws-ec2/test/example.images.lit.ts +++ b/packages/@aws-cdk/aws-ec2/test/example.images.lit.ts @@ -8,6 +8,7 @@ const amznLinux = ec2.MachineImage.latestAmazonLinux({ edition: ec2.AmazonLinuxEdition.STANDARD, virtualization: ec2.AmazonLinuxVirt.HVM, storage: ec2.AmazonLinuxStorage.GENERAL_PURPOSE, + cpuType: ec2.AmazonLinuxCpuType.X86_64, }); // Pick a Windows edition to use diff --git a/packages/@aws-cdk/aws-ec2/test/userdata.test.ts b/packages/@aws-cdk/aws-ec2/test/userdata.test.ts index 4f4a357d55e37..5be98eb41668c 100644 --- a/packages/@aws-cdk/aws-ec2/test/userdata.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/userdata.test.ts @@ -52,7 +52,7 @@ nodeunitShim({ test.equals(rendered, 'trap {\n' + '$success=($PSItem.Exception.Message -eq "Success")\n' + - 'cfn-signal --stack Stack --resource RESOURCE1989552F --region ${Token[AWS::Region.4]} --success ($success.ToString().ToLower())\n' + + 'cfn-signal --stack Default --resource RESOURCE1989552F --region ${Token[AWS::Region.4]} --success ($success.ToString().ToLower())\n' + 'break\n' + '}\n' + 'command1\n' + @@ -157,7 +157,7 @@ nodeunitShim({ test.equals(rendered, '#!/bin/bash\n' + 'function exitTrap(){\n' + 'exitCode=$?\n' + - '/opt/aws/bin/cfn-signal --stack Stack --resource RESOURCE1989552F --region ${Token[AWS::Region.4]} -e $exitCode || echo \'Failed to send Cloudformation Signal\'\n' + + '/opt/aws/bin/cfn-signal --stack Default --resource RESOURCE1989552F --region ${Token[AWS::Region.4]} -e $exitCode || echo \'Failed to send Cloudformation Signal\'\n' + '}\n' + 'trap exitTrap EXIT\n' + 'command1'); 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 b0e63a42a4874..a1c8e39c28141 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts @@ -260,7 +260,7 @@ nodeunitShim({ })); expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { - GroupDescription: 'VpcNetwork/EcrDocker/SecurityGroup', + GroupDescription: 'Default/VpcNetwork/EcrDocker/SecurityGroup', VpcId: { Ref: 'VpcNetworkB258E83A', }, diff --git a/packages/@aws-cdk/aws-ec2/test/vpc.test.ts b/packages/@aws-cdk/aws-ec2/test/vpc.test.ts index 4bd8ab4049db3..113a8eb7edd56 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc.test.ts @@ -32,11 +32,11 @@ nodeunitShim({ new Vpc(stack, 'TheVPC'); expect(stack).to( haveResource('AWS::EC2::VPC', - hasTags( [ {Key: 'Name', Value: 'TheVPC'} ])), + hasTags( [ {Key: 'Name', Value: 'TestStack/TheVPC'} ])), ); expect(stack).to( haveResource('AWS::EC2::InternetGateway', - hasTags( [ {Key: 'Name', Value: 'TheVPC'} ])), + hasTags( [ {Key: 'Name', Value: 'TestStack/TheVPC'} ])), ); test.done(); }, @@ -61,6 +61,46 @@ nodeunitShim({ test.done(); }, + 'dns getters correspond to CFN properties': (() => { + + const tests: any = { }; + + const inputs = [ + {dnsSupport: false, dnsHostnames: false}, + // {dnsSupport: false, dnsHostnames: true} - this configuration is illegal so its not part of the permutations. + {dnsSupport: true, dnsHostnames: false}, + {dnsSupport: true, dnsHostnames: true}, + ]; + + for (const input of inputs) { + + tests[`[dnsSupport=${input.dnsSupport},dnsHostnames=${input.dnsHostnames}]`] = (test: Test) => { + + const stack = getTestStack(); + const vpc = new Vpc(stack, 'TheVPC', { + cidr: '192.168.0.0/16', + enableDnsHostnames: input.dnsHostnames, + enableDnsSupport: input.dnsSupport, + defaultInstanceTenancy: DefaultInstanceTenancy.DEDICATED, + }); + + expect(stack).to(haveResource('AWS::EC2::VPC', { + CidrBlock: '192.168.0.0/16', + EnableDnsHostnames: input.dnsHostnames, + EnableDnsSupport: input.dnsSupport, + InstanceTenancy: DefaultInstanceTenancy.DEDICATED, + })); + + test.equal(input.dnsSupport, vpc.dnsSupportEnabled); + test.equal(input.dnsHostnames, vpc.dnsHostnamesEnabled); + test.done(); + + }; + } + + return tests; + })(), + 'contains the correct number of subnets'(test: Test) { const stack = getTestStack(); const vpc = new Vpc(stack, 'TheVPC'); @@ -458,7 +498,7 @@ nodeunitShim({ for (let i = 1; i < 4; i++) { expect(stack).to(haveResource('AWS::EC2::Subnet', hasTags([{ Key: 'Name', - Value: `VPC/egressSubnet${i}`, + Value: `TestStack/VPC/egressSubnet${i}`, }, { Key: 'aws-cdk:subnet-name', Value: 'egress', diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts index 27da4d31e3be0..3869d38d8826a 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts @@ -42,6 +42,13 @@ export interface ApplicationLoadBalancedServiceBaseProps { */ readonly publicLoadBalancer?: boolean; + /** + * Determines whether or not the Security Group for the Load Balancer's Listener will be open to all traffic by default. + * + * @default true -- The security group allows ingress from all IP addresses. + */ + readonly openListener?: boolean; + /** * The desired number of instantiations of the task definition to keep running on the service. * The minimum value is 1 @@ -323,7 +330,7 @@ export abstract class ApplicationLoadBalancedServiceBase extends cdk.Construct { this.listener = loadBalancer.addListener('PublicListener', { protocol, port: props.listenerPort, - open: true, + open: props.openListener ?? true, }); this.targetGroup = this.listener.addTargets('ECS', targetProps); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.multiple-application-load-balanced-ecs-service.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.multiple-application-load-balanced-ecs-service.expected.json index 9af53dfb65b39..7afecdb128746 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.multiple-application-load-balanced-ecs-service.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.multiple-application-load-balanced-ecs-service.expected.json @@ -441,13 +441,39 @@ "Statement": [ { "Action": [ - "ecs:CreateCluster", "ecs:DeregisterContainerInstance", - "ecs:DiscoverPollEndpoint", - "ecs:Poll", "ecs:RegisterContainerInstance", - "ecs:StartTelemetrySession", - "ecs:Submit*", + "ecs:Submit*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "ClusterEB0386A7", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:Poll", + "ecs:StartTelemetrySession" + ], + "Effect": "Allow", + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "ClusterEB0386A7", + "Arn" + ] + } + } + } + }, + { + "Action": [ + "ecs:DiscoverPollEndpoint", "ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents" @@ -632,7 +658,17 @@ "ecs:DescribeTasks" ], "Effect": "Allow", - "Resource": "*" + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "ClusterEB0386A7", + "Arn" + ] + } + } + } }, { "Action": [ diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.expected.json index 7cddaef40ca08..e9b8c11f8754a 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.expected.json @@ -261,21 +261,46 @@ "Statement": [ { "Action": [ - "ecs:CreateCluster", "ecs:DeregisterContainerInstance", - "ecs:DiscoverPollEndpoint", - "ecs:Poll", "ecs:RegisterContainerInstance", - "ecs:StartTelemetrySession", - "ecs:Submit*", + "ecs:Submit*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:Poll", + "ecs:StartTelemetrySession" + ], + "Effect": "Allow", + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } + }, + { + "Action": [ + "ecs:DiscoverPollEndpoint", "ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents" ], "Effect": "Allow", "Resource": "*" - } - ], + } ], "Version": "2012-10-17" }, "PolicyName": "EcsClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy04DC6C80", @@ -449,7 +474,17 @@ "ecs:DescribeTasks" ], "Effect": "Allow", - "Resource": "*" + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } }, { "Action": [ diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts index 71736c651573b..581c1841b7429 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import { arrayWith, expect, haveResource, haveResourceLike, objectLike } from '@aws-cdk/assert'; import { Certificate } from '@aws-cdk/aws-certificatemanager'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; @@ -1048,4 +1048,58 @@ export = { test.done(); }, + + 'test ECS loadbalanced construct default/open security group'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + // WHEN + new ecsPatterns.ApplicationLoadBalancedEc2Service(stack, 'Service', { + cluster, + memoryReservationMiB: 1024, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('test'), + }, + }); + + // THEN - Stack contains no ingress security group rules + expect(stack).to(haveResourceLike('AWS::EC2::SecurityGroup', { + SecurityGroupIngress: [{ + CidrIp: '0.0.0.0/0', + FromPort: 80, + IpProtocol: 'tcp', + ToPort: 80, + }], + })); + + test.done(); + }, + + 'test ECS loadbalanced construct closed security group'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + // WHEN + new ecsPatterns.ApplicationLoadBalancedEc2Service(stack, 'Service', { + cluster, + memoryReservationMiB: 1024, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('test'), + }, + openListener: false, + }); + + // THEN - Stack contains no ingress security group rules + expect(stack).notTo(haveResourceLike('AWS::EC2::SecurityGroup', { + SecurityGroupIngress: arrayWith(objectLike({})), + })); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts index b0b8a21705ddb..4b30dfa721480 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts @@ -515,7 +515,8 @@ export = { 'passing in imported network load balancer and resources to NLB Fargate service'(test: Test) { // GIVEN - const stack1 = new cdk.Stack(); + const app = new cdk.App(); + const stack1 = new cdk.Stack(app, 'MyStack'); const vpc1 = new ec2.Vpc(stack1, 'VPC'); const cluster1 = new ecs.Cluster(stack1, 'Cluster', { vpc: vpc1 }); const nlbArn = 'arn:aws:elasticloadbalancing::000000000000::dummyloadbalancer'; diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index a16f4410af706..0fff4fd87bd93 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -212,16 +212,41 @@ export class Cluster extends Resource implements ICluster { // ECS instances must be able to do these things // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html + // But, scoped down to minimal permissions required. + // Notes: + // - 'ecs:CreateCluster' removed. The cluster already exists. autoScalingGroup.addToRolePolicy(new iam.PolicyStatement({ actions: [ - 'ecs:CreateCluster', 'ecs:DeregisterContainerInstance', - 'ecs:DiscoverPollEndpoint', - 'ecs:Poll', 'ecs:RegisterContainerInstance', - 'ecs:StartTelemetrySession', 'ecs:Submit*', + ], + resources: [ + this.clusterArn, + ], + })); + autoScalingGroup.addToRolePolicy(new iam.PolicyStatement({ + actions: [ + // These act on a cluster instance, and the instance doesn't exist until the service starts. + // Thus, scope to the cluster using a condition. + // See: https://docs.aws.amazon.com/IAM/latest/UserGuide/list_amazonelasticcontainerservice.html + 'ecs:Poll', + 'ecs:StartTelemetrySession', + ], + resources: ['*'], + conditions: { + ArnEquals: { 'ecs:cluster': this.clusterArn }, + }, + })); + autoScalingGroup.addToRolePolicy(new iam.PolicyStatement({ + actions: [ + // These do not support resource constraints, and must be resource '*' + 'ecs:DiscoverPollEndpoint', 'ecr:GetAuthorizationToken', + // Preserved for backwards compatibility. + // Users are able to enable cloudwatch agent using CDK. Existing + // customers might be installing CW agent as part of user-data so if we + // remove these permissions we will break that customer use cases. 'logs:CreateLogStream', 'logs:PutLogEvents', ], diff --git a/packages/@aws-cdk/aws-ecs/lib/drain-hook/instance-drain-hook.ts b/packages/@aws-cdk/aws-ecs/lib/drain-hook/instance-drain-hook.ts index 34f402fb97ab1..f8062befb60d4 100644 --- a/packages/@aws-cdk/aws-ecs/lib/drain-hook/instance-drain-hook.ts +++ b/packages/@aws-cdk/aws-ecs/lib/drain-hook/instance-drain-hook.ts @@ -90,6 +90,9 @@ export class InstanceDrainHook extends cdk.Construct { fn.addToRolePolicy(new iam.PolicyStatement({ actions: ['ecs:DescribeContainerInstances', 'ecs:DescribeTasks'], resources: ['*'], + conditions: { + ArnEquals: { 'ecs:cluster': props.cluster.clusterArn }, + }, })); // Restrict to the ECS Cluster diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.app-mesh-proxy-config.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.app-mesh-proxy-config.expected.json index c644c86649ef6..24784b1521a45 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.app-mesh-proxy-config.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.app-mesh-proxy-config.expected.json @@ -420,13 +420,39 @@ "Statement": [ { "Action": [ - "ecs:CreateCluster", "ecs:DeregisterContainerInstance", - "ecs:DiscoverPollEndpoint", - "ecs:Poll", "ecs:RegisterContainerInstance", - "ecs:StartTelemetrySession", - "ecs:Submit*", + "ecs:Submit*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:Poll", + "ecs:StartTelemetrySession" + ], + "Effect": "Allow", + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } + }, + { + "Action": [ + "ecs:DiscoverPollEndpoint", "ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents" @@ -611,7 +637,17 @@ "ecs:DescribeTasks" ], "Effect": "Allow", - "Resource": "*" + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } }, { "Action": [ diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.clb-host-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.clb-host-nw.expected.json index edb849319d5b8..80111c4451470 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.clb-host-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.clb-host-nw.expected.json @@ -441,13 +441,39 @@ "Statement": [ { "Action": [ - "ecs:CreateCluster", "ecs:DeregisterContainerInstance", - "ecs:DiscoverPollEndpoint", - "ecs:Poll", "ecs:RegisterContainerInstance", - "ecs:StartTelemetrySession", - "ecs:Submit*", + "ecs:Submit*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:Poll", + "ecs:StartTelemetrySession" + ], + "Effect": "Allow", + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } + }, + { + "Action": [ + "ecs:DiscoverPollEndpoint", "ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents" @@ -632,7 +658,17 @@ "ecs:DescribeTasks" ], "Effect": "Allow", - "Resource": "*" + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } }, { "Action": [ diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.firelens-s3-config.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.firelens-s3-config.expected.json index a379d7c8c3109..ba3354e59a7ef 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.firelens-s3-config.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.firelens-s3-config.expected.json @@ -420,13 +420,39 @@ "Statement": [ { "Action": [ - "ecs:CreateCluster", "ecs:DeregisterContainerInstance", - "ecs:DiscoverPollEndpoint", - "ecs:Poll", "ecs:RegisterContainerInstance", - "ecs:StartTelemetrySession", - "ecs:Submit*", + "ecs:Submit*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:Poll", + "ecs:StartTelemetrySession" + ], + "Effect": "Allow", + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } + }, + { + "Action": [ + "ecs:DiscoverPollEndpoint", "ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents" @@ -611,7 +637,17 @@ "ecs:DescribeTasks" ], "Effect": "Allow", - "Resource": "*" + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } }, { "Action": [ diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json index e5e7464767c36..20b2143e7ebfa 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json @@ -420,13 +420,39 @@ "Statement": [ { "Action": [ - "ecs:CreateCluster", "ecs:DeregisterContainerInstance", - "ecs:DiscoverPollEndpoint", - "ecs:Poll", "ecs:RegisterContainerInstance", - "ecs:StartTelemetrySession", - "ecs:Submit*", + "ecs:Submit*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:Poll", + "ecs:StartTelemetrySession" + ], + "Effect": "Allow", + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } + }, + { + "Action": [ + "ecs:DiscoverPollEndpoint", "ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents" @@ -611,7 +637,17 @@ "ecs:DescribeTasks" ], "Effect": "Allow", - "Resource": "*" + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } }, { "Action": [ diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json index 74b289cadbde6..ae18cb651155e 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json @@ -441,13 +441,39 @@ "Statement": [ { "Action": [ - "ecs:CreateCluster", "ecs:DeregisterContainerInstance", - "ecs:DiscoverPollEndpoint", - "ecs:Poll", "ecs:RegisterContainerInstance", - "ecs:StartTelemetrySession", - "ecs:Submit*", + "ecs:Submit*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:Poll", + "ecs:StartTelemetrySession" + ], + "Effect": "Allow", + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } + }, + { + "Action": [ + "ecs:DiscoverPollEndpoint", "ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents" @@ -632,7 +658,17 @@ "ecs:DescribeTasks" ], "Effect": "Allow", - "Resource": "*" + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } }, { "Action": [ diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json index 46f3b9a4e26cf..66d961bceb04b 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json @@ -420,13 +420,39 @@ "Statement": [ { "Action": [ - "ecs:CreateCluster", "ecs:DeregisterContainerInstance", - "ecs:DiscoverPollEndpoint", - "ecs:Poll", "ecs:RegisterContainerInstance", - "ecs:StartTelemetrySession", - "ecs:Submit*", + "ecs:Submit*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:Poll", + "ecs:StartTelemetrySession" + ], + "Effect": "Allow", + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } + }, + { + "Action": [ + "ecs:DiscoverPollEndpoint", "ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents" @@ -611,7 +637,17 @@ "ecs:DescribeTasks" ], "Effect": "Allow", - "Resource": "*" + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } }, { "Action": [ diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json index 16d9538eb2127..2214c69632d98 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json @@ -420,13 +420,39 @@ "Statement": [ { "Action": [ - "ecs:CreateCluster", "ecs:DeregisterContainerInstance", - "ecs:DiscoverPollEndpoint", - "ecs:Poll", "ecs:RegisterContainerInstance", - "ecs:StartTelemetrySession", - "ecs:Submit*", + "ecs:Submit*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:Poll", + "ecs:StartTelemetrySession" + ], + "Effect": "Allow", + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } + }, + { + "Action": [ + "ecs:DiscoverPollEndpoint", "ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents" @@ -611,7 +637,17 @@ "ecs:DescribeTasks" ], "Effect": "Allow", - "Resource": "*" + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } }, { "Action": [ diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.expected.json index 71a61e7a4a4a7..3c78f85f86425 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.expected.json @@ -420,13 +420,39 @@ "Statement": [ { "Action": [ - "ecs:CreateCluster", "ecs:DeregisterContainerInstance", - "ecs:DiscoverPollEndpoint", - "ecs:Poll", "ecs:RegisterContainerInstance", - "ecs:StartTelemetrySession", - "ecs:Submit*", + "ecs:Submit*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:Poll", + "ecs:StartTelemetrySession" + ], + "Effect": "Allow", + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } + }, + { + "Action": [ + "ecs:DiscoverPollEndpoint", "ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents" @@ -613,7 +639,17 @@ "ecs:DescribeTasks" ], "Effect": "Allow", - "Resource": "*" + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } }, { "Action": [ @@ -866,13 +902,39 @@ "Statement": [ { "Action": [ - "ecs:CreateCluster", "ecs:DeregisterContainerInstance", - "ecs:DiscoverPollEndpoint", - "ecs:Poll", "ecs:RegisterContainerInstance", - "ecs:StartTelemetrySession", - "ecs:Submit*", + "ecs:Submit*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:Poll", + "ecs:StartTelemetrySession" + ], + "Effect": "Allow", + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } + }, + { + "Action": [ + "ecs:DiscoverPollEndpoint", "ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents" @@ -1058,7 +1120,17 @@ "ecs:DescribeTasks" ], "Effect": "Allow", - "Resource": "*" + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } }, { "Action": [ diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts index 454d904f7592f..991901512a6d2 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts @@ -66,7 +66,7 @@ export = { })); expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { - GroupDescription: 'FargateService/SecurityGroup', + GroupDescription: 'Default/FargateService/SecurityGroup', SecurityGroupEgress: [ { CidrIp: '0.0.0.0/0', @@ -897,7 +897,7 @@ export = { protocol: ecs.Protocol.TCP, })], }); - }, /Container 'FargateTaskDef\/MainContainer' has no mapping for port 8001 and protocol tcp. Did you call "container.addPortMappings\(\)"\?/); + }, /Container 'Default\/FargateTaskDef\/MainContainer' has no mapping for port 8001 and protocol tcp. Did you call "container.addPortMappings\(\)"\?/); test.done(); }, @@ -932,7 +932,7 @@ export = { containerPort: 8002, })], }); - }, /Container 'FargateTaskDef\/MainContainer' has no mapping for port 8002 and protocol tcp. Did you call "container.addPortMappings\(\)"\?/); + }, /Container 'Default\/FargateTaskDef\/MainContainer' has no mapping for port 8002 and protocol tcp. Did you call "container.addPortMappings\(\)"\?/); test.done(); }, diff --git a/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts index d0be3430ceb50..9eb4ca0ed3bc0 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts @@ -26,7 +26,7 @@ export = { Tags: [ { Key: 'Name', - Value: 'EcsCluster/Vpc', + Value: 'Default/EcsCluster/Vpc', }, ], })); @@ -74,7 +74,7 @@ export = { { Key: 'Name', PropagateAtLaunch: true, - Value: 'EcsCluster/DefaultAutoScalingGroup', + Value: 'Default/EcsCluster/DefaultAutoScalingGroup', }, ], VPCZoneIdentifier: [ @@ -88,7 +88,7 @@ export = { })); expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { - GroupDescription: 'EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup', + GroupDescription: 'Default/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup', SecurityGroupEgress: [ { CidrIp: '0.0.0.0/0', @@ -99,7 +99,7 @@ export = { Tags: [ { Key: 'Name', - Value: 'EcsCluster/DefaultAutoScalingGroup', + Value: 'Default/EcsCluster/DefaultAutoScalingGroup', }, ], VpcId: { @@ -127,13 +127,39 @@ export = { Statement: [ { Action: [ - 'ecs:CreateCluster', 'ecs:DeregisterContainerInstance', - 'ecs:DiscoverPollEndpoint', - 'ecs:Poll', 'ecs:RegisterContainerInstance', - 'ecs:StartTelemetrySession', 'ecs:Submit*', + ], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'EcsCluster97242B84', + 'Arn', + ], + }, + }, + { + Action: [ + 'ecs:Poll', + 'ecs:StartTelemetrySession', + ], + Effect: 'Allow', + Resource: '*', + Condition: { + ArnEquals: { + 'ecs:cluster': { + 'Fn::GetAtt': [ + 'EcsCluster97242B84', + 'Arn', + ], + }, + }, + }, + }, + { + Action: [ + 'ecs:DiscoverPollEndpoint', 'ecr:GetAuthorizationToken', 'logs:CreateLogStream', 'logs:PutLogEvents', @@ -171,7 +197,7 @@ export = { Tags: [ { Key: 'Name', - Value: 'MyVpc', + Value: 'Default/MyVpc', }, ], })); @@ -219,7 +245,7 @@ export = { { Key: 'Name', PropagateAtLaunch: true, - Value: 'EcsCluster/DefaultAutoScalingGroup', + Value: 'Default/EcsCluster/DefaultAutoScalingGroup', }, ], VPCZoneIdentifier: [ @@ -233,7 +259,7 @@ export = { })); expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { - GroupDescription: 'EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup', + GroupDescription: 'Default/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup', SecurityGroupEgress: [ { CidrIp: '0.0.0.0/0', @@ -244,7 +270,7 @@ export = { Tags: [ { Key: 'Name', - Value: 'EcsCluster/DefaultAutoScalingGroup', + Value: 'Default/EcsCluster/DefaultAutoScalingGroup', }, ], VpcId: { @@ -272,13 +298,39 @@ export = { Statement: [ { Action: [ - 'ecs:CreateCluster', 'ecs:DeregisterContainerInstance', - 'ecs:DiscoverPollEndpoint', - 'ecs:Poll', 'ecs:RegisterContainerInstance', - 'ecs:StartTelemetrySession', 'ecs:Submit*', + ], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'EcsCluster97242B84', + 'Arn', + ], + }, + }, + { + Action: [ + 'ecs:Poll', + 'ecs:StartTelemetrySession', + ], + Effect: 'Allow', + Resource: '*', + Condition: { + ArnEquals: { + 'ecs:cluster': { + 'Fn::GetAtt': [ + 'EcsCluster97242B84', + 'Arn', + ], + }, + }, + }, + }, + { + Action: [ + 'ecs:DiscoverPollEndpoint', 'ecr:GetAuthorizationToken', 'logs:CreateLogStream', 'logs:PutLogEvents', @@ -392,6 +444,16 @@ export = { ], Effect: 'Allow', Resource: '*', + Condition: { + ArnEquals: { + 'ecs:cluster': { + 'Fn::GetAtt': [ + 'EcsCluster97242B84', + 'Arn', + ], + }, + }, + }, }, { Action: [ @@ -471,7 +533,7 @@ export = { Tags: [ { Key: 'Name', - Value: 'MyVpc', + Value: 'Default/MyVpc', }, ], })); @@ -519,7 +581,7 @@ export = { { Key: 'Name', PropagateAtLaunch: true, - Value: 'EcsCluster/DefaultAutoScalingGroup', + Value: 'Default/EcsCluster/DefaultAutoScalingGroup', }, ], VPCZoneIdentifier: [ @@ -533,7 +595,7 @@ export = { })); expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { - GroupDescription: 'EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup', + GroupDescription: 'Default/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup', SecurityGroupEgress: [ { CidrIp: '0.0.0.0/0', @@ -544,7 +606,7 @@ export = { Tags: [ { Key: 'Name', - Value: 'EcsCluster/DefaultAutoScalingGroup', + Value: 'Default/EcsCluster/DefaultAutoScalingGroup', }, ], VpcId: { @@ -572,13 +634,39 @@ export = { Statement: [ { Action: [ - 'ecs:CreateCluster', 'ecs:DeregisterContainerInstance', - 'ecs:DiscoverPollEndpoint', - 'ecs:Poll', 'ecs:RegisterContainerInstance', - 'ecs:StartTelemetrySession', 'ecs:Submit*', + ], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'EcsCluster97242B84', + 'Arn', + ], + }, + }, + { + Action: [ + 'ecs:Poll', + 'ecs:StartTelemetrySession', + ], + Effect: 'Allow', + Resource: '*', + Condition: { + ArnEquals: { + 'ecs:cluster': { + 'Fn::GetAtt': [ + 'EcsCluster97242B84', + 'Arn', + ], + }, + }, + }, + }, + { + Action: [ + 'ecs:DiscoverPollEndpoint', 'ecr:GetAuthorizationToken', 'logs:CreateLogStream', 'logs:PutLogEvents', @@ -1228,7 +1316,7 @@ export = { Tags: [ { Key: 'Name', - Value: 'MyPublicVpc', + Value: 'Default/MyPublicVpc', }, ], })); @@ -1277,7 +1365,7 @@ export = { { Key: 'Name', PropagateAtLaunch: true, - Value: 'EcsCluster/DefaultAutoScalingGroup', + Value: 'Default/EcsCluster/DefaultAutoScalingGroup', }, ], VPCZoneIdentifier: [ @@ -1291,7 +1379,7 @@ export = { })); expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { - GroupDescription: 'EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup', + GroupDescription: 'Default/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup', SecurityGroupEgress: [ { CidrIp: '0.0.0.0/0', @@ -1302,7 +1390,7 @@ export = { Tags: [ { Key: 'Name', - Value: 'EcsCluster/DefaultAutoScalingGroup', + Value: 'Default/EcsCluster/DefaultAutoScalingGroup', }, ], VpcId: { diff --git a/packages/@aws-cdk/aws-eks/README.md b/packages/@aws-cdk/aws-eks/README.md index ee73d0bedcba5..003167058532b 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -1,4 +1,5 @@ ## Amazon EKS Construct Library + --- @@ -29,6 +30,7 @@ const cluster = new eks.Cluster(this, 'hello-eks', { version: eks.KubernetesVersion.V1_16, }); +// apply a kubernetes manifest to the cluster cluster.addResource('mypod', { apiVersion: 'v1', kind: 'Pod', @@ -45,6 +47,59 @@ cluster.addResource('mypod', { }); ``` +In order to interact with your cluster through `kubectl`, you can use the `aws +eks update-kubeconfig` [AWS CLI command](https://docs.aws.amazon.com/cli/latest/reference/eks/update-kubeconfig.html) +to configure your local kubeconfig. + +The EKS module will define a CloudFormation output in your stack which contains +the command to run. For example: + +``` +Outputs: +ClusterConfigCommand43AAE40F = aws eks update-kubeconfig --name cluster-xxxxx --role-arn arn:aws:iam::112233445566:role/yyyyy +``` + +> The IAM role specified in this command is called the "**masters role**". This is +> an IAM role that is associated with the `system:masters` [RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) +> group and has super-user access to the cluster. +> +> You can specify this role using the `mastersRole` option, or otherwise a role will be +> automatically created for you. This role can be assumed by anyone in the account with +> `sts:AssumeRole` permissions for this role. + +Execute the `aws eks update-kubeconfig ...` command in your terminal to create a +local kubeconfig: + +```console +$ aws eks update-kubeconfig --name cluster-xxxxx --role-arn arn:aws:iam::112233445566:role/yyyyy +Added new context arn:aws:eks:rrrrr:112233445566:cluster/cluster-xxxxx to /home/boom/.kube/config +``` + +And now you can simply use `kubectl`: + +```console +$ kubectl get all -n kube-system +NAME READY STATUS RESTARTS AGE +pod/aws-node-fpmwv 1/1 Running 0 21m +pod/aws-node-m9htf 1/1 Running 0 21m +pod/coredns-5cb4fb54c7-q222j 1/1 Running 0 23m +pod/coredns-5cb4fb54c7-v9nxx 1/1 Running 0 23m +... +``` + +### Endpoint Access + +You can configure the [cluster endpoint access](https://docs.aws.amazon.com/eks/latest/userguide/cluster-endpoint.html) by using the `endpointAccess` property: + +```typescript +const cluster = new eks.Cluster(this, 'hello-eks', { + version: eks.KubernetesVersion.V1_16, + endpointAccess: eks.EndpointAccess.PRIVATE // No access outside of your VPC. +}); +``` + +The default value is `eks.EndpointAccess.PUBLIC_AND_PRIVATE`. Which means the cluster endpoint is accessible from outside of your VPC, and worker node traffic to the endpoint will stay within your VPC. + ### Capacity By default, `eks.Cluster` is created with a managed nodegroup with x2 `m5.large` instances. You must specify the kubernetes version for the cluster with the `version` property. @@ -78,7 +133,7 @@ new eks.Cluster(this, 'cluster', { To disable the default capacity, simply set `defaultCapacity` to `0`: ```ts -new eks.Cluster(this, 'cluster-with-no-capacity', { +new eks.Cluster(this, 'cluster-with-no-capacity', { defaultCapacity: 0, version: eks.KubernetesVersion.V1_16, }); @@ -105,8 +160,8 @@ cluster.addCapacity('frontend-nodes', { ### Managed Node Groups -Amazon EKS managed node groups automate the provisioning and lifecycle management of nodes (Amazon EC2 instances) -for Amazon EKS Kubernetes clusters. By default, `eks.Nodegroup` create a nodegroup with x2 `t3.medium` instances. +Amazon EKS managed node groups automate the provisioning and lifecycle management of nodes (Amazon EC2 instances) +for Amazon EKS Kubernetes clusters. By default, `eks.Nodegroup` create a nodegroup with x2 `t3.medium` instances. ```ts new eks.Nodegroup(stack, 'nodegroup', { cluster }); @@ -128,7 +183,7 @@ AWS Fargate is a technology that provides on-demand, right-sized compute capacity for containers. With AWS Fargate, you no longer have to provision, configure, or scale groups of virtual machines to run containers. This removes the need to choose server types, decide when to scale your node groups, or -optimize cluster packing. +optimize cluster packing. You can control which pods start on Fargate and how they run with Fargate Profiles, which are defined as part of your Amazon EKS cluster. @@ -194,7 +249,6 @@ When adding capacity, you can specify options for which is responsible for associating the node to the EKS cluster. For example, you can use `kubeletExtraArgs` to add custom node labels or taints. - ```ts // up to ten spot instances cluster.addCapacity('spot', { @@ -210,90 +264,6 @@ cluster.addCapacity('spot', { To disable bootstrapping altogether (i.e. to fully customize user-data), set `bootstrapEnabled` to `false` when you add the capacity. -### Masters Role - -The Amazon EKS construct library allows you to specify an IAM role that will be -granted `system:masters` privileges on your cluster. - -Without specifying a `mastersRole`, you will not be able to interact manually -with the cluster. - -The following example defines an IAM role that can be assumed by all users -in the account and shows how to use the `mastersRole` property to map this -role to the Kubernetes `system:masters` group: - -```ts -// first define the role -const clusterAdmin = new iam.Role(this, 'AdminRole', { - assumedBy: new iam.AccountRootPrincipal() -}); - -// now define the cluster and map role to "masters" RBAC group -new eks.Cluster(this, 'Cluster', { - mastersRole: clusterAdmin, - version: eks.KubernetesVersion.V1_16, -}); -``` - -When you `cdk deploy` this CDK app, you will notice that an output will be printed -with the `update-kubeconfig` command. - -Something like this: - -``` -Outputs: -eks-integ-defaults.ClusterConfigCommand43AAE40F = aws eks update-kubeconfig --name cluster-ba7c166b-c4f3-421c-bf8a-6812e4036a33 --role-arn arn:aws:iam::112233445566:role/eks-integ-defaults-Role1ABCC5F0-1EFK2W5ZJD98Y -``` - -Copy & paste the "`aws eks update-kubeconfig ...`" command to your shell in -order to connect to your EKS cluster with the "masters" role. - -Now, given [AWS CLI](https://aws.amazon.com/cli/) is configured to use AWS -credentials for a user that is trusted by the masters role, you should be able -to interact with your cluster through `kubectl` (the above example will trust -all users in the account). - -For example: - -```console -$ aws eks update-kubeconfig --name cluster-ba7c166b-c4f3-421c-bf8a-6812e4036a33 --role-arn arn:aws:iam::112233445566:role/eks-integ-defaults-Role1ABCC5F0-1EFK2W5ZJD98Y -Added new context arn:aws:eks:eu-west-2:112233445566:cluster/cluster-ba7c166b-c4f3-421c-bf8a-6812e4036a33 to /Users/boom/.kube/config - -$ kubectl get nodes # list all nodes -NAME STATUS ROLES AGE VERSION -ip-10-0-147-66.eu-west-2.compute.internal Ready 21m v1.13.7-eks-c57ff8 -ip-10-0-169-151.eu-west-2.compute.internal Ready 21m v1.13.7-eks-c57ff8 - -$ kubectl get all -n kube-system -NAME READY STATUS RESTARTS AGE -pod/aws-node-fpmwv 1/1 Running 0 21m -pod/aws-node-m9htf 1/1 Running 0 21m -pod/coredns-5cb4fb54c7-q222j 1/1 Running 0 23m -pod/coredns-5cb4fb54c7-v9nxx 1/1 Running 0 23m -pod/kube-proxy-d4jrh 1/1 Running 0 21m -pod/kube-proxy-q7hh7 1/1 Running 0 21m - -NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE -service/kube-dns ClusterIP 172.20.0.10 53/UDP,53/TCP 23m - -NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE -daemonset.apps/aws-node 2 2 2 2 2 23m -daemonset.apps/kube-proxy 2 2 2 2 2 23m - -NAME READY UP-TO-DATE AVAILABLE AGE -deployment.apps/coredns 2/2 2 2 23m - -NAME DESIRED CURRENT READY AGE -replicaset.apps/coredns-5cb4fb54c7 2 2 2 23m -``` - -For your convenience, an AWS CloudFormation output will automatically be -included in your template and will be printed when running `cdk deploy`. - -**NOTE**: if the cluster is configured with `kubectlEnabled: false`, it -will be created with the role/user that created the AWS CloudFormation -stack. See [Kubectl Support](#kubectl-support) for details. - ### Kubernetes Resources The `KubernetesResource` construct or `cluster.addResource` method can be used @@ -348,6 +318,20 @@ new KubernetesResource(this, 'hello-kub', { cluster.addResource('hello-kub', service, deployment); ``` +##### Kubectl Environment + +The resources are created in the cluster by running `kubectl apply` from a python lambda function. You can configure the environment of this function by specifying it at cluster instantiation. For example, this can useful in order to configure an http proxy: + +```typescript +const cluster = new eks.Cluster(this, 'hello-eks', { + version: eks.KubernetesVersion.V1_16, + kubectlEnvironment: { + 'http_proxy': 'http://proxy.myproxy.com' + } +}); + +``` + #### Adding resources from a URL The following example will deploy the resource manifest hosting on remote server: @@ -427,8 +411,6 @@ Furthermore, when auto-scaling capacity is added to the cluster (through of the auto-scaling group will be automatically mapped to RBAC so nodes can connect to the cluster. No manual mapping is required any longer. -> NOTE: `cluster.awsAuth` will throw an error if your cluster is created with `kubectlEnabled: false`. - For example, let's say you want to grant an IAM user administrative privileges on your cluster: @@ -483,68 +465,6 @@ If you want to SSH into nodes in a private subnet, you should set up a bastion host in a public subnet. That setup is recommended, but is unfortunately beyond the scope of this documentation. -### kubectl Support - -When you create an Amazon EKS cluster, the IAM entity user or role, such as a -[federated user](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers.html) -that creates the cluster, is automatically granted `system:masters` permissions -in the cluster's RBAC configuration. - -In order to allow programmatically defining **Kubernetes resources** in your AWS -CDK app and provisioning them through AWS CloudFormation, we will need to assume -this "masters" role every time we want to issue `kubectl` operations against your -cluster. - -At the moment, the [AWS::EKS::Cluster](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-cluster.html) -AWS CloudFormation resource does not support this behavior, so in order to -support "programmatic kubectl", such as applying manifests -and mapping IAM roles from within your CDK application, the Amazon EKS -construct library uses a custom resource for provisioning the cluster. -This custom resource is executed with an IAM role that we can then use -to issue `kubectl` commands. - -The default behavior of this library is to use this custom resource in order -to retain programmatic control over the cluster. In other words: to allow -you to define Kubernetes resources in your CDK code instead of having to -manage your Kubernetes applications through a separate system. - -One of the implications of this design is that, by default, the user who -provisioned the AWS CloudFormation stack (executed `cdk deploy`) will -not have administrative privileges on the EKS cluster. - -1. Additional resources will be synthesized into your template (the AWS Lambda - function, the role and policy). -2. As described in [Interacting with Your Cluster](#interacting-with-your-cluster), - if you wish to be able to manually interact with your cluster, you will need - to map an IAM role or user to the `system:masters` group. This can be either - done by specifying a `mastersRole` when the cluster is defined, calling - `cluster.awsAuth.addMastersRole` or explicitly mapping an IAM role or IAM user to the - relevant Kubernetes RBAC groups using `cluster.addRoleMapping` and/or - `cluster.addUserMapping`. - -If you wish to disable the programmatic kubectl behavior and use the standard -AWS::EKS::Cluster resource, you can specify `kubectlEnabled: false` when you define -the cluster: - -```ts -new eks.Cluster(this, 'cluster', { - kubectlEnabled: false -}); -``` - -**Take care**: a change in this property will cause the cluster to be destroyed -and a new cluster to be created. - -When kubectl is disabled, you should be aware of the following: - -1. When you log-in to your cluster, you don't need to specify `--role-arn` as - long as you are using the same user that created the cluster. -2. As described in the Amazon EKS User Guide, you will need to manually - edit the [aws-auth ConfigMap](https://docs.aws.amazon.com/eks/latest/userguide/add-user-role.html) - when you add capacity in order to map the IAM instance role to RBAC to allow nodes to join the cluster. -3. Any `eks.Cluster` APIs that depend on programmatic kubectl support will fail - with an error: `cluster.addResource`, `cluster.addChart`, `cluster.awsAuth`, `props.mastersRole`. - ### Helm Charts The `HelmChart` construct or `cluster.addChart` method can be used 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 d88c8900e3020..48c7baa85ac6a 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 @@ -261,7 +261,22 @@ export class ClusterResourceHandler extends ResourceHandler { } function parseProps(props: any): aws.EKS.CreateClusterRequest { - return props?.Config ?? { }; + + const parsed = props?.Config ?? { }; + + // this is weird but these boolean properties are passed by CFN as a string, and we need them to be booleanic for the SDK. + // Otherwise it fails with 'Unexpected Parameter: params.resourcesVpcConfig.endpointPrivateAccess is expected to be a boolean' + + if (typeof(parsed.resourcesVpcConfig?.endpointPrivateAccess) === 'string') { + parsed.resourcesVpcConfig.endpointPrivateAccess = parsed.resourcesVpcConfig.endpointPrivateAccess === 'true'; + } + + if (typeof(parsed.resourcesVpcConfig?.endpointPublicAccess) === 'string') { + parsed.resourcesVpcConfig.endpointPublicAccess = parsed.resourcesVpcConfig.endpointPublicAccess === 'true'; + } + + return parsed; + } interface UpdateMap { @@ -280,6 +295,9 @@ function analyzeUpdate(oldProps: Partial, newProps const newVpcProps = newProps.resourcesVpcConfig || { }; const oldVpcProps = oldProps.resourcesVpcConfig || { }; + const oldPublicAccessCidrs = new Set(oldVpcProps.publicAccessCidrs ?? []); + const newPublicAccessCidrs = new Set(newVpcProps.publicAccessCidrs ?? []); + return { replaceName: newProps.name !== oldProps.name, replaceVpc: @@ -287,9 +305,14 @@ function analyzeUpdate(oldProps: Partial, newProps JSON.stringify(newVpcProps.securityGroupIds) !== JSON.stringify(oldVpcProps.securityGroupIds), updateAccess: newVpcProps.endpointPrivateAccess !== oldVpcProps.endpointPrivateAccess || - newVpcProps.endpointPublicAccess !== oldVpcProps.endpointPublicAccess, + newVpcProps.endpointPublicAccess !== oldVpcProps.endpointPublicAccess || + !setsEqual(newPublicAccessCidrs, oldPublicAccessCidrs), replaceRole: newProps.roleArn !== oldProps.roleArn, updateVersion: newProps.version !== oldProps.version, updateLogging: JSON.stringify(newProps.logging) !== JSON.stringify(oldProps.logging), }; } + +function setsEqual(first: Set, second: Set) { + return first.size === second.size || [...first].every((e: string) => second.has(e)); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts index 13b493808d0b0..c885feaa476d2 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts @@ -2,7 +2,26 @@ import * as iam from '@aws-cdk/aws-iam'; import { ArnComponents, Construct, CustomResource, Lazy, Stack, Token } from '@aws-cdk/core'; import { CLUSTER_RESOURCE_TYPE } from './cluster-resource-handler/consts'; import { ClusterResourceProvider } from './cluster-resource-provider'; -import { CfnClusterProps } from './eks.generated'; +import { CfnClusterProps, CfnCluster } from './eks.generated'; + +export interface ClusterResourceProps extends CfnClusterProps { + + /** + * Enable private endpoint access to the cluster. + */ + readonly endpointPrivateAccess: boolean; + + /** + * Enable public endpoint access to the cluster. + */ + readonly endpointPublicAccess: boolean; + + /** + * Limit public address with CIDR blocks. + */ + readonly publicAccessCidrs?: string[]; + +} /** * A low-level CFN resource Amazon EKS cluster implemented through a custom @@ -32,7 +51,7 @@ export class ClusterResource extends Construct { private readonly trustedPrincipals: string[] = []; - constructor(scope: Construct, id: string, props: CfnClusterProps) { + constructor(scope: Construct, id: string, props: ClusterResourceProps) { super(scope, id); const stack = Stack.of(this); @@ -117,7 +136,21 @@ export class ClusterResource extends Construct { resourceType: CLUSTER_RESOURCE_TYPE, serviceToken: provider.serviceToken, properties: { - Config: props, + // the structure of config needs to be that of 'aws.EKS.CreateClusterRequest' since its passed as is + // to the eks.createCluster sdk invocation. + Config: { + name: props.name, + version: props.version, + roleArn: props.roleArn, + encryptionConfig: props.encryptionConfig, + resourcesVpcConfig: { + subnetIds: (props.resourcesVpcConfig as CfnCluster.ResourcesVpcConfigProperty).subnetIds, + securityGroupIds: (props.resourcesVpcConfig as CfnCluster.ResourcesVpcConfigProperty).securityGroupIds, + endpointPublicAccess: props.endpointPublicAccess, + endpointPrivateAccess: props.endpointPrivateAccess, + publicAccessCidrs: props.publicAccessCidrs, + }, + }, AssumeRoleArn: this.creationRole.roleArn, // IMPORTANT: increment this number when you add new attributes to the diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index 64338e877075b..f572bc6e35c98 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -8,12 +8,12 @@ import { CfnOutput, CfnResource, Construct, IResource, Resource, Stack, Tag, Tok import * as YAML from 'yaml'; import { AwsAuth } from './aws-auth'; import { clusterArnComponents, ClusterResource } from './cluster-resource'; -import { CfnCluster, CfnClusterProps } from './eks.generated'; +import { CfnClusterProps } from './eks.generated'; import { FargateProfile, FargateProfileOptions } from './fargate-profile'; import { HelmChart, HelmChartOptions } from './helm-chart'; import { KubernetesPatch } from './k8s-patch'; import { KubernetesResource } from './k8s-resource'; -import { KubectlProvider } from './kubectl-provider'; +import { KubectlProvider, KubectlProviderProps } from './kubectl-provider'; import { Nodegroup, NodegroupOptions } from './managed-nodegroup'; import { ServiceAccount, ServiceAccountOptions } from './service-account'; import { LifecycleLabel, renderAmazonLinuxUserData, renderBottlerocketUserData } from './user-data'; @@ -118,9 +118,9 @@ export interface ClusterAttributes { /** * Options for configuring an EKS cluster. */ -export interface ClusterOptions { +export interface CommonClusterOptions { /** - * The VPC in which to create the Cluster + * The VPC in which to create the Cluster. * * @default - a VPC with default configuration will be created and can be accessed through `cluster.vpc`. */ @@ -169,17 +169,36 @@ export interface ClusterOptions { */ readonly version: KubernetesVersion; + /** + * Determines whether a CloudFormation output with the name of the cluster + * will be synthesized. + * + * @default false + */ + readonly outputClusterName?: boolean; + + /** + * Determines whether a CloudFormation output with the `aws eks + * update-kubeconfig` command will be synthesized. This command will include + * the cluster name and, if applicable, the ARN of the masters IAM role. + * + * @default true + */ + readonly outputConfigCommand?: boolean; +} + +/** + * Options for EKS clusters. + */ +export interface ClusterOptions extends CommonClusterOptions { /** * An IAM role that will be added to the `system:masters` Kubernetes RBAC * group. * * @see https://kubernetes.io/docs/reference/access-authn-authz/rbac/#default-roles-and-role-bindings * - * @default - By default, it will only possible to update this Kubernetes - * system by adding resources to this cluster via `addResource` or - * by defining `KubernetesResource` resources in your AWS CDK app. - * Use this if you wish to grant cluster administration privileges - * to another role. + * @default - a role that assumable by anyone with permissions in the same + * account will automatically be defined */ readonly mastersRole?: iam.IRole; @@ -193,55 +212,143 @@ export interface ClusterOptions { readonly coreDnsComputeType?: CoreDnsComputeType; /** - * Determines whether a CloudFormation output with the name of the cluster - * will be synthesized. + * Determines whether a CloudFormation output with the ARN of the "masters" + * IAM role will be synthesized (if `mastersRole` is specified). * * @default false */ - readonly outputClusterName?: boolean; + readonly outputMastersRoleArn?: boolean; /** - * Determines whether a CloudFormation output with the ARN of the "masters" - * IAM role will be synthesized (if `mastersRole` is specified). + * Configure access to the Kubernetes API server endpoint.. * - * @default false + * @see https://docs.aws.amazon.com/eks/latest/userguide/cluster-endpoint.html + * + * @default EndpointAccess.PUBLIC_AND_PRIVATE */ - readonly outputMastersRoleArn?: boolean; + readonly endpointAccess?: EndpointAccess; /** - * Determines whether a CloudFormation output with the `aws eks - * update-kubeconfig` command will be synthesized. This command will include - * the cluster name and, if applicable, the ARN of the masters IAM role. + * Environment variables for the kubectl execution. Only relevant for kubectl enabled clusters. * - * @default true + * @default - No environment variables. */ - readonly outputConfigCommand?: boolean; + readonly kubectlEnvironment?: { [key: string]: string }; } /** - * Configuration props for EKS clusters. + * Group access configuration together. */ -export interface ClusterProps extends ClusterOptions { +interface EndpointAccessConfig { + + /** + * Indicates if private access is enabled. + */ + readonly privateAccess: boolean; + + /** + * Indicates if public access is enabled. + */ + readonly publicAccess: boolean; + /** + * Public access is allowed only from these CIDR blocks. + * An empty array means access is open to any address. + * + * @default - No restrictions. + */ + readonly publicCidrs?: string[]; + +} + +/** + * Endpoint access characteristics. + */ +export class EndpointAccess { + + /** + * The cluster endpoint is accessible from outside of your VPC. + * Worker node traffic will leave your VPC to connect to the endpoint. + * + * By default, the endpoint is exposed to all adresses. You can optionally limit the CIDR blocks that can access the public endpoint using the `PUBLIC.onlyFrom` method. + * If you limit access to specific CIDR blocks, you must ensure that the CIDR blocks that you + * specify include the addresses that worker nodes and Fargate pods (if you use them) + * access the public endpoint from. + * + * @param cidr The CIDR blocks. + */ + public static readonly PUBLIC = new EndpointAccess({privateAccess: false, publicAccess: true}); + + /** + * The cluster endpoint is only accessible through your VPC. + * Worker node traffic to the endpoint will stay within your VPC. + */ + public static readonly PRIVATE = new EndpointAccess({privateAccess: true, publicAccess: false}); + + /** + * The cluster endpoint is accessible from outside of your VPC. + * Worker node traffic to the endpoint will stay within your VPC. + * + * By default, the endpoint is exposed to all adresses. You can optionally limit the CIDR blocks that can access the public endpoint using the `PUBLIC_AND_PRIVATE.onlyFrom` method. + * If you limit access to specific CIDR blocks, you must ensure that the CIDR blocks that you + * specify include the addresses that worker nodes and Fargate pods (if you use them) + * access the public endpoint from. + * + * @param cidr The CIDR blocks. + */ + public static readonly PUBLIC_AND_PRIVATE = new EndpointAccess({privateAccess: true, publicAccess: true}); + + private constructor( + /** + * Configuration properties. + * + * @internal + */ + public readonly _config: EndpointAccessConfig) { + if (!_config.publicAccess && _config.publicCidrs && _config.publicCidrs.length > 0) { + throw new Error('CIDR blocks can only be configured when public access is enabled'); + } + } + /** - * Allows defining `kubectrl`-related resources on this cluster. + * Restrict public access to specific CIDR blocks. + * If public access is disabled, this method will result in an error. * - * If this is disabled, it will not be possible to use the following - * capabilities: - * - `addResource` - * - `addRoleMapping` - * - `addUserMapping` - * - `addMastersRole` and `props.mastersRole` + * @param cidr CIDR blocks. + */ + public onlyFrom(...cidr: string[]) { + return new EndpointAccess({ + ...this._config, + // override CIDR + publicCidrs: cidr, + }); + } +} + +/** + * Common configuration props for EKS clusters. + */ +export interface ClusterProps extends ClusterOptions { + /** + * NOT SUPPORTED: We no longer allow disabling kubectl-support. Setting this + * option to `false` will throw an error. * - * If this is disabled, the cluster can only be managed by issuing `kubectl` - * commands from a session that uses the IAM role/user that created the - * account. + * To temporary allow you to retain existing clusters created with + * `kubectlEnabled: false`, you can use `eks.LegacyCluster` class, which is a + * drop-in replacement for `eks.Cluster` with `kubectlEnabled: false`. * - * _NOTE_: changing this value will destoy the cluster. This is because a - * managable cluster must be created using an AWS CloudFormation custom - * resource which executes with an IAM role owned by the CDK app. + * Bear in mind that this is a temporary workaround. We have plans to remove + * `eks.LegacyCluster`. If you have a use case for using `eks.LegacyCluster`, + * please add a comment here https://github.com/aws/aws-cdk/issues/9332 and + * let us know so we can make sure to continue to support your use case with + * `eks.Cluster`. This issue also includes additional context into why this + * class is being removed. * - * @default true The cluster can be managed by the AWS CDK application. + * @deprecated `eks.LegacyCluster` is __temporarily__ provided as a drop-in + * replacement until you are able to migrate to `eks.Cluster`. + * + * @see https://github.com/aws/aws-cdk/issues/9332 + * @default true */ readonly kubectlEnabled?: boolean; @@ -381,11 +488,6 @@ export class Cluster extends Resource implements ICluster { */ public readonly role: iam.IRole; - /** - * Indicates if `kubectl` related operations can be performed on this cluster. - */ - public readonly kubectlEnabled: boolean; - /** * The auto scaling group that hosts the default capacity for this cluster. * This will be `undefined` if the `defaultCapacityType` is not `EC2` or @@ -411,7 +513,7 @@ export class Cluster extends Resource implements ICluster { * that manages it. If this cluster is not kubectl-enabled (i.e. uses the * stock `CfnCluster`), this is `undefined`. */ - private readonly _clusterResource?: ClusterResource; + private readonly _clusterResource: ClusterResource; /** * Manages the aws-auth config map. @@ -424,6 +526,14 @@ export class Cluster extends Resource implements ICluster { private _neuronDevicePlugin?: KubernetesResource; + private readonly endpointAccess: EndpointAccess; + + private readonly kubctlProviderSecurityGroup: ec2.ISecurityGroup; + + private readonly vpcSubnets: ec2.SubnetSelection[]; + + private readonly kubectlProviderEnv?: { [key: string]: string }; + private readonly version: KubernetesVersion; /** @@ -452,6 +562,14 @@ export class Cluster extends Resource implements ICluster { physicalName: props.clusterName, }); + if (props.kubectlEnabled === false) { + throw new Error( + 'The "eks.Cluster" class no longer allows disabling kubectl support. ' + + 'As a temporary workaround, you can use the drop-in replacement class `eks.LegacyCluster`, ' + + 'but bear in mind that this class will soon be removed and will no longer receive additional ' + + 'features or bugfixes. See https://github.com/aws/aws-cdk/issues/9332 for more details'); + } + const stack = Stack.of(this); this.vpc = props.vpc || new ec2.Vpc(this, 'DefaultVpc'); @@ -477,9 +595,10 @@ export class Cluster extends Resource implements ICluster { defaultPort: ec2.Port.tcp(443), // Control Plane has an HTTPS API }); + this.vpcSubnets = props.vpcSubnets ?? [{ subnetType: ec2.SubnetType.PUBLIC }, { subnetType: ec2.SubnetType.PRIVATE }]; + // Get subnetIds for all selected subnets - const placements = props.vpcSubnets || [{ subnetType: ec2.SubnetType.PUBLIC }, { subnetType: ec2.SubnetType.PRIVATE }]; - const subnetIds = [...new Set(Array().concat(...placements.map(s => this.vpc.selectSubnets(s).subnetIds)))]; + const subnetIds = [...new Set(Array().concat(...this.vpcSubnets.map(s => this.vpc.selectSubnets(s).subnetIds)))]; const clusterProps: CfnClusterProps = { name: this.physicalName, @@ -491,37 +610,57 @@ export class Cluster extends Resource implements ICluster { }, }; - let resource; - this.kubectlEnabled = props.kubectlEnabled === undefined ? true : props.kubectlEnabled; - if (this.kubectlEnabled) { - resource = new ClusterResource(this, 'Resource', clusterProps); - this._clusterResource = resource; - - // see https://github.com/aws/aws-cdk/issues/9027 - this._clusterResource.creationRole.addToPolicy(new iam.PolicyStatement({ - actions: ['ec2:DescribeVpcs'], - resources: [ stack.formatArn({ - service: 'ec2', - resource: 'vpc', - resourceName: this.vpc.vpcId, - })], - })); - - // we use an SSM parameter as a barrier because it's free and fast. - this._kubectlReadyBarrier = new CfnResource(this, 'KubectlReadyBarrier', { - type: 'AWS::SSM::Parameter', - properties: { - Type: 'String', - Value: 'aws:cdk:eks:kubectl-ready', - }, - }); + this.endpointAccess = props.endpointAccess ?? EndpointAccess.PUBLIC_AND_PRIVATE; + this.kubectlProviderEnv = props.kubectlEnvironment; - // add the cluster resource itself as a dependency of the barrier - this._kubectlReadyBarrier.node.addDependency(this._clusterResource); - } else { - resource = new CfnCluster(this, 'Resource', clusterProps); + if (this.endpointAccess._config.privateAccess && this.vpc instanceof ec2.Vpc) { + // validate VPC properties according to: https://docs.aws.amazon.com/eks/latest/userguide/cluster-endpoint.html + if (!this.vpc.dnsHostnamesEnabled || !this.vpc.dnsSupportEnabled) { + throw new Error('Private endpoint access requires the VPC to have DNS support and DNS hostnames enabled. Use `enableDnsHostnames: true` and `enableDnsSupport: true` when creating the VPC.'); + } } + this.kubctlProviderSecurityGroup = new ec2.SecurityGroup(this, 'KubectlProviderSecurityGroup', { + vpc: this.vpc, + description: 'Comminication between KubectlProvider and EKS Control Plane', + }); + + // grant the kubectl provider access to the cluster control plane. + this.connections.allowFrom(this.kubctlProviderSecurityGroup, this.connections.defaultPort!); + + const resource = this._clusterResource = new ClusterResource(this, 'Resource', { + ...clusterProps, + endpointPrivateAccess: this.endpointAccess._config.privateAccess, + endpointPublicAccess: this.endpointAccess._config.publicAccess, + publicAccessCidrs: this.endpointAccess._config.publicCidrs, + }); + + // the security group and vpc must exist in order to properly delete the cluster (since we run `kubectl delete`). + // this ensures that. + this._clusterResource.node.addDependency(this.kubctlProviderSecurityGroup, this.vpc); + + // see https://github.com/aws/aws-cdk/issues/9027 + this._clusterResource.creationRole.addToPolicy(new iam.PolicyStatement({ + actions: ['ec2:DescribeVpcs'], + resources: [ stack.formatArn({ + service: 'ec2', + resource: 'vpc', + resourceName: this.vpc.vpcId, + })], + })); + + // we use an SSM parameter as a barrier because it's free and fast. + this._kubectlReadyBarrier = new CfnResource(this, 'KubectlReadyBarrier', { + type: 'AWS::SSM::Parameter', + properties: { + Type: 'String', + Value: 'aws:cdk:eks:kubectl-ready', + }, + }); + + // add the cluster resource itself as a dependency of the barrier + this._kubectlReadyBarrier.node.addDependency(this._clusterResource); + this.clusterName = this.getResourceNameAttribute(resource.ref); this.clusterArn = this.getResourceArnAttribute(resource.attrArn, clusterArnComponents(this.physicalName)); @@ -538,21 +677,22 @@ export class Cluster extends Resource implements ICluster { new CfnOutput(this, 'ClusterName', { value: this.clusterName }); } - // map the IAM role to the `system:masters` group. - if (props.mastersRole) { - if (!this.kubectlEnabled) { - throw new Error('Cannot specify a "masters" role if kubectl is disabled'); - } - - this.awsAuth.addMastersRole(props.mastersRole); + // if an explicit role is not configured, define a masters role that can + // be assumed by anyone in the account (with sts:AssumeRole permissions of + // course) + const mastersRole = props.mastersRole ?? new iam.Role(this, 'MastersRole', { + assumedBy: new iam.AccountRootPrincipal(), + }); - if (props.outputMastersRoleArn) { - new CfnOutput(this, 'MastersRoleArn', { value: props.mastersRole.roleArn }); - } + // map the IAM role to the `system:masters` group. + this.awsAuth.addMastersRole(mastersRole); - commonCommandOptions.push(`--role-arn ${props.mastersRole.roleArn}`); + if (props.outputMastersRoleArn) { + new CfnOutput(this, 'MastersRoleArn', { value: mastersRole.roleArn }); } + commonCommandOptions.push(`--role-arn ${mastersRole.roleArn}`); + // allocate default capacity if non-zero (or default). const minCapacity = props.defaultCapacity === undefined ? DEFAULT_CAPACITY_COUNT : props.defaultCapacity; if (minCapacity > 0) { @@ -571,9 +711,7 @@ export class Cluster extends Resource implements ICluster { new CfnOutput(this, 'GetTokenCommand', { value: `${getTokenCommandPrefix} ${postfix}` }); } - if (this.kubectlEnabled) { - this.defineCoreDnsComputeType(props.coreDnsComputeType ?? CoreDnsComputeType.EC2); - } + this.defineCoreDnsComputeType(props.coreDnsComputeType ?? CoreDnsComputeType.EC2); } /** @@ -692,14 +830,10 @@ export class Cluster extends Resource implements ICluster { applyToLaunchedInstances: true, }); - if (options.mapRole === true && !this.kubectlEnabled) { - throw new Error('Cannot map instance IAM role to RBAC if kubectl is disabled for the cluster'); - } - // do not attempt to map the role if `kubectl` is not enabled for this // cluster or if `mapRole` is set to false. By default this should happen. const mapRole = options.mapRole === undefined ? true : options.mapRole; - if (mapRole && this.kubectlEnabled) { + if (mapRole) { // see https://docs.aws.amazon.com/en_us/eks/latest/userguide/add-user-role.html this.awsAuth.addRoleMapping(autoScalingGroup.role, { username: 'system:node:{{EC2PrivateDNSName}}', @@ -717,7 +851,7 @@ export class Cluster extends Resource implements ICluster { } // if this is an ASG with spot instances, install the spot interrupt handler (only if kubectl is enabled). - if (autoScalingGroup.spotPrice && this.kubectlEnabled) { + if (autoScalingGroup.spotPrice) { this.addSpotInterruptHandler(); } } @@ -726,10 +860,6 @@ export class Cluster extends Resource implements ICluster { * Lazily creates the AwsAuth resource, which manages AWS authentication mapping. */ public get awsAuth() { - if (!this.kubectlEnabled) { - throw new Error('Cannot define aws-auth mappings if kubectl is disabled'); - } - if (!this._awsAuth) { this._awsAuth = new AwsAuth(this, 'AwsAuth', { cluster: this }); } @@ -745,10 +875,6 @@ export class Cluster extends Resource implements ICluster { * @attribute */ public get clusterOpenIdConnectIssuerUrl(): string { - if (!this._clusterResource) { - throw new Error('unable to obtain OpenID Connect issuer URL. Cluster must be kubectl-enabled'); - } - return this._clusterResource.attrOpenIdConnectIssuerUrl; } @@ -760,10 +886,6 @@ export class Cluster extends Resource implements ICluster { * @attribute */ public get clusterOpenIdConnectIssuer(): string { - if (!this._clusterResource) { - throw new Error('unable to obtain OpenID Connect issuer. Cluster must be kubectl-enabled'); - } - return this._clusterResource.attrOpenIdConnectIssuer; } @@ -774,10 +896,6 @@ export class Cluster extends Resource implements ICluster { * A provider will only be defined if this property is accessed (lazy initialization). */ public get openIdConnectProvider() { - if (!this.kubectlEnabled) { - throw new Error('Cannot specify a OpenID Connect Provider if kubectl is disabled'); - } - if (!this._openIdConnectProvider) { this._openIdConnectProvider = new iam.OpenIdConnectProvider(this, 'OpenIdConnectProvider', { url: this.clusterOpenIdConnectIssuerUrl, @@ -802,7 +920,6 @@ export class Cluster extends Resource implements ICluster { * @param id logical id of this manifest * @param manifest a list of Kubernetes resource specifications * @returns a `KubernetesResource` object. - * @throws If `kubectlEnabled` is `false` */ public addResource(id: string, ...manifest: any[]) { return new KubernetesResource(this, `manifest-${id}`, { cluster: this, manifest }); @@ -814,7 +931,6 @@ export class Cluster extends Resource implements ICluster { * @param id logical id of this chart. * @param options options of this chart. * @returns a `HelmChart` object - * @throws If `kubectlEnabled` is `false` */ public addChart(id: string, options: HelmChartOptions) { return new HelmChart(this, `chart-${id}`, { cluster: this, ...options }); @@ -856,10 +972,6 @@ export class Cluster extends Resource implements ICluster { * @internal */ public get _kubectlCreationRole() { - if (!this._clusterResource) { - throw new Error('Unable to perform this operation since kubectl is not enabled for this cluster'); - } - return this._clusterResource.creationRole; } @@ -896,15 +1008,28 @@ export class Cluster extends Resource implements ICluster { public _attachKubectlResourceScope(resourceScope: Construct): KubectlProvider { const uid = '@aws-cdk/aws-eks.KubectlProvider'; - if (!this._clusterResource) { - throw new Error('Unable to perform this operation since kubectl is not enabled for this cluster'); - } - // singleton let provider = this.stack.node.tryFindChild(uid) as KubectlProvider; if (!provider) { // create the provider. - provider = new KubectlProvider(this.stack, uid); + + let providerProps: KubectlProviderProps = { + env: this.kubectlProviderEnv, + }; + + if (!this.endpointAccess._config.publicAccess) { + // endpoint access is private only, we need to attach the + // provider to the VPC so that it can access the cluster. + providerProps = { + ...providerProps, + vpc: this.vpc, + // lambda can only be accociated with max 16 subnets and they all need to be private. + vpcSubnets: {subnets: this.selectPrivateSubnets().slice(0, 16)}, + securityGroups: [this.kubctlProviderSecurityGroup], + }; + } + + provider = new KubectlProvider(this.stack, uid, providerProps); } // allow the kubectl provider to assume the cluster creation role. @@ -919,6 +1044,17 @@ export class Cluster extends Resource implements ICluster { return provider; } + private selectPrivateSubnets(): ec2.ISubnet[] { + + const privateSubnets: ec2.ISubnet[] = []; + + for (const placement of this.vpcSubnets) { + privateSubnets.push(...this.vpc.selectSubnets(placement).subnets.filter(s => s instanceof ec2.PrivateSubnet)); + } + + return privateSubnets; + } + /** * Installs the AWS spot instance interrupt handler on the cluster if it's not * already added. @@ -987,10 +1123,6 @@ export class Cluster extends Resource implements ICluster { * omitted/removed, since the cluster is created with the "ec2" compute type by default. */ private defineCoreDnsComputeType(type: CoreDnsComputeType) { - if (!this.kubectlEnabled) { - throw new Error('kubectl must be enabled in order to define the compute type for CoreDNS'); - } - // ec2 is the "built in" compute type of the cluster so if this is the // requested type we can simply omit the resource. since the resource's // `restorePatch` is configured to restore the value to "ec2" this means diff --git a/packages/@aws-cdk/aws-eks/lib/fargate-profile.ts b/packages/@aws-cdk/aws-eks/lib/fargate-profile.ts index 76a32929de190..428d96ae24b36 100644 --- a/packages/@aws-cdk/aws-eks/lib/fargate-profile.ts +++ b/packages/@aws-cdk/aws-eks/lib/fargate-profile.ts @@ -140,12 +140,6 @@ export class FargateProfile extends Construct implements ITaggable { constructor(scope: Construct, id: string, props: FargateProfileProps) { super(scope, id); - // currently the custom resource requires a role to assume when interacting with the cluster - // and we only have this role when kubectl is enabled. - if (!props.cluster.kubectlEnabled) { - throw new Error('adding Faregate Profiles to clusters without kubectl enabled is currently unsupported'); - } - const provider = ClusterResourceProvider.getOrCreate(this); this.podExecutionRole = props.podExecutionRole ?? new iam.Role(this, 'PodExecutionRole', { diff --git a/packages/@aws-cdk/aws-eks/lib/index.ts b/packages/@aws-cdk/aws-eks/lib/index.ts index 5e1009d98eec7..5773e46ffc2bc 100644 --- a/packages/@aws-cdk/aws-eks/lib/index.ts +++ b/packages/@aws-cdk/aws-eks/lib/index.ts @@ -1,6 +1,7 @@ export * from './aws-auth'; export * from './aws-auth-mapping'; export * from './cluster'; +export * from './legacy-cluster'; export * from './eks.generated'; export * from './fargate-profile'; export * from './helm-chart'; diff --git a/packages/@aws-cdk/aws-eks/lib/kubectl-handler/apply/__init__.py b/packages/@aws-cdk/aws-eks/lib/kubectl-handler/apply/__init__.py index c892d9bc4e7ff..e5c9d712758fd 100644 --- a/packages/@aws-cdk/aws-eks/lib/kubectl-handler/apply/__init__.py +++ b/packages/@aws-cdk/aws-eks/lib/kubectl-handler/apply/__init__.py @@ -49,18 +49,21 @@ def apply_handler(event, context): def kubectl(verb, file): - retry = 3 + maxAttempts = 3 + retry = maxAttempts while retry > 0: try: cmd = ['kubectl', verb, '--kubeconfig', kubeconfig, '-f', file] + logger.info(f'Running command: {cmd}') output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as exc: output = exc.output if b'i/o timeout' in output and retry > 0: - logger.info("kubectl timed out, retries left: %s" % retry) - retry = retry - 1 + retry = retry - 1 + logger.info("kubectl timed out, retries left: %s" % retry) else: raise Exception(output) else: logger.info(output) return + raise Exception(f'Operation failed after {maxAttempts} attempts: {output}') 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 6e740b1cdaca8..67171b11aeede 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 @@ -75,10 +75,11 @@ def helm(verb, release, chart = None, repo = None, file = None, namespace = None if wait: cmnd.append('--wait') if not timeout is None: - cmnd.extend(['--timeout', timeout]) + cmnd.extend(['--timeout', timeout]) cmnd.extend(['--kubeconfig', kubeconfig]) - retry = 3 + maxAttempts = 3 + retry = maxAttempts while retry > 0: try: output = subprocess.check_output(cmnd, stderr=subprocess.STDOUT, cwd=outdir) @@ -87,7 +88,8 @@ def helm(verb, release, chart = None, repo = None, file = None, namespace = None except subprocess.CalledProcessError as exc: output = exc.output if b'Broken pipe' in output: - logger.info("Broken pipe, retries left: %s" % retry) retry = retry - 1 + logger.info("Broken pipe, retries left: %s" % retry) else: raise Exception(output) + raise Exception(f'Operation failed after {maxAttempts} attempts: {output}') diff --git a/packages/@aws-cdk/aws-eks/lib/kubectl-handler/patch/__init__.py b/packages/@aws-cdk/aws-eks/lib/kubectl-handler/patch/__init__.py index d6211b9348e1e..6597341a4806d 100644 --- a/packages/@aws-cdk/aws-eks/lib/kubectl-handler/patch/__init__.py +++ b/packages/@aws-cdk/aws-eks/lib/kubectl-handler/patch/__init__.py @@ -36,9 +36,9 @@ def patch_handler(event, context): patch_type = props['PatchType'] patch_json = None - if request_type == 'Create' or request_type == 'Update': + if request_type == 'Create' or request_type == 'Update': patch_json = apply_patch_json - elif request_type == 'Delete': + elif request_type == 'Delete': patch_json = restore_patch_json else: raise Exception("invalid request type %s" % request_type) @@ -47,7 +47,8 @@ def patch_handler(event, context): def kubectl(args): - retry = 3 + maxAttempts = 3 + retry = maxAttempts while retry > 0: try: cmd = [ 'kubectl', '--kubeconfig', kubeconfig ] + args @@ -55,10 +56,11 @@ def kubectl(args): except subprocess.CalledProcessError as exc: output = exc.output if b'i/o timeout' in output and retry > 0: - logger.info("kubectl timed out, retries left: %s" % retry) retry = retry - 1 + logger.info("kubectl timed out, retries left: %s" % retry) else: raise Exception(output) else: logger.info(output) return + raise Exception(f'Operation failed after {maxAttempts} attempts: {output}') \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts b/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts index 9fe12c4b6169b..c7c845fefcb9b 100644 --- a/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts +++ b/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts @@ -1,10 +1,41 @@ +import * as path from 'path'; +import { IVpc, ISecurityGroup, SubnetSelection } from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import { Construct, Duration, NestedStack } from '@aws-cdk/core'; import * as cr from '@aws-cdk/custom-resources'; -import * as path from 'path'; import { KubectlLayer } from './kubectl-layer'; +export interface KubectlProviderProps { + + /** + * Connect the provider to a VPC. + * + * @default - no vpc attachement. + */ + readonly vpc?: IVpc; + + /** + * Select the Vpc subnets to attach to the provider. + * + * @default - no subnets. + */ + readonly vpcSubnets?: SubnetSelection; + + /** + * Attach security groups to the provider. + * + * @default - no security groups. + */ + readonly securityGroups?: ISecurityGroup[]; + + /** + * Environment variables to inject to the provider function. + */ + readonly env?: { [key: string]: string }; + +} + export class KubectlProvider extends NestedStack { /** * The custom resource provider. @@ -16,7 +47,7 @@ export class KubectlProvider extends NestedStack { */ public readonly role: iam.IRole; - public constructor(scope: Construct, id: string) { + public constructor(scope: Construct, id: string, props: KubectlProviderProps = { }) { super(scope, id); const handler = new lambda.Function(this, 'Handler', { @@ -27,6 +58,10 @@ export class KubectlProvider extends NestedStack { description: 'onEvent handler for EKS kubectl resource provider', layers: [ KubectlLayer.getOrCreate(this, { version: '2.0.0' }) ], memorySize: 256, + vpc: props.vpc, + securityGroups: props.securityGroups, + vpcSubnets: props.vpcSubnets, + environment: props.env, }); this.provider = new cr.Provider(this, 'Provider', { diff --git a/packages/@aws-cdk/aws-eks/lib/legacy-cluster.ts b/packages/@aws-cdk/aws-eks/lib/legacy-cluster.ts new file mode 100644 index 0000000000000..3e2ba0feb9abd --- /dev/null +++ b/packages/@aws-cdk/aws-eks/lib/legacy-cluster.ts @@ -0,0 +1,449 @@ +import * as autoscaling from '@aws-cdk/aws-autoscaling'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as ssm from '@aws-cdk/aws-ssm'; +import { CfnOutput, Construct, Resource, Stack, Tag, Token } from '@aws-cdk/core'; +import { ICluster, ClusterAttributes, KubernetesVersion, NodeType, DefaultCapacityType, EksOptimizedImage, CapacityOptions, MachineImageType, AutoScalingGroupOptions, CommonClusterOptions } from './cluster'; +import { clusterArnComponents } from './cluster-resource'; +import { CfnCluster, CfnClusterProps } from './eks.generated'; +import { Nodegroup, NodegroupOptions } from './managed-nodegroup'; +import { renderAmazonLinuxUserData, renderBottlerocketUserData } from './user-data'; + +// defaults are based on https://eksctl.io +const DEFAULT_CAPACITY_COUNT = 2; +const DEFAULT_CAPACITY_TYPE = ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.LARGE); + +/** + * Common configuration props for EKS clusters. + */ +export interface LegacyClusterProps extends CommonClusterOptions { + /** + * Number of instances to allocate as an initial capacity for this cluster. + * Instance type can be configured through `defaultCapacityInstanceType`, + * which defaults to `m5.large`. + * + * Use `cluster.addCapacity` to add additional customized capacity. Set this + * to `0` is you wish to avoid the initial capacity allocation. + * + * @default 2 + */ + readonly defaultCapacity?: number; + + /** + * The instance type to use for the default capacity. This will only be taken + * into account if `defaultCapacity` is > 0. + * + * @default m5.large + */ + readonly defaultCapacityInstance?: ec2.InstanceType; + + /** + * The default capacity type for the cluster. + * + * @default NODEGROUP + */ + readonly defaultCapacityType?: DefaultCapacityType; +} + +/** + * A Cluster represents a managed Kubernetes Service (EKS) + * + * This is a fully managed cluster of API Servers (control-plane) + * The user is still required to create the worker nodes. + * + * @resource AWS::EKS::Cluster + */ +export class LegacyCluster extends Resource implements ICluster { + /** + * Import an existing cluster + * + * @param scope the construct scope, in most cases 'this' + * @param id the id or name to import as + * @param attrs the cluster properties to use for importing information + */ + public static fromClusterAttributes(scope: Construct, id: string, attrs: ClusterAttributes): ICluster { + return new ImportedCluster(scope, id, attrs); + } + + /** + * The VPC in which this Cluster was created + */ + public readonly vpc: ec2.IVpc; + + /** + * The Name of the created EKS Cluster + */ + public readonly clusterName: string; + + /** + * The AWS generated ARN for the Cluster resource + * + * @example arn:aws:eks:us-west-2:666666666666:cluster/prod + */ + public readonly clusterArn: string; + + /** + * The endpoint URL for the Cluster + * + * This is the URL inside the kubeconfig file to use with kubectl + * + * @example https://5E1D0CEXAMPLEA591B746AFC5AB30262.yl4.us-west-2.eks.amazonaws.com + */ + public readonly clusterEndpoint: string; + + /** + * The certificate-authority-data for your cluster. + */ + public readonly clusterCertificateAuthorityData: string; + + /** + * The cluster security group that was created by Amazon EKS for the cluster. + */ + public readonly clusterSecurityGroupId: string; + + /** + * Amazon Resource Name (ARN) or alias of the customer master key (CMK). + */ + public readonly clusterEncryptionConfigKeyArn: string; + + /** + * Manages connection rules (Security Group Rules) for the cluster + * + * @type {ec2.Connections} + * @memberof Cluster + */ + public readonly connections: ec2.Connections; + + /** + * IAM role assumed by the EKS Control Plane + */ + public readonly role: iam.IRole; + + /** + * The auto scaling group that hosts the default capacity for this cluster. + * This will be `undefined` if the `defaultCapacityType` is not `EC2` or + * `defaultCapacityType` is `EC2` but default capacity is set to 0. + */ + public readonly defaultCapacity?: autoscaling.AutoScalingGroup; + + /** + * The node group that hosts the default capacity for this cluster. + * This will be `undefined` if the `defaultCapacityType` is `EC2` or + * `defaultCapacityType` is `NODEGROUP` but default capacity is set to 0. + */ + public readonly defaultNodegroup?: Nodegroup; + + private readonly version: KubernetesVersion; + + /** + * Initiates an EKS Cluster with the supplied arguments + * + * @param scope a Construct, most likely a cdk.Stack created + * @param name the name of the Construct to create + * @param props properties in the IClusterProps interface + */ + constructor(scope: Construct, id: string, props: LegacyClusterProps) { + super(scope, id, { + physicalName: props.clusterName, + }); + + const stack = Stack.of(this); + + this.vpc = props.vpc || new ec2.Vpc(this, 'DefaultVpc'); + this.version = props.version; + + this.tagSubnets(); + + // this is the role used by EKS when interacting with AWS resources + this.role = props.role || new iam.Role(this, 'Role', { + assumedBy: new iam.ServicePrincipal('eks.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEKSClusterPolicy'), + ], + }); + + const securityGroup = props.securityGroup || new ec2.SecurityGroup(this, 'ControlPlaneSecurityGroup', { + vpc: this.vpc, + description: 'EKS Control Plane Security Group', + }); + + this.connections = new ec2.Connections({ + securityGroups: [securityGroup], + defaultPort: ec2.Port.tcp(443), // Control Plane has an HTTPS API + }); + + // Get subnetIds for all selected subnets + const placements = props.vpcSubnets || [{ subnetType: ec2.SubnetType.PUBLIC }, { subnetType: ec2.SubnetType.PRIVATE }]; + const subnetIds = [...new Set(Array().concat(...placements.map(s => this.vpc.selectSubnets(s).subnetIds)))]; + + const clusterProps: CfnClusterProps = { + name: this.physicalName, + roleArn: this.role.roleArn, + version: props.version.version, + resourcesVpcConfig: { + securityGroupIds: [securityGroup.securityGroupId], + subnetIds, + }, + }; + + const resource = new CfnCluster(this, 'Resource', clusterProps); + + this.clusterName = this.getResourceNameAttribute(resource.ref); + this.clusterArn = this.getResourceArnAttribute(resource.attrArn, clusterArnComponents(this.physicalName)); + + this.clusterEndpoint = resource.attrEndpoint; + this.clusterCertificateAuthorityData = resource.attrCertificateAuthorityData; + this.clusterSecurityGroupId = resource.attrClusterSecurityGroupId; + this.clusterEncryptionConfigKeyArn = resource.attrEncryptionConfigKeyArn; + + const updateConfigCommandPrefix = `aws eks update-kubeconfig --name ${this.clusterName}`; + const getTokenCommandPrefix = `aws eks get-token --cluster-name ${this.clusterName}`; + const commonCommandOptions = [ `--region ${stack.region}` ]; + + if (props.outputClusterName) { + new CfnOutput(this, 'ClusterName', { value: this.clusterName }); + } + + // allocate default capacity if non-zero (or default). + const minCapacity = props.defaultCapacity === undefined ? DEFAULT_CAPACITY_COUNT : props.defaultCapacity; + if (minCapacity > 0) { + const instanceType = props.defaultCapacityInstance || DEFAULT_CAPACITY_TYPE; + this.defaultCapacity = props.defaultCapacityType === DefaultCapacityType.EC2 ? + this.addCapacity('DefaultCapacity', { instanceType, minCapacity }) : undefined; + + this.defaultNodegroup = props.defaultCapacityType !== DefaultCapacityType.EC2 ? + this.addNodegroup('DefaultCapacity', { instanceType, minSize: minCapacity }) : undefined; + } + + const outputConfigCommand = props.outputConfigCommand === undefined ? true : props.outputConfigCommand; + if (outputConfigCommand) { + const postfix = commonCommandOptions.join(' '); + new CfnOutput(this, 'ConfigCommand', { value: `${updateConfigCommandPrefix} ${postfix}` }); + new CfnOutput(this, 'GetTokenCommand', { value: `${getTokenCommandPrefix} ${postfix}` }); + } + } + + /** + * Add nodes to this EKS cluster + * + * The nodes will automatically be configured with the right VPC and AMI + * for the instance type and Kubernetes version. + * + * Spot instances will be labeled `lifecycle=Ec2Spot` and tainted with `PreferNoSchedule`. + * If kubectl is enabled, the + * [spot interrupt handler](https://github.com/awslabs/ec2-spot-labs/tree/master/ec2-spot-eks-solution/spot-termination-handler) + * daemon will be installed on all spot instances to handle + * [EC2 Spot Instance Termination Notices](https://aws.amazon.com/blogs/aws/new-ec2-spot-instance-termination-notices/). + */ + public addCapacity(id: string, options: CapacityOptions): autoscaling.AutoScalingGroup { + if (options.machineImageType === MachineImageType.BOTTLEROCKET && options.bootstrapOptions !== undefined ) { + throw new Error('bootstrapOptions is not supported for Bottlerocket'); + } + const asg = new autoscaling.AutoScalingGroup(this, id, { + ...options, + vpc: this.vpc, + machineImage: options.machineImageType === MachineImageType.BOTTLEROCKET ? + new BottleRocketImage() : + new EksOptimizedImage({ + nodeType: nodeTypeForInstanceType(options.instanceType), + kubernetesVersion: this.version.version, + }), + updateType: options.updateType || autoscaling.UpdateType.ROLLING_UPDATE, + instanceType: options.instanceType, + }); + + this.addAutoScalingGroup(asg, { + mapRole: options.mapRole, + bootstrapOptions: options.bootstrapOptions, + bootstrapEnabled: options.bootstrapEnabled, + machineImageType: options.machineImageType, + }); + + return asg; + } + + /** + * Add managed nodegroup to this Amazon EKS cluster + * + * This method will create a new managed nodegroup and add into the capacity. + * + * @see https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html + * @param id The ID of the nodegroup + * @param options options for creating a new nodegroup + */ + public addNodegroup(id: string, options?: NodegroupOptions): Nodegroup { + return new Nodegroup(this, `Nodegroup${id}`, { + cluster: this, + ...options, + }); + } + + /** + * Add compute capacity to this EKS cluster in the form of an AutoScalingGroup + * + * The AutoScalingGroup must be running an EKS-optimized AMI containing the + * /etc/eks/bootstrap.sh script. This method will configure Security Groups, + * add the right policies to the instance role, apply the right tags, and add + * the required user data to the instance's launch configuration. + * + * Spot instances will be labeled `lifecycle=Ec2Spot` and tainted with `PreferNoSchedule`. + * If kubectl is enabled, the + * [spot interrupt handler](https://github.com/awslabs/ec2-spot-labs/tree/master/ec2-spot-eks-solution/spot-termination-handler) + * daemon will be installed on all spot instances to handle + * [EC2 Spot Instance Termination Notices](https://aws.amazon.com/blogs/aws/new-ec2-spot-instance-termination-notices/). + * + * Prefer to use `addCapacity` if possible. + * + * @see https://docs.aws.amazon.com/eks/latest/userguide/launch-workers.html + * @param autoScalingGroup [disable-awslint:ref-via-interface] + * @param options options for adding auto scaling groups, like customizing the bootstrap script + */ + public addAutoScalingGroup(autoScalingGroup: autoscaling.AutoScalingGroup, options: AutoScalingGroupOptions) { + // self rules + autoScalingGroup.connections.allowInternally(ec2.Port.allTraffic()); + + // Cluster to:nodes rules + autoScalingGroup.connections.allowFrom(this, ec2.Port.tcp(443)); + autoScalingGroup.connections.allowFrom(this, ec2.Port.tcpRange(1025, 65535)); + + // Allow HTTPS from Nodes to Cluster + autoScalingGroup.connections.allowTo(this, ec2.Port.tcp(443)); + + // Allow all node outbound traffic + autoScalingGroup.connections.allowToAnyIpv4(ec2.Port.allTcp()); + autoScalingGroup.connections.allowToAnyIpv4(ec2.Port.allUdp()); + autoScalingGroup.connections.allowToAnyIpv4(ec2.Port.allIcmp()); + + const bootstrapEnabled = options.bootstrapEnabled !== undefined ? options.bootstrapEnabled : true; + if (options.bootstrapOptions && !bootstrapEnabled) { + throw new Error('Cannot specify "bootstrapOptions" if "bootstrapEnabled" is false'); + } + + if (bootstrapEnabled) { + const userData = options.machineImageType === MachineImageType.BOTTLEROCKET ? + renderBottlerocketUserData(this) : + renderAmazonLinuxUserData(this.clusterName, autoScalingGroup, options.bootstrapOptions); + autoScalingGroup.addUserData(...userData); + } + + autoScalingGroup.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEKSWorkerNodePolicy')); + autoScalingGroup.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEKS_CNI_Policy')); + autoScalingGroup.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerRegistryReadOnly')); + + // EKS Required Tags + Tag.add(autoScalingGroup, `kubernetes.io/cluster/${this.clusterName}`, 'owned', { + applyToLaunchedInstances: true, + }); + + if (options.mapRole) { + throw new Error('Cannot map instance IAM role to RBAC if kubectl is disabled for the cluster'); + } + + // since we are not mapping the instance role to RBAC, synthesize an + // output so it can be pasted into `aws-auth-cm.yaml` + new CfnOutput(autoScalingGroup, 'InstanceRoleARN', { + value: autoScalingGroup.role.roleArn, + }); + } + + /** + * Opportunistically tag subnets with the required tags. + * + * If no subnets could be found (because this is an imported VPC), add a warning. + * + * @see https://docs.aws.amazon.com/eks/latest/userguide/network_reqs.html + */ + private tagSubnets() { + const tagAllSubnets = (type: string, subnets: ec2.ISubnet[], tag: string) => { + for (const subnet of subnets) { + // if this is not a concrete subnet, attach a construct warning + if (!ec2.Subnet.isVpcSubnet(subnet)) { + // message (if token): "could not auto-tag public/private subnet with tag..." + // message (if not token): "count not auto-tag public/private subnet xxxxx with tag..." + const subnetID = Token.isUnresolved(subnet.subnetId) ? '' : ` ${subnet.subnetId}`; + this.node.addWarning(`Could not auto-tag ${type} subnet${subnetID} with "${tag}=1", please remember to do this manually`); + continue; + } + + subnet.node.applyAspect(new Tag(tag, '1')); + } + }; + + // https://docs.aws.amazon.com/eks/latest/userguide/network_reqs.html + tagAllSubnets('private', this.vpc.privateSubnets, 'kubernetes.io/role/internal-elb'); + tagAllSubnets('public', this.vpc.publicSubnets, 'kubernetes.io/role/elb'); + } +} + +/** + * Import a cluster to use in another stack + */ +class ImportedCluster extends Resource implements ICluster { + public readonly vpc: ec2.IVpc; + public readonly clusterCertificateAuthorityData: string; + public readonly clusterSecurityGroupId: string; + public readonly clusterEncryptionConfigKeyArn: string; + public readonly clusterName: string; + public readonly clusterArn: string; + public readonly clusterEndpoint: string; + public readonly connections = new ec2.Connections(); + + constructor(scope: Construct, id: string, props: ClusterAttributes) { + super(scope, id); + + this.vpc = ec2.Vpc.fromVpcAttributes(this, 'VPC', props.vpc); + this.clusterName = props.clusterName; + this.clusterEndpoint = props.clusterEndpoint; + this.clusterArn = props.clusterArn; + this.clusterCertificateAuthorityData = props.clusterCertificateAuthorityData; + this.clusterSecurityGroupId = props.clusterSecurityGroupId; + this.clusterEncryptionConfigKeyArn = props.clusterEncryptionConfigKeyArn; + + let i = 1; + for (const sgProps of props.securityGroups) { + this.connections.addSecurityGroup(ec2.SecurityGroup.fromSecurityGroupId(this, `SecurityGroup${i}`, sgProps.securityGroupId)); + i++; + } + } +} + +/** + * Construct an Bottlerocket image from the latest AMI published in SSM + */ +class BottleRocketImage implements ec2.IMachineImage { + private readonly kubernetesVersion?: string; + + private readonly amiParameterName: string; + + /** + * Constructs a new instance of the BottleRocketImage class. + */ + public constructor() { + // only 1.15 is currently available + this.kubernetesVersion = '1.15'; + + // set the SSM parameter name + this.amiParameterName = `/aws/service/bottlerocket/aws-k8s-${this.kubernetesVersion}/x86_64/latest/image_id`; + } + + /** + * Return the correct image + */ + public getImage(scope: Construct): ec2.MachineImageConfig { + const ami = ssm.StringParameter.valueForStringParameter(scope, this.amiParameterName); + return { + imageId: ami, + osType: ec2.OperatingSystemType.LINUX, + userData: ec2.UserData.custom(''), + }; + } +} + +const GPU_INSTANCETYPES = ['p2', 'p3', 'g4']; +const INFERENTIA_INSTANCETYPES = ['inf1']; + +function nodeTypeForInstanceType(instanceType: ec2.InstanceType) { + return GPU_INSTANCETYPES.includes(instanceType.toString().substring(0, 2)) ? NodeType.GPU : + INFERENTIA_INSTANCETYPES.includes(instanceType.toString().substring(0, 4)) ? NodeType.INFERENTIA : + NodeType.STANDARD; +} diff --git a/packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts b/packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts index 3ab5287b9f782..db2130e01879d 100644 --- a/packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts +++ b/packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts @@ -1,7 +1,7 @@ import { InstanceType, ISecurityGroup, SubnetSelection } from '@aws-cdk/aws-ec2'; import { IRole, ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import { Construct, IResource, Resource } from '@aws-cdk/core'; -import { Cluster } from './cluster'; +import { Cluster, ICluster } from './cluster'; import { CfnNodegroup } from './eks.generated'; /** @@ -163,9 +163,8 @@ export interface NodegroupOptions { export interface NodegroupProps extends NodegroupOptions { /** * Cluster resource - * [disable-awslint:ref-via-interface]" */ - readonly cluster: Cluster; + readonly cluster: ICluster; } /** @@ -198,7 +197,7 @@ export class Nodegroup extends Resource implements INodegroup { * * @attribute ClusterName */ - public readonly cluster: Cluster; + public readonly cluster: ICluster; /** * IAM role of the instance profile for the nodegroup */ @@ -265,7 +264,7 @@ export class Nodegroup extends Resource implements INodegroup { // managed nodegroups update the `aws-auth` on creation, but we still need to track // its state for consistency. - if (this.cluster.kubectlEnabled) { + if (this.cluster instanceof Cluster) { // see https://docs.aws.amazon.com/en_us/eks/latest/userguide/add-user-role.html this.cluster.awsAuth.addRoleMapping(this.role, { username: 'system:node:{{EC2PrivateDNSName}}', diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-private-endpoint.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-private-endpoint.expected.json new file mode 100644 index 0000000000000..0889cc63a0237 --- /dev/null +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-private-endpoint.expected.json @@ -0,0 +1,1346 @@ +{ + "Resources": { + "AdminRole38563C57": { + "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" + } + } + }, + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-private-endpoint-test/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "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": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-private-endpoint-test/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-private-endpoint-test/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": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-private-endpoint-test/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-private-endpoint-test/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "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": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-private-endpoint-test/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-private-endpoint-test/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" + ] + }, + "VpcPublicSubnet3SubnetBE12F0B6": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-private-endpoint-test/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTable93458DBB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-private-endpoint-test/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + } + } + }, + "VpcPublicSubnet3DefaultRoute4697774F": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "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": "kubernetes.io/role/internal-elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-private-endpoint-test/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-private-endpoint-test/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.128.0/19", + "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": "kubernetes.io/role/internal-elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-private-endpoint-test/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-private-endpoint-test/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": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet3SubnetF258B56E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-private-endpoint-test/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableD98824C7": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-private-endpoint-test/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + } + }, + "VpcPrivateSubnet3DefaultRoute94B74F0D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-private-endpoint-test/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "ClusterRoleFA261979": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "eks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEKSClusterPolicy" + ] + ] + } + ] + } + }, + "ClusterControlPlaneSecurityGroupD274242C": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "EKS Control Plane Security Group", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "ClusterControlPlaneSecurityGroupfromawscdkeksclusterprivateendpointtestClusterKubectlProviderSecurityGroup6A0B729C443DF3A2707": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "Description": "from awscdkeksclusterprivateendpointtestClusterKubectlProviderSecurityGroup6A0B729C:443", + "FromPort": 443, + "GroupId": { + "Fn::GetAtt": [ + "ClusterControlPlaneSecurityGroupD274242C", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "ClusterKubectlProviderSecurityGroup2D90691C", + "GroupId" + ] + }, + "ToPort": 443 + } + }, + "ClusterKubectlProviderSecurityGroup2D90691C": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Comminication between KubectlProvider and EKS Control Plane", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "ClusterCreationRole360249B6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": [ + { + "Fn::GetAtt": [ + "awscdkawseksClusterResourceProviderNestedStackawscdkawseksClusterResourceProviderNestedStackResource9827C454", + "Outputs.awscdkeksclusterprivateendpointtestawscdkawseksClusterResourceProviderOnEventHandlerServiceRole4392FD6EArn" + ] + }, + { + "Fn::GetAtt": [ + "awscdkawseksClusterResourceProviderNestedStackawscdkawseksClusterResourceProviderNestedStackResource9827C454", + "Outputs.awscdkeksclusterprivateendpointtestawscdkawseksClusterResourceProviderIsCompleteHandlerServiceRole956A78E2Arn" + ] + } + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B", + "Outputs.awscdkeksclusterprivateendpointtestawscdkawseksKubectlProviderHandlerServiceRole5505C312Arn" + ] + } + } + } + ], + "Version": "2012-10-17" + } + }, + "DependsOn": [ + "ClusterKubectlProviderSecurityGroup2D90691C", + "VpcIGWD7BA715C", + "VpcPrivateSubnet1DefaultRouteBE02A9ED", + "VpcPrivateSubnet1RouteTableB2C5B500", + "VpcPrivateSubnet1RouteTableAssociation70C59FA6", + "VpcPrivateSubnet1Subnet536B997A", + "VpcPrivateSubnet2DefaultRoute060D2087", + "VpcPrivateSubnet2RouteTableA678073B", + "VpcPrivateSubnet2RouteTableAssociationA89CAD56", + "VpcPrivateSubnet2Subnet3788AAA1", + "VpcPrivateSubnet3DefaultRoute94B74F0D", + "VpcPrivateSubnet3RouteTableD98824C7", + "VpcPrivateSubnet3RouteTableAssociation16BDDC43", + "VpcPrivateSubnet3SubnetF258B56E", + "VpcPublicSubnet1DefaultRoute3DA9E72A", + "VpcPublicSubnet1EIPD7E02669", + "VpcPublicSubnet1NATGateway4D7517AA", + "VpcPublicSubnet1RouteTable6C95E38E", + "VpcPublicSubnet1RouteTableAssociation97140677", + "VpcPublicSubnet1Subnet5C2D37C4", + "VpcPublicSubnet2DefaultRoute97F91067", + "VpcPublicSubnet2RouteTable94F7E489", + "VpcPublicSubnet2RouteTableAssociationDD5762D8", + "VpcPublicSubnet2Subnet691E08A3", + "VpcPublicSubnet3DefaultRoute4697774F", + "VpcPublicSubnet3RouteTable93458DBB", + "VpcPublicSubnet3RouteTableAssociation1F1EDF02", + "VpcPublicSubnet3SubnetBE12F0B6", + "Vpc8378EB38", + "VpcVPCGWBF912B6E" + ] + }, + "ClusterCreationRoleDefaultPolicyE8BDFC7B": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "ClusterRoleFA261979", + "Arn" + ] + } + }, + { + "Action": [ + "ec2:DescribeSubnets", + "ec2:DescribeRouteTables" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "eks:CreateCluster", + "eks:DescribeCluster", + "eks:DescribeUpdate", + "eks:DeleteCluster", + "eks:UpdateClusterVersion", + "eks:UpdateClusterConfig", + "eks:CreateFargateProfile", + "eks:TagResource", + "eks:UntagResource" + ], + "Effect": "Allow", + "Resource": [ + "*" + ] + }, + { + "Action": [ + "eks:DescribeFargateProfile", + "eks:DeleteFargateProfile" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "iam:GetRole", + "iam:listAttachedRolePolicies" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "iam:CreateServiceLinkedRole", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "ec2:DescribeVpcs", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:test-region:12345678:vpc/", + { + "Ref": "Vpc8378EB38" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ClusterCreationRoleDefaultPolicyE8BDFC7B", + "Roles": [ + { + "Ref": "ClusterCreationRole360249B6" + } + ] + }, + "DependsOn": [ + "ClusterKubectlProviderSecurityGroup2D90691C", + "VpcIGWD7BA715C", + "VpcPrivateSubnet1DefaultRouteBE02A9ED", + "VpcPrivateSubnet1RouteTableB2C5B500", + "VpcPrivateSubnet1RouteTableAssociation70C59FA6", + "VpcPrivateSubnet1Subnet536B997A", + "VpcPrivateSubnet2DefaultRoute060D2087", + "VpcPrivateSubnet2RouteTableA678073B", + "VpcPrivateSubnet2RouteTableAssociationA89CAD56", + "VpcPrivateSubnet2Subnet3788AAA1", + "VpcPrivateSubnet3DefaultRoute94B74F0D", + "VpcPrivateSubnet3RouteTableD98824C7", + "VpcPrivateSubnet3RouteTableAssociation16BDDC43", + "VpcPrivateSubnet3SubnetF258B56E", + "VpcPublicSubnet1DefaultRoute3DA9E72A", + "VpcPublicSubnet1EIPD7E02669", + "VpcPublicSubnet1NATGateway4D7517AA", + "VpcPublicSubnet1RouteTable6C95E38E", + "VpcPublicSubnet1RouteTableAssociation97140677", + "VpcPublicSubnet1Subnet5C2D37C4", + "VpcPublicSubnet2DefaultRoute97F91067", + "VpcPublicSubnet2RouteTable94F7E489", + "VpcPublicSubnet2RouteTableAssociationDD5762D8", + "VpcPublicSubnet2Subnet691E08A3", + "VpcPublicSubnet3DefaultRoute4697774F", + "VpcPublicSubnet3RouteTable93458DBB", + "VpcPublicSubnet3RouteTableAssociation1F1EDF02", + "VpcPublicSubnet3SubnetBE12F0B6", + "Vpc8378EB38", + "VpcVPCGWBF912B6E" + ] + }, + "Cluster9EE0221C": { + "Type": "Custom::AWSCDK-EKS-Cluster", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "awscdkawseksClusterResourceProviderNestedStackawscdkawseksClusterResourceProviderNestedStackResource9827C454", + "Outputs.awscdkeksclusterprivateendpointtestawscdkawseksClusterResourceProviderframeworkonEvent080B290CArn" + ] + }, + "Config": { + "version": "1.16", + "roleArn": { + "Fn::GetAtt": [ + "ClusterRoleFA261979", + "Arn" + ] + }, + "resourcesVpcConfig": { + "subnetIds": [ + { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + }, + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "securityGroupIds": [ + { + "Fn::GetAtt": [ + "ClusterControlPlaneSecurityGroupD274242C", + "GroupId" + ] + } + ], + "endpointPublicAccess": false, + "endpointPrivateAccess": true + } + }, + "AssumeRoleArn": { + "Fn::GetAtt": [ + "ClusterCreationRole360249B6", + "Arn" + ] + }, + "AttributesRevision": 2 + }, + "DependsOn": [ + "ClusterKubectlProviderSecurityGroup2D90691C", + "ClusterCreationRoleDefaultPolicyE8BDFC7B", + "ClusterCreationRole360249B6", + "VpcIGWD7BA715C", + "VpcPrivateSubnet1DefaultRouteBE02A9ED", + "VpcPrivateSubnet1RouteTableB2C5B500", + "VpcPrivateSubnet1RouteTableAssociation70C59FA6", + "VpcPrivateSubnet1Subnet536B997A", + "VpcPrivateSubnet2DefaultRoute060D2087", + "VpcPrivateSubnet2RouteTableA678073B", + "VpcPrivateSubnet2RouteTableAssociationA89CAD56", + "VpcPrivateSubnet2Subnet3788AAA1", + "VpcPrivateSubnet3DefaultRoute94B74F0D", + "VpcPrivateSubnet3RouteTableD98824C7", + "VpcPrivateSubnet3RouteTableAssociation16BDDC43", + "VpcPrivateSubnet3SubnetF258B56E", + "VpcPublicSubnet1DefaultRoute3DA9E72A", + "VpcPublicSubnet1EIPD7E02669", + "VpcPublicSubnet1NATGateway4D7517AA", + "VpcPublicSubnet1RouteTable6C95E38E", + "VpcPublicSubnet1RouteTableAssociation97140677", + "VpcPublicSubnet1Subnet5C2D37C4", + "VpcPublicSubnet2DefaultRoute97F91067", + "VpcPublicSubnet2RouteTable94F7E489", + "VpcPublicSubnet2RouteTableAssociationDD5762D8", + "VpcPublicSubnet2Subnet691E08A3", + "VpcPublicSubnet3DefaultRoute4697774F", + "VpcPublicSubnet3RouteTable93458DBB", + "VpcPublicSubnet3RouteTableAssociation1F1EDF02", + "VpcPublicSubnet3SubnetBE12F0B6", + "Vpc8378EB38", + "VpcVPCGWBF912B6E" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "ClusterKubectlReadyBarrier200052AF": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "aws:cdk:eks:kubectl-ready" + }, + "DependsOn": [ + "ClusterCreationRoleDefaultPolicyE8BDFC7B", + "ClusterCreationRole360249B6", + "Cluster9EE0221C" + ] + }, + "ClusterAwsAuthmanifestFE51F8AE": { + "Type": "Custom::AWSCDK-EKS-KubernetesResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B", + "Outputs.awscdkeksclusterprivateendpointtestawscdkawseksKubectlProviderframeworkonEventC2C76E2FArn" + ] + }, + "Manifest": { + "Fn::Join": [ + "", + [ + "[{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"aws-auth\",\"namespace\":\"kube-system\"},\"data\":{\"mapRoles\":\"[{\\\"rolearn\\\":\\\"", + { + "Fn::GetAtt": [ + "AdminRole38563C57", + "Arn" + ] + }, + "\\\",\\\"username\\\":\\\"", + { + "Fn::GetAtt": [ + "AdminRole38563C57", + "Arn" + ] + }, + "\\\",\\\"groups\\\":[\\\"system:masters\\\"]},{\\\"rolearn\\\":\\\"", + { + "Fn::GetAtt": [ + "ClusterNodegroupDefaultCapacityNodeGroupRole55953B04", + "Arn" + ] + }, + "\\\",\\\"username\\\":\\\"system:node:{{EC2PrivateDNSName}}\\\",\\\"groups\\\":[\\\"system:bootstrappers\\\",\\\"system:nodes\\\"]}]\",\"mapUsers\":\"[]\",\"mapAccounts\":\"[]\"}}]" + ] + ] + }, + "ClusterName": { + "Ref": "Cluster9EE0221C" + }, + "RoleArn": { + "Fn::GetAtt": [ + "ClusterCreationRole360249B6", + "Arn" + ] + } + }, + "DependsOn": [ + "ClusterKubectlReadyBarrier200052AF" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "ClusterNodegroupDefaultCapacityNodeGroupRole55953B04": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ec2.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEKSWorkerNodePolicy" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEKS_CNI_Policy" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" + ] + ] + } + ] + } + }, + "ClusterNodegroupDefaultCapacityDA0920A3": { + "Type": "AWS::EKS::Nodegroup", + "Properties": { + "ClusterName": { + "Ref": "Cluster9EE0221C" + }, + "NodeRole": { + "Fn::GetAtt": [ + "ClusterNodegroupDefaultCapacityNodeGroupRole55953B04", + "Arn" + ] + }, + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "ForceUpdateEnabled": true, + "InstanceTypes": [ + "m5.large" + ], + "ScalingConfig": { + "DesiredSize": 2, + "MaxSize": 2, + "MinSize": 2 + } + } + }, + "Clustermanifestconfigmap3F180550": { + "Type": "Custom::AWSCDK-EKS-KubernetesResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B", + "Outputs.awscdkeksclusterprivateendpointtestawscdkawseksKubectlProviderframeworkonEventC2C76E2FArn" + ] + }, + "Manifest": "[{\"kind\":\"ConfigMap\",\"apiVersion\":\"v1\",\"data\":{\"hello\":\"world\"},\"metadata\":{\"name\":\"config-map\"}}]", + "ClusterName": { + "Ref": "Cluster9EE0221C" + }, + "RoleArn": { + "Fn::GetAtt": [ + "ClusterCreationRole360249B6", + "Arn" + ] + } + }, + "DependsOn": [ + "ClusterKubectlReadyBarrier200052AF" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "awscdkawseksClusterResourceProviderNestedStackawscdkawseksClusterResourceProviderNestedStackResource9827C454": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.test-region.", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParameters3c8e15207108696f26eb3900c56b9ed4a81535ed7d0fdb4477972f1741ad9789S3BucketBC18629C" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters3c8e15207108696f26eb3900c56b9ed4a81535ed7d0fdb4477972f1741ad9789S3VersionKeyE68C888F" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters3c8e15207108696f26eb3900c56b9ed4a81535ed7d0fdb4477972f1741ad9789S3VersionKeyE68C888F" + } + ] + } + ] + } + ] + ] + }, + "Parameters": { + "referencetoawscdkeksclusterprivateendpointtestAssetParameters00ba02e613a29439c93f9aef4e82e253763eb70cd32026df071449485c692791S3Bucket87F4EA82Ref": { + "Ref": "AssetParameters00ba02e613a29439c93f9aef4e82e253763eb70cd32026df071449485c692791S3Bucket26C90BA0" + }, + "referencetoawscdkeksclusterprivateendpointtestAssetParameters00ba02e613a29439c93f9aef4e82e253763eb70cd32026df071449485c692791S3VersionKey3BEF8ACDRef": { + "Ref": "AssetParameters00ba02e613a29439c93f9aef4e82e253763eb70cd32026df071449485c692791S3VersionKeyD269C675" + }, + "referencetoawscdkeksclusterprivateendpointtestAssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cS3Bucket7CB66361Ref": { + "Ref": "AssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cS3BucketF1BD2256" + }, + "referencetoawscdkeksclusterprivateendpointtestAssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cS3VersionKeyF78CAD23Ref": { + "Ref": "AssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cS3VersionKeyF47FA401" + } + } + } + }, + "awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.test-region.", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParametersf2ad7629f5f54ad293dccc2fb60891424f9149f12d84f2f12728543b145962a0S3BucketACD6057C" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersf2ad7629f5f54ad293dccc2fb60891424f9149f12d84f2f12728543b145962a0S3VersionKey20D7AC7B" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersf2ad7629f5f54ad293dccc2fb60891424f9149f12d84f2f12728543b145962a0S3VersionKey20D7AC7B" + } + ] + } + ] + } + ] + ] + }, + "Parameters": { + "referencetoawscdkeksclusterprivateendpointtestAssetParameters649b09403c8414e624c965d6c2f0e41c341c2afa5d8e7bae4ac5746fe230f502S3Bucket5848D8F5Ref": { + "Ref": "AssetParameters649b09403c8414e624c965d6c2f0e41c341c2afa5d8e7bae4ac5746fe230f502S3BucketE7D09A6B" + }, + "referencetoawscdkeksclusterprivateendpointtestAssetParameters649b09403c8414e624c965d6c2f0e41c341c2afa5d8e7bae4ac5746fe230f502S3VersionKeyD69255C2Ref": { + "Ref": "AssetParameters649b09403c8414e624c965d6c2f0e41c341c2afa5d8e7bae4ac5746fe230f502S3VersionKey1DA734B2" + }, + "referencetoawscdkeksclusterprivateendpointtestVpcPrivateSubnet1Subnet94DAD769Ref": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + "referencetoawscdkeksclusterprivateendpointtestVpcPrivateSubnet2Subnet04963C08Ref": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + "referencetoawscdkeksclusterprivateendpointtestVpcPrivateSubnet3SubnetC47FD39ARef": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + }, + "referencetoawscdkeksclusterprivateendpointtestClusterKubectlProviderSecurityGroup67FA4325GroupId": { + "Fn::GetAtt": [ + "ClusterKubectlProviderSecurityGroup2D90691C", + "GroupId" + ] + }, + "referencetoawscdkeksclusterprivateendpointtestAssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cS3Bucket7CB66361Ref": { + "Ref": "AssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cS3BucketF1BD2256" + }, + "referencetoawscdkeksclusterprivateendpointtestAssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cS3VersionKeyF78CAD23Ref": { + "Ref": "AssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cS3VersionKeyF47FA401" + } + } + } + } + }, + "Outputs": { + "ClusterConfigCommand43AAE40F": { + "Value": { + "Fn::Join": [ + "", + [ + "aws eks update-kubeconfig --name ", + { + "Ref": "Cluster9EE0221C" + }, + " --region test-region --role-arn ", + { + "Fn::GetAtt": [ + "AdminRole38563C57", + "Arn" + ] + } + ] + ] + } + }, + "ClusterGetTokenCommand06AE992E": { + "Value": { + "Fn::Join": [ + "", + [ + "aws eks get-token --cluster-name ", + { + "Ref": "Cluster9EE0221C" + }, + " --region test-region --role-arn ", + { + "Fn::GetAtt": [ + "AdminRole38563C57", + "Arn" + ] + } + ] + ] + } + } + }, + "Parameters": { + "AssetParameters00ba02e613a29439c93f9aef4e82e253763eb70cd32026df071449485c692791S3Bucket26C90BA0": { + "Type": "String", + "Description": "S3 bucket for asset \"00ba02e613a29439c93f9aef4e82e253763eb70cd32026df071449485c692791\"" + }, + "AssetParameters00ba02e613a29439c93f9aef4e82e253763eb70cd32026df071449485c692791S3VersionKeyD269C675": { + "Type": "String", + "Description": "S3 key for asset version \"00ba02e613a29439c93f9aef4e82e253763eb70cd32026df071449485c692791\"" + }, + "AssetParameters00ba02e613a29439c93f9aef4e82e253763eb70cd32026df071449485c692791ArtifactHashAADC8B03": { + "Type": "String", + "Description": "Artifact hash for asset \"00ba02e613a29439c93f9aef4e82e253763eb70cd32026df071449485c692791\"" + }, + "AssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cS3BucketF1BD2256": { + "Type": "String", + "Description": "S3 bucket for asset \"974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74c\"" + }, + "AssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cS3VersionKeyF47FA401": { + "Type": "String", + "Description": "S3 key for asset version \"974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74c\"" + }, + "AssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cArtifactHash5C0B1EA0": { + "Type": "String", + "Description": "Artifact hash for asset \"974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74c\"" + }, + "AssetParameters649b09403c8414e624c965d6c2f0e41c341c2afa5d8e7bae4ac5746fe230f502S3BucketE7D09A6B": { + "Type": "String", + "Description": "S3 bucket for asset \"649b09403c8414e624c965d6c2f0e41c341c2afa5d8e7bae4ac5746fe230f502\"" + }, + "AssetParameters649b09403c8414e624c965d6c2f0e41c341c2afa5d8e7bae4ac5746fe230f502S3VersionKey1DA734B2": { + "Type": "String", + "Description": "S3 key for asset version \"649b09403c8414e624c965d6c2f0e41c341c2afa5d8e7bae4ac5746fe230f502\"" + }, + "AssetParameters649b09403c8414e624c965d6c2f0e41c341c2afa5d8e7bae4ac5746fe230f502ArtifactHash815E1969": { + "Type": "String", + "Description": "Artifact hash for asset \"649b09403c8414e624c965d6c2f0e41c341c2afa5d8e7bae4ac5746fe230f502\"" + }, + "AssetParameters3c8e15207108696f26eb3900c56b9ed4a81535ed7d0fdb4477972f1741ad9789S3BucketBC18629C": { + "Type": "String", + "Description": "S3 bucket for asset \"3c8e15207108696f26eb3900c56b9ed4a81535ed7d0fdb4477972f1741ad9789\"" + }, + "AssetParameters3c8e15207108696f26eb3900c56b9ed4a81535ed7d0fdb4477972f1741ad9789S3VersionKeyE68C888F": { + "Type": "String", + "Description": "S3 key for asset version \"3c8e15207108696f26eb3900c56b9ed4a81535ed7d0fdb4477972f1741ad9789\"" + }, + "AssetParameters3c8e15207108696f26eb3900c56b9ed4a81535ed7d0fdb4477972f1741ad9789ArtifactHash026B7D88": { + "Type": "String", + "Description": "Artifact hash for asset \"3c8e15207108696f26eb3900c56b9ed4a81535ed7d0fdb4477972f1741ad9789\"" + }, + "AssetParametersf2ad7629f5f54ad293dccc2fb60891424f9149f12d84f2f12728543b145962a0S3BucketACD6057C": { + "Type": "String", + "Description": "S3 bucket for asset \"f2ad7629f5f54ad293dccc2fb60891424f9149f12d84f2f12728543b145962a0\"" + }, + "AssetParametersf2ad7629f5f54ad293dccc2fb60891424f9149f12d84f2f12728543b145962a0S3VersionKey20D7AC7B": { + "Type": "String", + "Description": "S3 key for asset version \"f2ad7629f5f54ad293dccc2fb60891424f9149f12d84f2f12728543b145962a0\"" + }, + "AssetParametersf2ad7629f5f54ad293dccc2fb60891424f9149f12d84f2f12728543b145962a0ArtifactHash05CD8D10": { + "Type": "String", + "Description": "Artifact hash for asset \"f2ad7629f5f54ad293dccc2fb60891424f9149f12d84f2f12728543b145962a0\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-private-endpoint.ts b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-private-endpoint.ts new file mode 100644 index 0000000000000..c8bcd43bdebfa --- /dev/null +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-private-endpoint.ts @@ -0,0 +1,48 @@ +/// !cdk-integ pragma:ignore-assets +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import { App } from '@aws-cdk/core'; +import * as eks from '../lib'; +import { TestStack } from './util'; + +class EksClusterStack extends TestStack { + constructor(scope: App, id: string) { + super(scope, id); + + // allow all account users to assume this role in order to admin the cluster + const mastersRole = new iam.Role(this, 'AdminRole', { + assumedBy: new iam.AccountRootPrincipal(), + }); + + // just need one nat gateway to simplify the test + const vpc = new ec2.Vpc(this, 'Vpc', { maxAzs: 3, natGateways: 1 }); + + const cluster = new eks.Cluster(this, 'Cluster', { + vpc, + mastersRole, + defaultCapacity: 2, + version: eks.KubernetesVersion.V1_16, + endpointAccess: eks.EndpointAccess.PRIVATE, + }); + + // this is the valdiation. it won't work if the private access is not setup properly. + cluster.addResource('config-map', { + kind: 'ConfigMap', + apiVersion: 'v1', + data: { + hello: 'world', + }, + metadata: { + name: 'config-map', + }, + }); + + } +} + + +const app = new App(); + +new EksClusterStack(app, 'aws-cdk-eks-cluster-private-endpoint-test'); + +app.synth(); 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 1aa1b2d54725d..2be0396673fb6 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 @@ -581,6 +581,27 @@ } } }, + "ClusterControlPlaneSecurityGroupfromawscdkeksclustertestClusterKubectlProviderSecurityGroup0285626644359187EDA": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "Description": "from awscdkeksclustertestClusterKubectlProviderSecurityGroup02856266:443", + "FromPort": 443, + "GroupId": { + "Fn::GetAtt": [ + "ClusterControlPlaneSecurityGroupD274242C", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "ClusterKubectlProviderSecurityGroup2D90691C", + "GroupId" + ] + }, + "ToPort": 443 + } + }, "ClusterControlPlaneSecurityGroupfromawscdkeksclustertestClusterNodesInstanceSecurityGroupD0B64C54443795AF111": { "Type": "AWS::EC2::SecurityGroupIngress", "Properties": { @@ -665,6 +686,22 @@ "ToPort": 443 } }, + "ClusterKubectlProviderSecurityGroup2D90691C": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Comminication between KubectlProvider and EKS Control Plane", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, "ClusterCreationRole360249B6": { "Type": "AWS::IAM::Role", "Properties": { @@ -705,7 +742,39 @@ ], "Version": "2012-10-17" } - } + }, + "DependsOn": [ + "ClusterKubectlProviderSecurityGroup2D90691C", + "VpcIGWD7BA715C", + "VpcPrivateSubnet1DefaultRouteBE02A9ED", + "VpcPrivateSubnet1RouteTableB2C5B500", + "VpcPrivateSubnet1RouteTableAssociation70C59FA6", + "VpcPrivateSubnet1Subnet536B997A", + "VpcPrivateSubnet2DefaultRoute060D2087", + "VpcPrivateSubnet2RouteTableA678073B", + "VpcPrivateSubnet2RouteTableAssociationA89CAD56", + "VpcPrivateSubnet2Subnet3788AAA1", + "VpcPrivateSubnet3DefaultRoute94B74F0D", + "VpcPrivateSubnet3RouteTableD98824C7", + "VpcPrivateSubnet3RouteTableAssociation16BDDC43", + "VpcPrivateSubnet3SubnetF258B56E", + "VpcPublicSubnet1DefaultRoute3DA9E72A", + "VpcPublicSubnet1EIPD7E02669", + "VpcPublicSubnet1NATGateway4D7517AA", + "VpcPublicSubnet1RouteTable6C95E38E", + "VpcPublicSubnet1RouteTableAssociation97140677", + "VpcPublicSubnet1Subnet5C2D37C4", + "VpcPublicSubnet2DefaultRoute97F91067", + "VpcPublicSubnet2RouteTable94F7E489", + "VpcPublicSubnet2RouteTableAssociationDD5762D8", + "VpcPublicSubnet2Subnet691E08A3", + "VpcPublicSubnet3DefaultRoute4697774F", + "VpcPublicSubnet3RouteTable93458DBB", + "VpcPublicSubnet3RouteTableAssociation1F1EDF02", + "VpcPublicSubnet3SubnetBE12F0B6", + "Vpc8378EB38", + "VpcVPCGWBF912B6E" + ] }, "ClusterCreationRoleDefaultPolicyE8BDFC7B": { "Type": "AWS::IAM::Policy", @@ -806,7 +875,39 @@ "Ref": "ClusterCreationRole360249B6" } ] - } + }, + "DependsOn": [ + "ClusterKubectlProviderSecurityGroup2D90691C", + "VpcIGWD7BA715C", + "VpcPrivateSubnet1DefaultRouteBE02A9ED", + "VpcPrivateSubnet1RouteTableB2C5B500", + "VpcPrivateSubnet1RouteTableAssociation70C59FA6", + "VpcPrivateSubnet1Subnet536B997A", + "VpcPrivateSubnet2DefaultRoute060D2087", + "VpcPrivateSubnet2RouteTableA678073B", + "VpcPrivateSubnet2RouteTableAssociationA89CAD56", + "VpcPrivateSubnet2Subnet3788AAA1", + "VpcPrivateSubnet3DefaultRoute94B74F0D", + "VpcPrivateSubnet3RouteTableD98824C7", + "VpcPrivateSubnet3RouteTableAssociation16BDDC43", + "VpcPrivateSubnet3SubnetF258B56E", + "VpcPublicSubnet1DefaultRoute3DA9E72A", + "VpcPublicSubnet1EIPD7E02669", + "VpcPublicSubnet1NATGateway4D7517AA", + "VpcPublicSubnet1RouteTable6C95E38E", + "VpcPublicSubnet1RouteTableAssociation97140677", + "VpcPublicSubnet1Subnet5C2D37C4", + "VpcPublicSubnet2DefaultRoute97F91067", + "VpcPublicSubnet2RouteTable94F7E489", + "VpcPublicSubnet2RouteTableAssociationDD5762D8", + "VpcPublicSubnet2Subnet691E08A3", + "VpcPublicSubnet3DefaultRoute4697774F", + "VpcPublicSubnet3RouteTable93458DBB", + "VpcPublicSubnet3RouteTableAssociation1F1EDF02", + "VpcPublicSubnet3SubnetBE12F0B6", + "Vpc8378EB38", + "VpcVPCGWBF912B6E" + ] }, "Cluster9EE0221C": { "Type": "Custom::AWSCDK-EKS-Cluster", @@ -818,22 +919,14 @@ ] }, "Config": { + "version": "1.16", "roleArn": { "Fn::GetAtt": [ "ClusterRoleFA261979", "Arn" ] }, - "version": "1.16", "resourcesVpcConfig": { - "securityGroupIds": [ - { - "Fn::GetAtt": [ - "ClusterControlPlaneSecurityGroupD274242C", - "GroupId" - ] - } - ], "subnetIds": [ { "Ref": "VpcPublicSubnet1Subnet5C2D37C4" @@ -853,7 +946,17 @@ { "Ref": "VpcPrivateSubnet3SubnetF258B56E" } - ] + ], + "securityGroupIds": [ + { + "Fn::GetAtt": [ + "ClusterControlPlaneSecurityGroupD274242C", + "GroupId" + ] + } + ], + "endpointPublicAccess": true, + "endpointPrivateAccess": true } }, "AssumeRoleArn": { @@ -865,8 +968,38 @@ "AttributesRevision": 2 }, "DependsOn": [ + "ClusterKubectlProviderSecurityGroup2D90691C", "ClusterCreationRoleDefaultPolicyE8BDFC7B", - "ClusterCreationRole360249B6" + "ClusterCreationRole360249B6", + "VpcIGWD7BA715C", + "VpcPrivateSubnet1DefaultRouteBE02A9ED", + "VpcPrivateSubnet1RouteTableB2C5B500", + "VpcPrivateSubnet1RouteTableAssociation70C59FA6", + "VpcPrivateSubnet1Subnet536B997A", + "VpcPrivateSubnet2DefaultRoute060D2087", + "VpcPrivateSubnet2RouteTableA678073B", + "VpcPrivateSubnet2RouteTableAssociationA89CAD56", + "VpcPrivateSubnet2Subnet3788AAA1", + "VpcPrivateSubnet3DefaultRoute94B74F0D", + "VpcPrivateSubnet3RouteTableD98824C7", + "VpcPrivateSubnet3RouteTableAssociation16BDDC43", + "VpcPrivateSubnet3SubnetF258B56E", + "VpcPublicSubnet1DefaultRoute3DA9E72A", + "VpcPublicSubnet1EIPD7E02669", + "VpcPublicSubnet1NATGateway4D7517AA", + "VpcPublicSubnet1RouteTable6C95E38E", + "VpcPublicSubnet1RouteTableAssociation97140677", + "VpcPublicSubnet1Subnet5C2D37C4", + "VpcPublicSubnet2DefaultRoute97F91067", + "VpcPublicSubnet2RouteTable94F7E489", + "VpcPublicSubnet2RouteTableAssociationDD5762D8", + "VpcPublicSubnet2Subnet691E08A3", + "VpcPublicSubnet3DefaultRoute4697774F", + "VpcPublicSubnet3RouteTable93458DBB", + "VpcPublicSubnet3RouteTableAssociation1F1EDF02", + "VpcPublicSubnet3SubnetBE12F0B6", + "Vpc8378EB38", + "VpcVPCGWBF912B6E" ], "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -2724,7 +2857,7 @@ }, "/", { - "Ref": "AssetParameters5215f685494c7a295ec1b06b713a041a82e7ac216473965711a88e32405e9053S3Bucket50B33A86" + "Ref": "AssetParameterse8f5d2a182613ad64e98c81d59e2ad3ecb46c92c5b51c3612a5c614a0715e57bS3Bucket393DA96E" }, "/", { @@ -2734,7 +2867,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5215f685494c7a295ec1b06b713a041a82e7ac216473965711a88e32405e9053S3VersionKey1FB82B13" + "Ref": "AssetParameterse8f5d2a182613ad64e98c81d59e2ad3ecb46c92c5b51c3612a5c614a0715e57bS3VersionKey0633C6DF" } ] } @@ -2747,7 +2880,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5215f685494c7a295ec1b06b713a041a82e7ac216473965711a88e32405e9053S3VersionKey1FB82B13" + "Ref": "AssetParameterse8f5d2a182613ad64e98c81d59e2ad3ecb46c92c5b51c3612a5c614a0715e57bS3VersionKey0633C6DF" } ] } @@ -2757,17 +2890,17 @@ ] }, "Parameters": { - "referencetoawscdkeksclustertestAssetParametersc23ce59a47ffb1e28812148fb83f7dcb0d94f1f0286e122a2f1aa189c0b35d03S3BucketD5010C93Ref": { - "Ref": "AssetParametersc23ce59a47ffb1e28812148fb83f7dcb0d94f1f0286e122a2f1aa189c0b35d03S3Bucket2F8CA18B" + "referencetoawscdkeksclustertestAssetParameters00ba02e613a29439c93f9aef4e82e253763eb70cd32026df071449485c692791S3Bucket363F6F79Ref": { + "Ref": "AssetParameters00ba02e613a29439c93f9aef4e82e253763eb70cd32026df071449485c692791S3Bucket26C90BA0" }, - "referencetoawscdkeksclustertestAssetParametersc23ce59a47ffb1e28812148fb83f7dcb0d94f1f0286e122a2f1aa189c0b35d03S3VersionKeyAC8DDB71Ref": { - "Ref": "AssetParametersc23ce59a47ffb1e28812148fb83f7dcb0d94f1f0286e122a2f1aa189c0b35d03S3VersionKeyEFEE8BE5" + "referencetoawscdkeksclustertestAssetParameters00ba02e613a29439c93f9aef4e82e253763eb70cd32026df071449485c692791S3VersionKeyDC22C51CRef": { + "Ref": "AssetParameters00ba02e613a29439c93f9aef4e82e253763eb70cd32026df071449485c692791S3VersionKeyD269C675" }, - "referencetoawscdkeksclustertestAssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3Bucket8E231383Ref": { - "Ref": "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3Bucket0EEA1C2E" + "referencetoawscdkeksclustertestAssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cS3BucketA9A24CF5Ref": { + "Ref": "AssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cS3BucketF1BD2256" }, - "referencetoawscdkeksclustertestAssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3VersionKey33D81F32Ref": { - "Ref": "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3VersionKey7BCE18C9" + "referencetoawscdkeksclustertestAssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cS3VersionKey6036F880Ref": { + "Ref": "AssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cS3VersionKeyF47FA401" } } } @@ -2785,7 +2918,7 @@ }, "/", { - "Ref": "AssetParameters2181e1ea22ea11a566260dec2f26c5f66ac77bb1b73812ba467b9c3bc564e42bS3BucketBEF9DA08" + "Ref": "AssetParameters5db67dc64d67f3574c3c3e10970910e121e77f67974ab320c4dc47af2f88d2feS3Bucket864A12C7" }, "/", { @@ -2795,7 +2928,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters2181e1ea22ea11a566260dec2f26c5f66ac77bb1b73812ba467b9c3bc564e42bS3VersionKey8B401BBD" + "Ref": "AssetParameters5db67dc64d67f3574c3c3e10970910e121e77f67974ab320c4dc47af2f88d2feS3VersionKeyD0F4176F" } ] } @@ -2808,7 +2941,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters2181e1ea22ea11a566260dec2f26c5f66ac77bb1b73812ba467b9c3bc564e42bS3VersionKey8B401BBD" + "Ref": "AssetParameters5db67dc64d67f3574c3c3e10970910e121e77f67974ab320c4dc47af2f88d2feS3VersionKeyD0F4176F" } ] } @@ -2818,17 +2951,17 @@ ] }, "Parameters": { - "referencetoawscdkeksclustertestAssetParameters2d65340a9414c04d1844e421bd328aa3b80015d6a02e74afe9a222168b2ba050S3BucketA41B2C70Ref": { - "Ref": "AssetParameters2d65340a9414c04d1844e421bd328aa3b80015d6a02e74afe9a222168b2ba050S3Bucket0EAA682D" + "referencetoawscdkeksclustertestAssetParameters649b09403c8414e624c965d6c2f0e41c341c2afa5d8e7bae4ac5746fe230f502S3Bucket8095B011Ref": { + "Ref": "AssetParameters649b09403c8414e624c965d6c2f0e41c341c2afa5d8e7bae4ac5746fe230f502S3BucketE7D09A6B" }, - "referencetoawscdkeksclustertestAssetParameters2d65340a9414c04d1844e421bd328aa3b80015d6a02e74afe9a222168b2ba050S3VersionKey4E1E47F7Ref": { - "Ref": "AssetParameters2d65340a9414c04d1844e421bd328aa3b80015d6a02e74afe9a222168b2ba050S3VersionKeyF3400812" + "referencetoawscdkeksclustertestAssetParameters649b09403c8414e624c965d6c2f0e41c341c2afa5d8e7bae4ac5746fe230f502S3VersionKeyFE6DC258Ref": { + "Ref": "AssetParameters649b09403c8414e624c965d6c2f0e41c341c2afa5d8e7bae4ac5746fe230f502S3VersionKey1DA734B2" }, - "referencetoawscdkeksclustertestAssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3Bucket8E231383Ref": { - "Ref": "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3Bucket0EEA1C2E" + "referencetoawscdkeksclustertestAssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cS3BucketA9A24CF5Ref": { + "Ref": "AssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cS3BucketF1BD2256" }, - "referencetoawscdkeksclustertestAssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3VersionKey33D81F32Ref": { - "Ref": "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3VersionKey7BCE18C9" + "referencetoawscdkeksclustertestAssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cS3VersionKey6036F880Ref": { + "Ref": "AssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cS3VersionKeyF47FA401" } } } @@ -2860,7 +2993,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters3b28f4ee261986c158a160900e3042a61238f644fe502199d60bcea592128086S3Bucket57C0655B" + "Ref": "AssetParameters952bd1c03e8201c4c1c67d6de0f3fdaaf88fda05f89a1232c3f6364343cd5344S3Bucket055DC235" }, "S3Key": { "Fn::Join": [ @@ -2873,7 +3006,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters3b28f4ee261986c158a160900e3042a61238f644fe502199d60bcea592128086S3VersionKey4BC65AD6" + "Ref": "AssetParameters952bd1c03e8201c4c1c67d6de0f3fdaaf88fda05f89a1232c3f6364343cd5344S3VersionKey2FFFA299" } ] } @@ -2886,7 +3019,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters3b28f4ee261986c158a160900e3042a61238f644fe502199d60bcea592128086S3VersionKey4BC65AD6" + "Ref": "AssetParameters952bd1c03e8201c4c1c67d6de0f3fdaaf88fda05f89a1232c3f6364343cd5344S3VersionKey2FFFA299" } ] } @@ -2959,7 +3092,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161S3Bucket0C424907" + "Ref": "AssetParameterseb7a9b73a02dcd848325fc3abc22c1923c364d7480e06bd68a337dc3f33143d3S3BucketB6A9971A" }, "S3Key": { "Fn::Join": [ @@ -2972,7 +3105,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161S3VersionKey6841F1F8" + "Ref": "AssetParameterseb7a9b73a02dcd848325fc3abc22c1923c364d7480e06bd68a337dc3f33143d3S3VersionKey08BBD845" } ] } @@ -2985,7 +3118,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161S3VersionKey6841F1F8" + "Ref": "AssetParameterseb7a9b73a02dcd848325fc3abc22c1923c364d7480e06bd68a337dc3f33143d3S3VersionKey08BBD845" } ] } @@ -3099,89 +3232,89 @@ } }, "Parameters": { - "AssetParametersc23ce59a47ffb1e28812148fb83f7dcb0d94f1f0286e122a2f1aa189c0b35d03S3Bucket2F8CA18B": { + "AssetParameters00ba02e613a29439c93f9aef4e82e253763eb70cd32026df071449485c692791S3Bucket26C90BA0": { "Type": "String", - "Description": "S3 bucket for asset \"c23ce59a47ffb1e28812148fb83f7dcb0d94f1f0286e122a2f1aa189c0b35d03\"" + "Description": "S3 bucket for asset \"00ba02e613a29439c93f9aef4e82e253763eb70cd32026df071449485c692791\"" }, - "AssetParametersc23ce59a47ffb1e28812148fb83f7dcb0d94f1f0286e122a2f1aa189c0b35d03S3VersionKeyEFEE8BE5": { + "AssetParameters00ba02e613a29439c93f9aef4e82e253763eb70cd32026df071449485c692791S3VersionKeyD269C675": { "Type": "String", - "Description": "S3 key for asset version \"c23ce59a47ffb1e28812148fb83f7dcb0d94f1f0286e122a2f1aa189c0b35d03\"" + "Description": "S3 key for asset version \"00ba02e613a29439c93f9aef4e82e253763eb70cd32026df071449485c692791\"" }, - "AssetParametersc23ce59a47ffb1e28812148fb83f7dcb0d94f1f0286e122a2f1aa189c0b35d03ArtifactHashC187523A": { + "AssetParameters00ba02e613a29439c93f9aef4e82e253763eb70cd32026df071449485c692791ArtifactHashAADC8B03": { "Type": "String", - "Description": "Artifact hash for asset \"c23ce59a47ffb1e28812148fb83f7dcb0d94f1f0286e122a2f1aa189c0b35d03\"" + "Description": "Artifact hash for asset \"00ba02e613a29439c93f9aef4e82e253763eb70cd32026df071449485c692791\"" }, - "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3Bucket0EEA1C2E": { + "AssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cS3BucketF1BD2256": { "Type": "String", - "Description": "S3 bucket for asset \"956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3\"" + "Description": "S3 bucket for asset \"974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74c\"" }, - "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3VersionKey7BCE18C9": { + "AssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cS3VersionKeyF47FA401": { "Type": "String", - "Description": "S3 key for asset version \"956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3\"" + "Description": "S3 key for asset version \"974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74c\"" }, - "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3ArtifactHash2CBB11D2": { + "AssetParameters974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74cArtifactHash5C0B1EA0": { "Type": "String", - "Description": "Artifact hash for asset \"956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3\"" + "Description": "Artifact hash for asset \"974a6fb29abbd1d98fce56346da3743e79277f0f52e0e2cdf3f1867ac5b1e74c\"" }, - "AssetParameters2d65340a9414c04d1844e421bd328aa3b80015d6a02e74afe9a222168b2ba050S3Bucket0EAA682D": { + "AssetParameters649b09403c8414e624c965d6c2f0e41c341c2afa5d8e7bae4ac5746fe230f502S3BucketE7D09A6B": { "Type": "String", - "Description": "S3 bucket for asset \"2d65340a9414c04d1844e421bd328aa3b80015d6a02e74afe9a222168b2ba050\"" + "Description": "S3 bucket for asset \"649b09403c8414e624c965d6c2f0e41c341c2afa5d8e7bae4ac5746fe230f502\"" }, - "AssetParameters2d65340a9414c04d1844e421bd328aa3b80015d6a02e74afe9a222168b2ba050S3VersionKeyF3400812": { + "AssetParameters649b09403c8414e624c965d6c2f0e41c341c2afa5d8e7bae4ac5746fe230f502S3VersionKey1DA734B2": { "Type": "String", - "Description": "S3 key for asset version \"2d65340a9414c04d1844e421bd328aa3b80015d6a02e74afe9a222168b2ba050\"" + "Description": "S3 key for asset version \"649b09403c8414e624c965d6c2f0e41c341c2afa5d8e7bae4ac5746fe230f502\"" }, - "AssetParameters2d65340a9414c04d1844e421bd328aa3b80015d6a02e74afe9a222168b2ba050ArtifactHashF4CEE19F": { + "AssetParameters649b09403c8414e624c965d6c2f0e41c341c2afa5d8e7bae4ac5746fe230f502ArtifactHash815E1969": { "Type": "String", - "Description": "Artifact hash for asset \"2d65340a9414c04d1844e421bd328aa3b80015d6a02e74afe9a222168b2ba050\"" + "Description": "Artifact hash for asset \"649b09403c8414e624c965d6c2f0e41c341c2afa5d8e7bae4ac5746fe230f502\"" }, - "AssetParameters3b28f4ee261986c158a160900e3042a61238f644fe502199d60bcea592128086S3Bucket57C0655B": { + "AssetParameters952bd1c03e8201c4c1c67d6de0f3fdaaf88fda05f89a1232c3f6364343cd5344S3Bucket055DC235": { "Type": "String", - "Description": "S3 bucket for asset \"3b28f4ee261986c158a160900e3042a61238f644fe502199d60bcea592128086\"" + "Description": "S3 bucket for asset \"952bd1c03e8201c4c1c67d6de0f3fdaaf88fda05f89a1232c3f6364343cd5344\"" }, - "AssetParameters3b28f4ee261986c158a160900e3042a61238f644fe502199d60bcea592128086S3VersionKey4BC65AD6": { + "AssetParameters952bd1c03e8201c4c1c67d6de0f3fdaaf88fda05f89a1232c3f6364343cd5344S3VersionKey2FFFA299": { "Type": "String", - "Description": "S3 key for asset version \"3b28f4ee261986c158a160900e3042a61238f644fe502199d60bcea592128086\"" + "Description": "S3 key for asset version \"952bd1c03e8201c4c1c67d6de0f3fdaaf88fda05f89a1232c3f6364343cd5344\"" }, - "AssetParameters3b28f4ee261986c158a160900e3042a61238f644fe502199d60bcea592128086ArtifactHashD8D99435": { + "AssetParameters952bd1c03e8201c4c1c67d6de0f3fdaaf88fda05f89a1232c3f6364343cd5344ArtifactHash1AB042BC": { "Type": "String", - "Description": "Artifact hash for asset \"3b28f4ee261986c158a160900e3042a61238f644fe502199d60bcea592128086\"" + "Description": "Artifact hash for asset \"952bd1c03e8201c4c1c67d6de0f3fdaaf88fda05f89a1232c3f6364343cd5344\"" }, - "AssetParametersea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161S3Bucket0C424907": { + "AssetParameterseb7a9b73a02dcd848325fc3abc22c1923c364d7480e06bd68a337dc3f33143d3S3BucketB6A9971A": { "Type": "String", - "Description": "S3 bucket for asset \"ea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161\"" + "Description": "S3 bucket for asset \"eb7a9b73a02dcd848325fc3abc22c1923c364d7480e06bd68a337dc3f33143d3\"" }, - "AssetParametersea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161S3VersionKey6841F1F8": { + "AssetParameterseb7a9b73a02dcd848325fc3abc22c1923c364d7480e06bd68a337dc3f33143d3S3VersionKey08BBD845": { "Type": "String", - "Description": "S3 key for asset version \"ea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161\"" + "Description": "S3 key for asset version \"eb7a9b73a02dcd848325fc3abc22c1923c364d7480e06bd68a337dc3f33143d3\"" }, - "AssetParametersea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161ArtifactHash67B22EF2": { + "AssetParameterseb7a9b73a02dcd848325fc3abc22c1923c364d7480e06bd68a337dc3f33143d3ArtifactHashADF25EB1": { "Type": "String", - "Description": "Artifact hash for asset \"ea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161\"" + "Description": "Artifact hash for asset \"eb7a9b73a02dcd848325fc3abc22c1923c364d7480e06bd68a337dc3f33143d3\"" }, - "AssetParameters5215f685494c7a295ec1b06b713a041a82e7ac216473965711a88e32405e9053S3Bucket50B33A86": { + "AssetParameterse8f5d2a182613ad64e98c81d59e2ad3ecb46c92c5b51c3612a5c614a0715e57bS3Bucket393DA96E": { "Type": "String", - "Description": "S3 bucket for asset \"5215f685494c7a295ec1b06b713a041a82e7ac216473965711a88e32405e9053\"" + "Description": "S3 bucket for asset \"e8f5d2a182613ad64e98c81d59e2ad3ecb46c92c5b51c3612a5c614a0715e57b\"" }, - "AssetParameters5215f685494c7a295ec1b06b713a041a82e7ac216473965711a88e32405e9053S3VersionKey1FB82B13": { + "AssetParameterse8f5d2a182613ad64e98c81d59e2ad3ecb46c92c5b51c3612a5c614a0715e57bS3VersionKey0633C6DF": { "Type": "String", - "Description": "S3 key for asset version \"5215f685494c7a295ec1b06b713a041a82e7ac216473965711a88e32405e9053\"" + "Description": "S3 key for asset version \"e8f5d2a182613ad64e98c81d59e2ad3ecb46c92c5b51c3612a5c614a0715e57b\"" }, - "AssetParameters5215f685494c7a295ec1b06b713a041a82e7ac216473965711a88e32405e9053ArtifactHash599411DD": { + "AssetParameterse8f5d2a182613ad64e98c81d59e2ad3ecb46c92c5b51c3612a5c614a0715e57bArtifactHashA64B37F7": { "Type": "String", - "Description": "Artifact hash for asset \"5215f685494c7a295ec1b06b713a041a82e7ac216473965711a88e32405e9053\"" + "Description": "Artifact hash for asset \"e8f5d2a182613ad64e98c81d59e2ad3ecb46c92c5b51c3612a5c614a0715e57b\"" }, - "AssetParameters2181e1ea22ea11a566260dec2f26c5f66ac77bb1b73812ba467b9c3bc564e42bS3BucketBEF9DA08": { + "AssetParameters5db67dc64d67f3574c3c3e10970910e121e77f67974ab320c4dc47af2f88d2feS3Bucket864A12C7": { "Type": "String", - "Description": "S3 bucket for asset \"2181e1ea22ea11a566260dec2f26c5f66ac77bb1b73812ba467b9c3bc564e42b\"" + "Description": "S3 bucket for asset \"5db67dc64d67f3574c3c3e10970910e121e77f67974ab320c4dc47af2f88d2fe\"" }, - "AssetParameters2181e1ea22ea11a566260dec2f26c5f66ac77bb1b73812ba467b9c3bc564e42bS3VersionKey8B401BBD": { + "AssetParameters5db67dc64d67f3574c3c3e10970910e121e77f67974ab320c4dc47af2f88d2feS3VersionKeyD0F4176F": { "Type": "String", - "Description": "S3 key for asset version \"2181e1ea22ea11a566260dec2f26c5f66ac77bb1b73812ba467b9c3bc564e42b\"" + "Description": "S3 key for asset version \"5db67dc64d67f3574c3c3e10970910e121e77f67974ab320c4dc47af2f88d2fe\"" }, - "AssetParameters2181e1ea22ea11a566260dec2f26c5f66ac77bb1b73812ba467b9c3bc564e42bArtifactHash87F44C09": { + "AssetParameters5db67dc64d67f3574c3c3e10970910e121e77f67974ab320c4dc47af2f88d2feArtifactHash2B9F340F": { "Type": "String", - "Description": "Artifact hash for asset \"2181e1ea22ea11a566260dec2f26c5f66ac77bb1b73812ba467b9c3bc564e42b\"" + "Description": "Artifact hash for asset \"5db67dc64d67f3574c3c3e10970910e121e77f67974ab320c4dc47af2f88d2fe\"" }, "SsmParameterValueawsserviceeksoptimizedami116amazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { "Type": "AWS::SSM::Parameter::Value", diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.kubectl-disabled.ts b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.kubectl-disabled.ts index 2282772d42a7c..333cf0a77791a 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.kubectl-disabled.ts +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.kubectl-disabled.ts @@ -9,9 +9,8 @@ class EksClusterStack extends TestStack { const vpc = new ec2.Vpc(this, 'VPC'); - const cluster = new eks.Cluster(this, 'EKSCluster', { + const cluster = new eks.LegacyCluster(this, 'EKSCluster', { vpc, - kubectlEnabled: false, defaultCapacity: 0, version: eks.KubernetesVersion.V1_16, }); diff --git a/packages/@aws-cdk/aws-eks/test/test.awsauth.ts b/packages/@aws-cdk/aws-eks/test/test.awsauth.ts index 998dd00c184fb..f0f624730f2a5 100644 --- a/packages/@aws-cdk/aws-eks/test/test.awsauth.ts +++ b/packages/@aws-cdk/aws-eks/test/test.awsauth.ts @@ -53,6 +53,20 @@ export = { '', [ '[{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"aws-auth","namespace":"kube-system"},"data":{"mapRoles":"[{\\"rolearn\\":\\"', + { + 'Fn::GetAtt': [ + 'ClusterMastersRole9AA35625', + 'Arn', + ], + }, + '\\",\\"username\\":\\"', + { + 'Fn::GetAtt': [ + 'ClusterMastersRole9AA35625', + 'Arn', + ], + }, + '\\",\\"groups\\":[\\"system:masters\\"]},{\\"rolearn\\":\\"', { 'Fn::GetAtt': [ 'ClusterNodegroupDefaultCapacityNodeGroupRole55953B04', @@ -128,13 +142,27 @@ export = { '', [ '[{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"aws-auth","namespace":"kube-system"},"data":{"mapRoles":"[{\\"rolearn\\":\\"', + { + 'Fn::GetAtt': [ + 'ClusterMastersRole9AA35625', + 'Arn', + ], + }, + '\\",\\"username\\":\\"', + { + 'Fn::GetAtt': [ + 'ClusterMastersRole9AA35625', + 'Arn', + ], + }, + '\\",\\"groups\\":[\\"system:masters\\"]},{\\"rolearn\\":\\"', { 'Fn::GetAtt': [ 'ClusterNodegroupDefaultCapacityNodeGroupRole55953B04', 'Arn', ], }, - '\\",\\"username\\":\\"system:node:{{EC2PrivateDNSName}}\\",\\"groups\\":[\\"system:bootstrappers\\",\\"system:nodes\\"]},{\\"rolearn\\":\\"arn:aws:iam::123456789012:role/S3Access\\",\\"username\\":\\"arn:aws:iam::123456789012:role/S3Access\\",\\"groups\\":[\\"group1\\"]}]\",\"mapUsers\":\"[{\\"userarn\\":\\"arn:', + '\\",\\"username\\":\\"system:node:{{EC2PrivateDNSName}}\\",\\"groups\\":[\\"system:bootstrappers\\",\\"system:nodes\\"]},{\\"rolearn\\":\\"arn:aws:iam::123456789012:role/S3Access\\",\\"username\\":\\"arn:aws:iam::123456789012:role/S3Access\\",\\"groups\\":[\\"group1\\"]}]","mapUsers":"[{\\"userarn\\":\\"arn:', { Ref: 'AWS::Partition', }, @@ -175,6 +203,20 @@ export = { '', [ '[{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"aws-auth","namespace":"kube-system"},"data":{"mapRoles":"[{\\"rolearn\\":\\"', + { + 'Fn::GetAtt': [ + 'ClusterMastersRole9AA35625', + 'Arn', + ], + }, + '\\",\\"username\\":\\"', + { + 'Fn::GetAtt': [ + 'ClusterMastersRole9AA35625', + 'Arn', + ], + }, + '\\",\\"groups\\":[\\"system:masters\\"]},{\\"rolearn\\":\\"', { 'Fn::GetAtt': [ 'ClusterNodegroupDefaultCapacityNodeGroupRole55953B04', diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster.ts b/packages/@aws-cdk/aws-eks/test/test.cluster.ts index 1b87a8d94a152..aaf1d0a2de124 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import { countResources, expect, haveResource, haveResourceLike, not } from '@aws-cdk/assert'; +import { countResources, expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; @@ -20,17 +20,22 @@ export = { const { stack, vpc } = testFixture(); // WHEN - new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0, version: CLUSTER_VERSION }); + new eks.Cluster(stack, 'Cluster', { vpc, defaultCapacity: 0, version: CLUSTER_VERSION }); // THEN - expect(stack).to(haveResourceLike('AWS::EKS::Cluster', { - ResourcesVpcConfig: { - SubnetIds: [ - { Ref: 'VPCPublicSubnet1SubnetB4246D30' }, - { Ref: 'VPCPublicSubnet2Subnet74179F39' }, - { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, - { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, - ], + expect(stack).to(haveResourceLike('Custom::AWSCDK-EKS-Cluster', { + Config: { + roleArn: { 'Fn::GetAtt': [ 'ClusterRoleFA261979', 'Arn' ] }, + version: '1.16', + resourcesVpcConfig: { + securityGroupIds: [ { 'Fn::GetAtt': [ 'ClusterControlPlaneSecurityGroupD274242C', 'GroupId' ] } ], + subnetIds: [ + { Ref: 'VPCPublicSubnet1SubnetB4246D30' }, + { Ref: 'VPCPublicSubnet2Subnet74179F39' }, + { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, + { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, + ], + }, }, })); @@ -44,7 +49,7 @@ export = { // WHEN const vpc = new ec2.Vpc(stack, 'VPC'); - new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: true, defaultCapacity: 0, version: CLUSTER_VERSION }); + new eks.Cluster(stack, 'Cluster', { vpc, defaultCapacity: 0, version: CLUSTER_VERSION }); const layer = KubectlLayer.getOrCreate(stack, {}); // THEN @@ -65,7 +70,7 @@ export = { // WHEN const vpc = new ec2.Vpc(stack, 'VPC'); - new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: true, defaultCapacity: 0, version: CLUSTER_VERSION }); + new eks.Cluster(stack, 'Cluster', { vpc, defaultCapacity: 0, version: CLUSTER_VERSION }); new KubectlLayer(stack, 'NewLayer'); const layer = KubectlLayer.getOrCreate(stack); @@ -160,7 +165,7 @@ export = { const { stack, vpc } = testFixture(); // WHEN - new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0, version: CLUSTER_VERSION }); + new eks.Cluster(stack, 'Cluster', { vpc, defaultCapacity: 0, version: CLUSTER_VERSION }); // THEN expect(stack).to(haveResource('AWS::EC2::Subnet', { @@ -180,7 +185,7 @@ export = { const { stack, vpc } = testFixture(); // WHEN - new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0, version: CLUSTER_VERSION }); + new eks.Cluster(stack, 'Cluster', { vpc, defaultCapacity: 0, version: CLUSTER_VERSION }); // THEN expect(stack).to(haveResource('AWS::EC2::Subnet', { @@ -200,7 +205,7 @@ export = { // GIVEN const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { - vpc, kubectlEnabled: false, + vpc, defaultCapacity: 0, version: CLUSTER_VERSION, }); @@ -214,7 +219,7 @@ export = { expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { Tags: [ { - Key: { 'Fn::Join': ['', ['kubernetes.io/cluster/', { Ref: 'ClusterEB0386A7' }]] }, + Key: { 'Fn::Join': ['', ['kubernetes.io/cluster/', { Ref: 'Cluster9EE0221C' }]] }, PropagateAtLaunch: true, Value: 'owned', }, @@ -266,7 +271,6 @@ export = { const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { vpc, - kubectlEnabled: false, defaultCapacity: 0, version: CLUSTER_VERSION, }); @@ -281,7 +285,7 @@ export = { expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { Tags: [ { - Key: { 'Fn::Join': ['', ['kubernetes.io/cluster/', { Ref: 'ClusterEB0386A7' }]] }, + Key: { 'Fn::Join': ['', ['kubernetes.io/cluster/', { Ref: 'Cluster9EE0221C' }]] }, PropagateAtLaunch: true, Value: 'owned', }, @@ -300,7 +304,6 @@ export = { const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { vpc, - kubectlEnabled: false, defaultCapacity: 0, version: CLUSTER_VERSION, }); @@ -319,7 +322,6 @@ export = { const stack2 = new cdk.Stack(app, 'stack2', { env: { region: 'us-east-1' } }); const cluster = new eks.Cluster(stack1, 'Cluster', { vpc, - kubectlEnabled: false, defaultCapacity: 0, version: CLUSTER_VERSION, }); @@ -344,7 +346,7 @@ export = { Outputs: { ClusterARN: { Value: { - 'Fn::ImportValue': 'Stack:ExportsOutputFnGetAttClusterEB0386A7Arn2F2E3C3F', + 'Fn::ImportValue': 'Stack:ExportsOutputFnGetAttCluster9EE0221CArn9E0B683E', }, }, }, @@ -352,26 +354,7 @@ export = { test.done(); }, - 'disabled features when kubectl is disabled'(test: Test) { - // GIVEN - const { stack, vpc } = testFixture(); - const cluster = new eks.Cluster(stack, 'Cluster', { - vpc, - kubectlEnabled: false, - defaultCapacity: 0, - version: CLUSTER_VERSION, - }); - - test.throws(() => cluster.awsAuth, /Cannot define aws-auth mappings if kubectl is disabled/); - test.throws(() => cluster.addResource('foo', {}), /Unable to perform this operation since kubectl is not enabled for this cluster/); - test.throws(() => cluster.addCapacity('boo', { instanceType: new ec2.InstanceType('r5d.24xlarge'), mapRole: true }), - /Cannot map instance IAM role to RBAC if kubectl is disabled for the cluster/); - test.throws(() => new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }), /Unable to perform this operation since kubectl is not enabled for this cluster/); - test.throws(() => cluster.openIdConnectProvider, /Cannot specify a OpenID Connect Provider if kubectl is disabled/); - test.done(); - }, - - 'mastersRole can be used to map an IAM role to "system:masters" (required kubectl)'(test: Test) { + 'mastersRole can be used to map an IAM role to "system:masters"'(test: Test) { // GIVEN const { stack, vpc } = testFixture(); const role = new iam.Role(stack, 'role', { assumedBy: new iam.AnyPrincipal() }); @@ -475,7 +458,7 @@ export = { test.done(); }, - 'when kubectl is enabled (default) adding capacity will automatically map its IAM role'(test: Test) { + 'adding capacity will automatically map its IAM role'(test: Test) { // GIVEN const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { @@ -496,6 +479,20 @@ export = { '', [ '[{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"aws-auth","namespace":"kube-system"},"data":{"mapRoles":"[{\\"rolearn\\":\\"', + { + 'Fn::GetAtt': [ + 'ClusterMastersRole9AA35625', + 'Arn', + ], + }, + '\\",\\"username\\":\\"', + { + 'Fn::GetAtt': [ + 'ClusterMastersRole9AA35625', + 'Arn', + ], + }, + '\\",\\"groups\\":[\\"system:masters\\"]},{\\"rolearn\\":\\"', { 'Fn::GetAtt': [ 'ClusterdefaultInstanceRoleF20A29CD', @@ -527,27 +524,30 @@ export = { }); // THEN - expect(stack).to(not(haveResource(eks.KubernetesResource.RESOURCE_TYPE))); - test.done(); - }, - - 'addCapacity will *not* map the IAM role if kubectl is disabled'(test: Test) { - // GIVEN - const { stack, vpc } = testFixture(); - const cluster = new eks.Cluster(stack, 'Cluster', { - vpc, - kubectlEnabled: false, - defaultCapacity: 0, - version: CLUSTER_VERSION, - }); - - // WHEN - cluster.addCapacity('default', { - instanceType: new ec2.InstanceType('t2.nano'), - }); - - // THEN - expect(stack).to(not(haveResource(eks.KubernetesResource.RESOURCE_TYPE))); + expect(stack).to(haveResource(eks.KubernetesResource.RESOURCE_TYPE, { + Manifest: { + 'Fn::Join': [ + '', + [ + '[{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"aws-auth","namespace":"kube-system"},"data":{"mapRoles":"[{\\"rolearn\\":\\"', + { + 'Fn::GetAtt': [ + 'ClusterMastersRole9AA35625', + 'Arn', + ], + }, + '\\",\\"username\\":\\"', + { + 'Fn::GetAtt': [ + 'ClusterMastersRole9AA35625', + 'Arn', + ], + }, + '\\",\\"groups\\":[\\"system:masters\\"]}]","mapUsers":"[]","mapAccounts":"[]"}}]', + ], + ], + }, + })); test.done(); }, @@ -563,8 +563,8 @@ export = { const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; test.deepEqual(template.Outputs, { - ClusterConfigCommand43AAE40F: { Value: { 'Fn::Join': ['', ['aws eks update-kubeconfig --name ', { Ref: 'Cluster9EE0221C' }, ' --region us-east-1']] } }, - ClusterGetTokenCommand06AE992E: { Value: { 'Fn::Join': ['', ['aws eks get-token --cluster-name ', { Ref: 'Cluster9EE0221C' }, ' --region us-east-1']] } }, + ClusterConfigCommand43AAE40F: { Value: { 'Fn::Join': ['', ['aws eks update-kubeconfig --name ', { Ref: 'Cluster9EE0221C' }, ' --region us-east-1 --role-arn ', { 'Fn::GetAtt': [ 'ClusterMastersRole9AA35625', 'Arn' ] } ] ] } }, + ClusterGetTokenCommand06AE992E: { Value: { 'Fn::Join': ['', ['aws eks get-token --cluster-name ', { Ref: 'Cluster9EE0221C' }, ' --region us-east-1 --role-arn ', { 'Fn::GetAtt': [ 'ClusterMastersRole9AA35625', 'Arn' ] } ] ] } }, }); test.done(); }, @@ -726,7 +726,7 @@ export = { test.done(); }, - 'if kubectl is enabled, the interrupt handler is added'(test: Test) { + 'interrupt handler is added'(test: Test) { // GIVEN const { stack } = testFixtureNoVpc(); const cluster = new eks.Cluster(stack, 'Cluster', { defaultCapacity: 0, version: CLUSTER_VERSION }); @@ -769,26 +769,6 @@ export = { test.done(); }, - 'if kubectl is disabled, interrupt handler is not added'(test: Test) { - // GIVEN - const { stack } = testFixtureNoVpc(); - const cluster = new eks.Cluster(stack, 'Cluster', { - defaultCapacity: 0, - kubectlEnabled: false, - version: CLUSTER_VERSION, - }); - - // WHEN - cluster.addCapacity('MyCapcity', { - instanceType: new ec2.InstanceType('m3.xlargs'), - spotPrice: '0.01', - }); - - // THEN - expect(stack).notTo(haveResource(eks.KubernetesResource.RESOURCE_TYPE)); - test.done(); - }, - }, }, @@ -895,6 +875,8 @@ export = { { Ref: 'MyClusterDefaultVpcPrivateSubnet1SubnetE1D0DCDB' }, { Ref: 'MyClusterDefaultVpcPrivateSubnet2Subnet11FEA8D0' }, ], + endpointPrivateAccess: true, + endpointPublicAccess: true, }, }, })); @@ -1378,5 +1360,313 @@ export = { } test.done(); }, + + }, + + 'kubectl provider passes environment to lambda'(test: Test) { + + const { stack } = testFixture(); + + const cluster = new eks.Cluster(stack, 'Cluster1', { + version: CLUSTER_VERSION, endpointAccess: eks.EndpointAccess.PRIVATE, + kubectlEnvironment: { + Foo: 'Bar', + }, + }); + + cluster.addResource('resource', { + kind: 'ConfigMap', + apiVersion: 'v1', + data: { + hello: 'world', + }, + metadata: { + name: 'config-map', + }, + }); + + // the kubectl provider is inside a nested stack. + const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; + expect(nested).to(haveResource('AWS::Lambda::Function', { + Environment: { + Variables: { + Foo: 'Bar', + }, + }, + })); + + test.done(); + }, + + 'endpoint access': { + + 'can configure private endpoint access'(test: Test) { + // GIVEN + const { stack } = testFixture(); + new eks.Cluster(stack, 'Cluster1', { version: CLUSTER_VERSION, endpointAccess: eks.EndpointAccess.PRIVATE }); + + expect(stack).to(haveResource('Custom::AWSCDK-EKS-Cluster', { + Config: { + roleArn: { 'Fn::GetAtt': ['Cluster1RoleE88C32AD', 'Arn'] }, + version: '1.16', + resourcesVpcConfig: { + securityGroupIds: [{ 'Fn::GetAtt': ['Cluster1ControlPlaneSecurityGroupF9C67C32', 'GroupId'] }], + subnetIds: [ + { Ref: 'Cluster1DefaultVpcPublicSubnet1SubnetBEABA6ED' }, + { Ref: 'Cluster1DefaultVpcPublicSubnet2Subnet947A5158' }, + { Ref: 'Cluster1DefaultVpcPrivateSubnet1Subnet4E30ECA1' }, + { Ref: 'Cluster1DefaultVpcPrivateSubnet2Subnet707FCD37' }, + ], + endpointPrivateAccess: true, + endpointPublicAccess: false, + }, + }, + })); + + test.done(); + }, + + 'can configure cidr blocks in public endpoint access'(test: Test) { + // GIVEN + const { stack } = testFixture(); + new eks.Cluster(stack, 'Cluster1', { version: CLUSTER_VERSION, endpointAccess: eks.EndpointAccess.PUBLIC.onlyFrom('1.2.3.4/5') }); + + expect(stack).to(haveResource('Custom::AWSCDK-EKS-Cluster', { + Config: { + roleArn: { 'Fn::GetAtt': ['Cluster1RoleE88C32AD', 'Arn'] }, + version: '1.16', + resourcesVpcConfig: { + securityGroupIds: [{ 'Fn::GetAtt': ['Cluster1ControlPlaneSecurityGroupF9C67C32', 'GroupId'] }], + subnetIds: [ + { Ref: 'Cluster1DefaultVpcPublicSubnet1SubnetBEABA6ED' }, + { Ref: 'Cluster1DefaultVpcPublicSubnet2Subnet947A5158' }, + { Ref: 'Cluster1DefaultVpcPrivateSubnet1Subnet4E30ECA1' }, + { Ref: 'Cluster1DefaultVpcPrivateSubnet2Subnet707FCD37' }, + ], + endpointPrivateAccess: false, + endpointPublicAccess: true, + publicAccessCidrs: ['1.2.3.4/5'], + }, + }, + })); + + test.done(); + }, + + 'kubectl provider chooses only private subnets'(test: Test) { + + const { stack } = testFixture(); + + const vpc = new ec2.Vpc(stack, 'Vpc', { + maxAzs: 2, + natGateways: 1, + subnetConfiguration: [ + { + subnetType: ec2.SubnetType.PRIVATE, + name: 'Private1', + }, + { + subnetType: ec2.SubnetType.PUBLIC, + name: 'Public1', + }, + ], + }); + + const cluster = new eks.Cluster(stack, 'Cluster1', { + version: CLUSTER_VERSION, endpointAccess: eks.EndpointAccess.PRIVATE, + vpc, + }); + + cluster.addResource('resource', { + kind: 'ConfigMap', + apiVersion: 'v1', + data: { + hello: 'world', + }, + metadata: { + name: 'config-map', + }, + }); + + // the kubectl provider is inside a nested stack. + const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; + expect(nested).to(haveResource('AWS::Lambda::Function', { + VpcConfig: { + SecurityGroupIds: [ + { + Ref: 'referencetoStackCluster1KubectlProviderSecurityGroupDF05D03AGroupId', + }, + ], + SubnetIds: [ + { + Ref: 'referencetoStackVpcPrivate1Subnet1Subnet6764A0F6Ref', + }, + { + Ref: 'referencetoStackVpcPrivate1Subnet2SubnetDFD49645Ref', + }, + ], + }, + })); + + test.done(); + }, + + 'kubectl provider limits number of subnets to 16'(test: Test) { + + const { stack } = testFixture(); + + const subnetConfiguration: ec2.SubnetConfiguration[] = []; + + for (let i = 0; i < 20; i++) { + subnetConfiguration.push( { + subnetType: ec2.SubnetType.PRIVATE, + name: `Private${i}`, + }, + ); + } + + subnetConfiguration.push( { + subnetType: ec2.SubnetType.PUBLIC, + name: 'Public1', + }); + + const vpc2 = new ec2.Vpc(stack, 'Vpc', { + maxAzs: 2, + natGateways: 1, + subnetConfiguration, + }); + + const cluster = new eks.Cluster(stack, 'Cluster1', { + version: CLUSTER_VERSION, endpointAccess: eks.EndpointAccess.PRIVATE, + vpc: vpc2, + }); + + cluster.addResource('resource', { + kind: 'ConfigMap', + apiVersion: 'v1', + data: { + hello: 'world', + }, + metadata: { + name: 'config-map', + }, + }); + + // the kubectl provider is inside a nested stack. + const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; + test.equal(16, expect(nested).value.Resources.Handler886CB40B.Properties.VpcConfig.SubnetIds.length); + + test.done(); + }, + + 'kubectl provider considers vpc subnet selection'(test: Test) { + + const { stack } = testFixture(); + + const subnetConfiguration: ec2.SubnetConfiguration[] = []; + + for (let i = 0; i < 20; i++) { + subnetConfiguration.push( { + subnetType: ec2.SubnetType.PRIVATE, + name: `Private${i}`, + }, + ); + } + + subnetConfiguration.push( { + subnetType: ec2.SubnetType.PUBLIC, + name: 'Public1', + }); + + const vpc2 = new ec2.Vpc(stack, 'Vpc', { + maxAzs: 2, + natGateways: 1, + subnetConfiguration, + }); + + const cluster = new eks.Cluster(stack, 'Cluster1', { + version: CLUSTER_VERSION, endpointAccess: eks.EndpointAccess.PRIVATE, + vpc: vpc2, + vpcSubnets: [{subnetGroupName: 'Private1'}, {subnetGroupName: 'Private2'}], + }); + + cluster.addResource('resource', { + kind: 'ConfigMap', + apiVersion: 'v1', + data: { + hello: 'world', + }, + metadata: { + name: 'config-map', + }, + }); + + // the kubectl provider is inside a nested stack. + const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; + expect(nested).to(haveResource('AWS::Lambda::Function', { + VpcConfig: { + SecurityGroupIds: [ + { + Ref: 'referencetoStackCluster1KubectlProviderSecurityGroupDF05D03AGroupId', + }, + ], + SubnetIds: [ + { + Ref: 'referencetoStackVpcPrivate1Subnet1Subnet6764A0F6Ref', + }, + { + Ref: 'referencetoStackVpcPrivate1Subnet2SubnetDFD49645Ref', + }, + { + Ref: 'referencetoStackVpcPrivate2Subnet1Subnet586AD392Ref', + }, + { + Ref: 'referencetoStackVpcPrivate2Subnet2SubnetE42148C0Ref', + }, + ], + }, + })); + + test.done(); + }, + + 'throw when private access is configured without dns support enabled for the VPC'(test: Test) { + + const { stack } = testFixture(); + + test.throws(() => { + new eks.Cluster(stack, 'Cluster', { + vpc: new ec2.Vpc(stack, 'Vpc', { + enableDnsSupport: false, + }), + version: CLUSTER_VERSION, + }); + }, /Private endpoint access requires the VPC to have DNS support and DNS hostnames enabled/); + test.done(); + }, + + 'throw when private access is configured without dns hostnames enabled for the VPC'(test: Test) { + + const { stack } = testFixture(); + + test.throws(() => { + new eks.Cluster(stack, 'Cluster', { + vpc: new ec2.Vpc(stack, 'Vpc', { + enableDnsHostnames: false, + }), + version: CLUSTER_VERSION, + }); + }, /Private endpoint access requires the VPC to have DNS support and DNS hostnames enabled/); + test.done(); + }, + + 'throw when cidrs are configured without public access endpoint'(test: Test) { + + test.throws(() => { + eks.EndpointAccess.PRIVATE.onlyFrom('1.2.3.4/5'); + }, /CIDR blocks can only be configured when public access is enabled/); + test.done(); + }, + }, }; diff --git a/packages/@aws-cdk/aws-eks/test/test.fargate.ts b/packages/@aws-cdk/aws-eks/test/test.fargate.ts index 531b49882b3c3..40d1b507da019 100644 --- a/packages/@aws-cdk/aws-eks/test/test.fargate.ts +++ b/packages/@aws-cdk/aws-eks/test/test.fargate.ts @@ -313,6 +313,20 @@ export = { '', [ '[{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"aws-auth","namespace":"kube-system"},"data":{"mapRoles":"[{\\"rolearn\\":\\"', + { + 'Fn::GetAtt': [ + 'FargateClusterMastersRole50BAF9FD', + 'Arn', + ], + }, + '\\",\\"username\\":\\"', + { + 'Fn::GetAtt': [ + 'FargateClusterMastersRole50BAF9FD', + 'Arn', + ], + }, + '\\",\\"groups\\":[\\"system:masters\\"]},{\\"rolearn\\":\\"', { 'Fn::GetAtt': [ 'FargateClusterfargateprofiledefaultPodExecutionRole66F2610E', @@ -327,20 +341,6 @@ export = { test.done(); }, - 'cannot be added to a cluster without kubectl enabled'(test: Test) { - // GIVEN - const stack = new Stack(); - const cluster = new eks.Cluster(stack, 'MyCluster', { kubectlEnabled: false, version: CLUSTER_VERSION }); - - // WHEN - test.throws(() => new eks.FargateProfile(stack, 'MyFargateProfile', { - cluster, - selectors: [ { namespace: 'default' } ], - }), /unsupported/); - - test.done(); - }, - 'allow cluster creation role to iam:PassRole on fargate pod execution role'(test: Test) { // GIVEN const stack = new Stack(); diff --git a/packages/@aws-cdk/aws-eks/test/test.legacy-cluster.ts b/packages/@aws-cdk/aws-eks/test/test.legacy-cluster.ts new file mode 100644 index 0000000000000..625f9f8950c32 --- /dev/null +++ b/packages/@aws-cdk/aws-eks/test/test.legacy-cluster.ts @@ -0,0 +1,590 @@ +import { expect, haveResource, haveResourceLike, not } from '@aws-cdk/assert'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as eks from '../lib'; +import { testFixture, testFixtureNoVpc } from './util'; + +/* eslint-disable max-len */ + +const CLUSTER_VERSION = eks.KubernetesVersion.V1_16; + +export = { + 'a default cluster spans all subnets'(test: Test) { + // GIVEN + const { stack, vpc } = testFixture(); + + // WHEN + new eks.LegacyCluster(stack, 'Cluster', { vpc, defaultCapacity: 0, version: CLUSTER_VERSION }); + + // THEN + expect(stack).to(haveResourceLike('AWS::EKS::Cluster', { + ResourcesVpcConfig: { + SubnetIds: [ + { Ref: 'VPCPublicSubnet1SubnetB4246D30' }, + { Ref: 'VPCPublicSubnet2Subnet74179F39' }, + { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, + { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, + ], + }, + })); + + test.done(); + }, + + 'if "vpc" is not specified, vpc with default configuration will be created'(test: Test) { + // GIVEN + const { stack } = testFixtureNoVpc(); + + // WHEN + new eks.LegacyCluster(stack, 'cluster', { version: CLUSTER_VERSION }) ; + + // THEN + expect(stack).to(haveResource('AWS::EC2::VPC')); + test.done(); + }, + + 'default capacity': { + + 'x2 m5.large by default'(test: Test) { + // GIVEN + const { stack } = testFixtureNoVpc(); + + // WHEN + const cluster = new eks.LegacyCluster(stack, 'cluster', { version: CLUSTER_VERSION }); + + // THEN + test.ok(cluster.defaultNodegroup); + expect(stack).to(haveResource('AWS::EKS::Nodegroup', { + InstanceTypes: [ + 'm5.large', + ], + ScalingConfig: { + DesiredSize: 2, + MaxSize: 2, + MinSize: 2, + }, + })); + test.done(); + }, + + 'quantity and type can be customized'(test: Test) { + // GIVEN + const { stack } = testFixtureNoVpc(); + + // WHEN + const cluster = new eks.LegacyCluster(stack, 'cluster', { + defaultCapacity: 10, + defaultCapacityInstance: new ec2.InstanceType('m2.xlarge'), + version: CLUSTER_VERSION, + }); + + // THEN + test.ok(cluster.defaultNodegroup); + expect(stack).to(haveResource('AWS::EKS::Nodegroup', { + ScalingConfig: { + DesiredSize: 10, + MaxSize: 10, + MinSize: 10, + }, + })); + // expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm2.xlarge' })); + test.done(); + }, + + 'defaultCapacity=0 will not allocate at all'(test: Test) { + // GIVEN + const { stack } = testFixtureNoVpc(); + + // WHEN + const cluster = new eks.LegacyCluster(stack, 'cluster', { defaultCapacity: 0, version: CLUSTER_VERSION }); + + // THEN + test.ok(!cluster.defaultCapacity); + expect(stack).notTo(haveResource('AWS::AutoScaling::AutoScalingGroup')); + expect(stack).notTo(haveResource('AWS::AutoScaling::LaunchConfiguration')); + test.done(); + }, + }, + + 'creating a cluster tags the private VPC subnets'(test: Test) { + // GIVEN + const { stack, vpc } = testFixture(); + + // WHEN + new eks.LegacyCluster(stack, 'Cluster', { vpc, defaultCapacity: 0, version: CLUSTER_VERSION }); + + // THEN + expect(stack).to(haveResource('AWS::EC2::Subnet', { + Tags: [ + { Key: 'aws-cdk:subnet-name', Value: 'Private' }, + { Key: 'aws-cdk:subnet-type', Value: 'Private' }, + { Key: 'kubernetes.io/role/internal-elb', Value: '1' }, + { Key: 'Name', Value: 'Stack/VPC/PrivateSubnet1' }, + ], + })); + + test.done(); + }, + + 'creating a cluster tags the public VPC subnets'(test: Test) { + // GIVEN + const { stack, vpc } = testFixture(); + + // WHEN + new eks.LegacyCluster(stack, 'Cluster', { vpc, defaultCapacity: 0, version: CLUSTER_VERSION }); + + // THEN + expect(stack).to(haveResource('AWS::EC2::Subnet', { + MapPublicIpOnLaunch: true, + Tags: [ + { Key: 'aws-cdk:subnet-name', Value: 'Public' }, + { Key: 'aws-cdk:subnet-type', Value: 'Public' }, + { Key: 'kubernetes.io/role/elb', Value: '1' }, + { Key: 'Name', Value: 'Stack/VPC/PublicSubnet1' }, + ], + })); + + test.done(); + }, + + 'adding capacity creates an ASG with tags'(test: Test) { + // GIVEN + const { stack, vpc } = testFixture(); + const cluster = new eks.LegacyCluster(stack, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + }); + + // WHEN + cluster.addCapacity('Default', { + instanceType: new ec2.InstanceType('t2.medium'), + }); + + // THEN + expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + Tags: [ + { + Key: { 'Fn::Join': ['', ['kubernetes.io/cluster/', { Ref: 'ClusterEB0386A7' }]] }, + PropagateAtLaunch: true, + Value: 'owned', + }, + { + Key: 'Name', + PropagateAtLaunch: true, + Value: 'Stack/Cluster/Default', + }, + ], + })); + + test.done(); + }, + + 'create nodegroup with existing role'(test: Test) { + // GIVEN + const { stack } = testFixtureNoVpc(); + + // WHEN + const cluster = new eks.LegacyCluster(stack, 'cluster', { + defaultCapacity: 10, + defaultCapacityInstance: new ec2.InstanceType('m2.xlarge'), + version: CLUSTER_VERSION, + }); + + const existingRole = new iam.Role(stack, 'ExistingRole', { + assumedBy: new iam.AccountRootPrincipal(), + }); + + new eks.Nodegroup(stack, 'Nodegroup', { + cluster, + nodeRole: existingRole, + }); + + // THEN + test.ok(cluster.defaultNodegroup); + expect(stack).to(haveResource('AWS::EKS::Nodegroup', { + ScalingConfig: { + DesiredSize: 10, + MaxSize: 10, + MinSize: 10, + }, + })); + test.done(); + }, + + 'adding bottlerocket capacity creates an ASG with tags'(test: Test) { + // GIVEN + const { stack, vpc } = testFixture(); + const cluster = new eks.LegacyCluster(stack, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + }); + + // WHEN + cluster.addCapacity('Bottlerocket', { + instanceType: new ec2.InstanceType('t2.medium'), + machineImageType: eks.MachineImageType.BOTTLEROCKET, + }); + + // THEN + expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + Tags: [ + { + Key: { 'Fn::Join': ['', ['kubernetes.io/cluster/', { Ref: 'ClusterEB0386A7' }]] }, + PropagateAtLaunch: true, + Value: 'owned', + }, + { + Key: 'Name', + PropagateAtLaunch: true, + Value: 'Stack/Cluster/Bottlerocket', + }, + ], + })); + test.done(); + }, + + 'adding bottlerocket capacity with bootstrapOptions throws error'(test: Test) { + // GIVEN + const { stack, vpc } = testFixture(); + const cluster = new eks.LegacyCluster(stack, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + }); + + test.throws(() => cluster.addCapacity('Bottlerocket', { + instanceType: new ec2.InstanceType('t2.medium'), + machineImageType: eks.MachineImageType.BOTTLEROCKET, + bootstrapOptions: {}, + }), /bootstrapOptions is not supported for Bottlerocket/); + test.done(); + }, + + 'exercise export/import'(test: Test) { + // GIVEN + const { stack: stack1, vpc, app } = testFixture(); + const stack2 = new cdk.Stack(app, 'stack2', { env: { region: 'us-east-1' } }); + const cluster = new eks.LegacyCluster(stack1, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + }); + + // WHEN + const imported = eks.LegacyCluster.fromClusterAttributes(stack2, 'Imported', { + clusterArn: cluster.clusterArn, + vpc: cluster.vpc, + clusterEndpoint: cluster.clusterEndpoint, + clusterName: cluster.clusterName, + securityGroups: cluster.connections.securityGroups, + clusterCertificateAuthorityData: cluster.clusterCertificateAuthorityData, + clusterSecurityGroupId: cluster.clusterSecurityGroupId, + clusterEncryptionConfigKeyArn: cluster.clusterEncryptionConfigKeyArn, + }); + + // this should cause an export/import + new cdk.CfnOutput(stack2, 'ClusterARN', { value: imported.clusterArn }); + + // THEN + expect(stack2).toMatch({ + Outputs: { + ClusterARN: { + Value: { + 'Fn::ImportValue': 'Stack:ExportsOutputFnGetAttClusterEB0386A7Arn2F2E3C3F', + }, + }, + }, + }); + test.done(); + }, + + 'disabled features when kubectl is disabled'(test: Test) { + // GIVEN + const { stack, vpc } = testFixture(); + const cluster = new eks.LegacyCluster(stack, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + }); + + test.throws(() => cluster.addCapacity('boo', { instanceType: new ec2.InstanceType('r5d.24xlarge'), mapRole: true }), + /Cannot map instance IAM role to RBAC if kubectl is disabled for the cluster/); + test.done(); + }, + + 'addCapacity will *not* map the IAM role if mapRole is false'(test: Test) { + // GIVEN + const { stack, vpc } = testFixture(); + const cluster = new eks.LegacyCluster(stack, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + }); + + // WHEN + cluster.addCapacity('default', { + instanceType: new ec2.InstanceType('t2.nano'), + mapRole: false, + }); + + // THEN + expect(stack).to(not(haveResource(eks.KubernetesResource.RESOURCE_TYPE))); + test.done(); + }, + + 'addCapacity will *not* map the IAM role if kubectl is disabled'(test: Test) { + // GIVEN + const { stack, vpc } = testFixture(); + const cluster = new eks.LegacyCluster(stack, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + }); + + // WHEN + cluster.addCapacity('default', { + instanceType: new ec2.InstanceType('t2.nano'), + }); + + // THEN + expect(stack).to(not(haveResource(eks.KubernetesResource.RESOURCE_TYPE))); + test.done(); + }, + + 'outputs': { + 'aws eks update-kubeconfig is the only output synthesized by default'(test: Test) { + // GIVEN + const { app, stack } = testFixtureNoVpc(); + + // WHEN + new eks.LegacyCluster(stack, 'Cluster', { version: CLUSTER_VERSION }); + + // THEN + const assembly = app.synth(); + const template = assembly.getStackByName(stack.stackName).template; + test.deepEqual(template.Outputs, { + ClusterConfigCommand43AAE40F: { Value: { 'Fn::Join': ['', ['aws eks update-kubeconfig --name ', { Ref: 'ClusterEB0386A7' }, ' --region us-east-1']] } }, + ClusterGetTokenCommand06AE992E: { Value: { 'Fn::Join': ['', ['aws eks get-token --cluster-name ', { Ref: 'ClusterEB0386A7' }, ' --region us-east-1']] } }, + }); + test.done(); + }, + + 'if `outputConfigCommand=false` will disabled the output'(test: Test) { + // GIVEN + const { app, stack } = testFixtureNoVpc(); + + // WHEN + new eks.LegacyCluster(stack, 'Cluster', { + outputConfigCommand: false, + version: CLUSTER_VERSION, + }); + + // THEN + const assembly = app.synth(); + const template = assembly.getStackByName(stack.stackName).template; + test.ok(!template.Outputs); // no outputs + test.done(); + }, + + '`outputClusterName` can be used to synthesize an output with the cluster name'(test: Test) { + // GIVEN + const { app, stack } = testFixtureNoVpc(); + + // WHEN + new eks.LegacyCluster(stack, 'Cluster', { + outputConfigCommand: false, + outputClusterName: true, + version: CLUSTER_VERSION, + }); + + // THEN + const assembly = app.synth(); + const template = assembly.getStackByName(stack.stackName).template; + test.deepEqual(template.Outputs, { + ClusterClusterNameEB26049E: { Value: { Ref: 'ClusterEB0386A7' } }, + }); + test.done(); + }, + + 'boostrap user-data': { + + 'rendered by default for ASGs'(test: Test) { + // GIVEN + const { app, stack } = testFixtureNoVpc(); + const cluster = new eks.LegacyCluster(stack, 'Cluster', { defaultCapacity: 0, version: CLUSTER_VERSION }); + + // WHEN + cluster.addCapacity('MyCapcity', { instanceType: new ec2.InstanceType('m3.xlargs') }); + + // THEN + const template = app.synth().getStackByName(stack.stackName).template; + const userData = template.Resources.ClusterMyCapcityLaunchConfig58583345.Properties.UserData; + test.deepEqual(userData, { 'Fn::Base64': { 'Fn::Join': ['', ['#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', { Ref: 'ClusterEB0386A7' }, ' --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true\n/opt/aws/bin/cfn-signal --exit-code $? --stack Stack --resource ClusterMyCapcityASGD4CD8B97 --region us-east-1']] } }); + test.done(); + }, + + 'not rendered if bootstrap is disabled'(test: Test) { + // GIVEN + const { app, stack } = testFixtureNoVpc(); + const cluster = new eks.LegacyCluster(stack, 'Cluster', { defaultCapacity: 0, version: CLUSTER_VERSION }); + + // WHEN + cluster.addCapacity('MyCapcity', { + instanceType: new ec2.InstanceType('m3.xlargs'), + bootstrapEnabled: false, + }); + + // THEN + const template = app.synth().getStackByName(stack.stackName).template; + const userData = template.Resources.ClusterMyCapcityLaunchConfig58583345.Properties.UserData; + test.deepEqual(userData, { 'Fn::Base64': '#!/bin/bash' }); + test.done(); + }, + + // cursory test for options: see test.user-data.ts for full suite + 'bootstrap options'(test: Test) { + // GIVEN + const { app, stack } = testFixtureNoVpc(); + const cluster = new eks.LegacyCluster(stack, 'Cluster', { defaultCapacity: 0, version: CLUSTER_VERSION }); + + // WHEN + cluster.addCapacity('MyCapcity', { + instanceType: new ec2.InstanceType('m3.xlargs'), + bootstrapOptions: { + kubeletExtraArgs: '--node-labels FOO=42', + }, + }); + + // THEN + const template = app.synth().getStackByName(stack.stackName).template; + const userData = template.Resources.ClusterMyCapcityLaunchConfig58583345.Properties.UserData; + test.deepEqual(userData, { 'Fn::Base64': { 'Fn::Join': ['', ['#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', { Ref: 'ClusterEB0386A7' }, ' --kubelet-extra-args "--node-labels lifecycle=OnDemand --node-labels FOO=42" --use-max-pods true\n/opt/aws/bin/cfn-signal --exit-code $? --stack Stack --resource ClusterMyCapcityASGD4CD8B97 --region us-east-1']] } }); + test.done(); + }, + + 'spot instances': { + + 'nodes labeled an tainted accordingly'(test: Test) { + // GIVEN + const { app, stack } = testFixtureNoVpc(); + const cluster = new eks.LegacyCluster(stack, 'Cluster', { defaultCapacity: 0, version: CLUSTER_VERSION }); + + // WHEN + cluster.addCapacity('MyCapcity', { + instanceType: new ec2.InstanceType('m3.xlargs'), + spotPrice: '0.01', + }); + + // THEN + const template = app.synth().getStackByName(stack.stackName).template; + const userData = template.Resources.ClusterMyCapcityLaunchConfig58583345.Properties.UserData; + test.deepEqual(userData, { 'Fn::Base64': { 'Fn::Join': ['', ['#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', { Ref: 'ClusterEB0386A7' }, ' --kubelet-extra-args "--node-labels lifecycle=Ec2Spot --register-with-taints=spotInstance=true:PreferNoSchedule" --use-max-pods true\n/opt/aws/bin/cfn-signal --exit-code $? --stack Stack --resource ClusterMyCapcityASGD4CD8B97 --region us-east-1']] } }); + test.done(); + }, + + 'if kubectl is disabled, interrupt handler is not added'(test: Test) { + // GIVEN + const { stack } = testFixtureNoVpc(); + const cluster = new eks.LegacyCluster(stack, 'Cluster', { + defaultCapacity: 0, + version: CLUSTER_VERSION, + }); + + // WHEN + cluster.addCapacity('MyCapcity', { + instanceType: new ec2.InstanceType('m3.xlargs'), + spotPrice: '0.01', + }); + + // THEN + expect(stack).notTo(haveResource(eks.KubernetesResource.RESOURCE_TYPE)); + test.done(); + }, + + }, + + }, + + 'if bootstrap is disabled cannot specify options'(test: Test) { + // GIVEN + const { stack } = testFixtureNoVpc(); + const cluster = new eks.LegacyCluster(stack, 'Cluster', { defaultCapacity: 0, version: CLUSTER_VERSION }); + + // THEN + test.throws(() => cluster.addCapacity('MyCapcity', { + instanceType: new ec2.InstanceType('m3.xlargs'), + bootstrapEnabled: false, + bootstrapOptions: { awsApiRetryAttempts: 10 }, + }), /Cannot specify "bootstrapOptions" if "bootstrapEnabled" is false/); + test.done(); + }, + + 'EksOptimizedImage() with no nodeType always uses STANDARD with LATEST_KUBERNETES_VERSION'(test: Test) { + // GIVEN + const { app, stack } = testFixtureNoVpc(); + const LATEST_KUBERNETES_VERSION = '1.14'; + + // WHEN + new eks.EksOptimizedImage().getImage(stack); + + // THEN + const assembly = app.synth(); + const parameters = assembly.getStackByName(stack.stackName).template.Parameters; + test.ok(Object.entries(parameters).some( + ([k, v]) => k.startsWith('SsmParameterValueawsserviceeksoptimizedami') && + (v as any).Default.includes('/amazon-linux-2/'), + ), 'EKS STANDARD AMI should be in ssm parameters'); + test.ok(Object.entries(parameters).some( + ([k, v]) => k.startsWith('SsmParameterValueawsserviceeksoptimizedami') && + (v as any).Default.includes(LATEST_KUBERNETES_VERSION), + ), 'LATEST_KUBERNETES_VERSION should be in ssm parameters'); + test.done(); + }, + + 'EksOptimizedImage() with specific kubernetesVersion return correct AMI'(test: Test) { + // GIVEN + const { app, stack } = testFixtureNoVpc(); + + // WHEN + new eks.EksOptimizedImage({ kubernetesVersion: '1.15' }).getImage(stack); + + // THEN + const assembly = app.synth(); + const parameters = assembly.getStackByName(stack.stackName).template.Parameters; + test.ok(Object.entries(parameters).some( + ([k, v]) => k.startsWith('SsmParameterValueawsserviceeksoptimizedami') && + (v as any).Default.includes('/amazon-linux-2/'), + ), 'EKS STANDARD AMI should be in ssm parameters'); + test.ok(Object.entries(parameters).some( + ([k, v]) => k.startsWith('SsmParameterValueawsserviceeksoptimizedami') && + (v as any).Default.includes('/1.15/'), + ), 'kubernetesVersion should be in ssm parameters'); + test.done(); + }, + + 'EKS-Optimized AMI with GPU support when addCapacity'(test: Test) { + // GIVEN + const { app, stack } = testFixtureNoVpc(); + + // WHEN + new eks.LegacyCluster(stack, 'cluster', { + defaultCapacity: 0, + version: CLUSTER_VERSION, + }).addCapacity('GPUCapacity', { + instanceType: new ec2.InstanceType('g4dn.xlarge'), + }); + + // THEN + const assembly = app.synth(); + const parameters = assembly.getStackByName(stack.stackName).template.Parameters; + test.ok(Object.entries(parameters).some( + ([k, v]) => k.startsWith('SsmParameterValueawsserviceeksoptimizedami') && (v as any).Default.includes('amazon-linux-2-gpu'), + ), 'EKS AMI with GPU should be in ssm parameters'); + test.done(); + }, + }, +}; diff --git a/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts b/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts index 16c81f6567cbc..4a0b4dbbee697 100644 --- a/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts +++ b/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts @@ -73,6 +73,20 @@ export = { '', [ '[{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"aws-auth","namespace":"kube-system"},"data":{"mapRoles":"[{\\"rolearn\\":\\"', + { + 'Fn::GetAtt': [ + 'ClusterMastersRole9AA35625', + 'Arn', + ], + }, + '\\",\\"username\\":\\"', + { + 'Fn::GetAtt': [ + 'ClusterMastersRole9AA35625', + 'Arn', + ], + }, + '\\",\\"groups\\":[\\"system:masters\\"]},{\\"rolearn\\":\\"', { 'Fn::GetAtt': [ 'NodegroupNodeGroupRole038A128B', @@ -155,9 +169,8 @@ export = { const { stack, vpc } = testFixture(); // WHEN - const cluster = new eks.Cluster(stack, 'Cluster', { + const cluster = new eks.LegacyCluster(stack, 'Cluster', { vpc, - kubectlEnabled: false, defaultCapacity: 2, version: CLUSTER_VERSION, }); @@ -225,7 +238,6 @@ export = { const stack2 = new cdk.Stack(app, 'stack2', { env: { region: 'us-east-1' } }); const cluster = new eks.Cluster(stack1, 'Cluster', { vpc, - kubectlEnabled: false, defaultCapacity: 0, version: CLUSTER_VERSION, }); diff --git a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-ec2-task.lit.expected.json b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-ec2-task.lit.expected.json index d6af0f21c794a..5fb1bb925e0bf 100644 --- a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-ec2-task.lit.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-ec2-task.lit.expected.json @@ -261,13 +261,39 @@ "Statement": [ { "Action": [ - "ecs:CreateCluster", "ecs:DeregisterContainerInstance", - "ecs:DiscoverPollEndpoint", - "ecs:Poll", "ecs:RegisterContainerInstance", - "ecs:StartTelemetrySession", - "ecs:Submit*", + "ecs:Submit*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:Poll", + "ecs:StartTelemetrySession" + ], + "Effect": "Allow", + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } + }, + { + "Action": [ + "ecs:DiscoverPollEndpoint", "ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents" @@ -449,7 +475,17 @@ "ecs:DescribeTasks" ], "Effect": "Allow", - "Resource": "*" + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + } }, { "Action": [ diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index 2b669e92491d9..e6ae493b60ab7 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -578,42 +578,6 @@ export = { test.done(); }, - 'requires that the source stack be part of an App'(test: Test) { - const app = new cdk.App(); - - const sourceAccount = '123456789012'; - const sourceStack = new cdk.Stack(undefined, 'SourceStack', { env: { account: sourceAccount, region: 'us-west-2' } }); - const rule = new Rule(sourceStack, 'Rule'); - - const targetAccount = '234567890123'; - const targetStack = new cdk.Stack(app, 'TargetStack', { env: { account: targetAccount, region: 'us-west-2' } }); - const resource = new cdk.Construct(targetStack, 'Resource'); - - test.throws(() => { - rule.addTarget(new SomeTarget('T', resource)); - }, /Event stack which uses cross-account targets must be part of a CDK app/); - - test.done(); - }, - - 'requires that the target stack be part of an App'(test: Test) { - const app = new cdk.App(); - - const sourceAccount = '123456789012'; - const sourceStack = new cdk.Stack(app, 'SourceStack', { env: { account: sourceAccount, region: 'us-west-2' } }); - const rule = new Rule(sourceStack, 'Rule'); - - const targetAccount = '234567890123'; - const targetStack = new cdk.Stack(undefined, 'TargetStack', { env: { account: targetAccount, region: 'us-west-2' } }); - const resource = new cdk.Construct(targetStack, 'Resource'); - - test.throws(() => { - rule.addTarget(new SomeTarget('T', resource)); - }, /Target stack which uses cross-account event targets must be part of a CDK app/); - - test.done(); - }, - 'requires that the source and target stacks be part of the same App'(test: Test) { const sourceApp = new cdk.App(); const sourceAccount = '123456789012'; diff --git a/packages/@aws-cdk/aws-glue/test/table.test.ts b/packages/@aws-cdk/aws-glue/test/table.test.ts index 592fadf650165..a35d48e3d93dd 100644 --- a/packages/@aws-cdk/aws-glue/test/table.test.ts +++ b/packages/@aws-cdk/aws-glue/test/table.test.ts @@ -385,7 +385,7 @@ test('encrypted table: SSE-KMS (implicitly created key)', () => { ], Version: '2012-10-17', }, - Description: 'Created by Table/Bucket', + Description: 'Created by Default/Table/Bucket', })); cdkExpect(stack).to(haveResource('AWS::S3::Bucket', { diff --git a/packages/@aws-cdk/aws-iam/lib/policy.ts b/packages/@aws-cdk/aws-iam/lib/policy.ts index 6f852caec1a23..792f0f5861186 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy.ts @@ -124,7 +124,20 @@ export class Policy extends Resource implements IPolicy { Lazy.stringValue({ produce: () => generatePolicyName(scope, resource.logicalId) }), }); - const resource = new CfnPolicy(this, 'Resource', { + const self = this; + + class CfnPolicyConditional extends CfnPolicy { + /** + * This function returns `true` if the CFN resource should be included in + * the cloudformation template unless `force` is `true`, if the policy + * document is empty, the resource will not be included. + */ + protected shouldSynthesize() { + return self.force || self.referenceTaken || (!self.document.isEmpty && self.isAttached); + } + } + + const resource = new CfnPolicyConditional(this, 'Resource', { policyDocument: this.document, policyName: this.physicalName, roles: undefinedIfEmpty(() => this.roles.map(r => r.roleName)), @@ -222,14 +235,6 @@ export class Policy extends Resource implements IPolicy { return result; } - protected prepare() { - // Remove the resource if it shouldn't exist. This will prevent it from being rendered to the template. - const shouldExist = this.force || this.referenceTaken || (!this.document.isEmpty && this.isAttached); - if (!shouldExist) { - this.node.tryRemoveChild('Resource'); - } - } - /** * Whether the policy resource has been attached to any identity */ diff --git a/packages/@aws-cdk/aws-kinesis/test/stream.test.ts b/packages/@aws-cdk/aws-kinesis/test/stream.test.ts index 30403dd5d6ef5..c4b5556ce3c99 100644 --- a/packages/@aws-cdk/aws-kinesis/test/stream.test.ts +++ b/packages/@aws-cdk/aws-kinesis/test/stream.test.ts @@ -338,7 +338,7 @@ describe('Kinesis data streams', () => { MyStreamKey76F3300E: { Type: 'AWS::KMS::Key', Properties: { - Description: 'Created by MyStream', + Description: 'Created by Default/MyStream', KeyPolicy: { Statement: [ { @@ -555,7 +555,7 @@ describe('Kinesis data streams', () => { ], Version: '2012-10-17', }, - Description: 'Created by MyStream', + Description: 'Created by Default/MyStream', }, UpdateReplacePolicy: 'Retain', DeletionPolicy: 'Retain', @@ -684,7 +684,7 @@ describe('Kinesis data streams', () => { ], Version: '2012-10-17', }, - Description: 'Created by MyStream', + Description: 'Created by Default/MyStream', }, UpdateReplacePolicy: 'Retain', DeletionPolicy: 'Retain', @@ -753,7 +753,7 @@ describe('Kinesis data streams', () => { MyStreamKey76F3300E: { Type: 'AWS::KMS::Key', Properties: { - Description: 'Created by MyStream', + Description: 'Created by Default/MyStream', KeyPolicy: { Statement: [ { diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/test.dynamo.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/test.dynamo.ts index 745b016109d30..cc5a09e4ec1c8 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/test.dynamo.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/test.dynamo.ts @@ -127,7 +127,7 @@ export = { test.throws(() => fn.addEventSource(new sources.DynamoEventSource(table, { batchSize: 50, startingPosition: lambda.StartingPosition.LATEST, - })), /DynamoDB Streams must be enabled on the table T/); + })), /DynamoDB Streams must be enabled on the table Default\/T/); test.done(); }, diff --git a/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts b/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts index 22cec7c841bde..53ea90723a32b 100644 --- a/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts +++ b/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts @@ -28,7 +28,7 @@ export function bundle(options: BundlingOptions): lambda.AssetCode { let depsCommand = chain([ hasRequirements ? `${installer} install -r requirements.txt -t ${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}` : '', - `rsync -r . ${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}`, + `cp -au . ${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}`, ]); return lambda.Code.fromAsset(options.entry, { 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 492f7b3dbd890..fc03c7ae9bba8 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts @@ -21,7 +21,7 @@ test('Bundling', () => { bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'rsync -r . /asset-output', + 'cp -au . /asset-output', ], }), }); @@ -48,7 +48,7 @@ test('Bundling with requirements.txt installed', () => { bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'pip3 install -r requirements.txt -t /asset-output && rsync -r . /asset-output', + 'pip3 install -r requirements.txt -t /asset-output && cp -au . /asset-output', ], }), }); @@ -72,7 +72,7 @@ test('Bundling Python 2.7 with requirements.txt installed', () => { bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'pip install -r requirements.txt -t /asset-output && rsync -r . /asset-output', + 'pip install -r requirements.txt -t /asset-output && cp -au . /asset-output', ], }), }); diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json new file mode 100644 index 0000000000000..56d8661ab73d8 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json @@ -0,0 +1,113 @@ +{ + "Resources": { + "myhandlerServiceRole77891068": { + "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" + ] + ] + } + ] + } + }, + "myhandlerD202FA8E": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeS3Bucket0552B5BB" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeS3VersionKey9522AC10" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeS3VersionKey9522AC10" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "myhandlerServiceRole77891068", + "Arn" + ] + }, + "Runtime": "python3.8" + }, + "DependsOn": [ + "myhandlerServiceRole77891068" + ] + } + }, + "Parameters": { + "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeS3Bucket0552B5BB": { + "Type": "String", + "Description": "S3 bucket for asset \"822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1ade\"" + }, + "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeS3VersionKey9522AC10": { + "Type": "String", + "Description": "S3 key for asset version \"822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1ade\"" + }, + "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeArtifactHash3CE06D09": { + "Type": "String", + "Description": "Artifact hash for asset \"822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1ade\"" + } + }, + "Outputs": { + "FunctionArn": { + "Value": { + "Fn::GetAtt": [ + "myhandlerD202FA8E", + "Arn" + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.ts b/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.ts new file mode 100644 index 0000000000000..0e6546d2ecbbb --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.ts @@ -0,0 +1,28 @@ +import * as path from 'path'; +import { Runtime } from '@aws-cdk/aws-lambda'; +import { App, CfnOutput, Construct, Stack, StackProps } from '@aws-cdk/core'; +import * as lambda from '../lib'; + +/* + * Stack verification steps: + * * aws lambda invoke --function-name --invocation-type Event --payload '"OK"' response.json + */ + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const fn = new lambda.PythonFunction(this, 'my_handler', { + entry: path.join(__dirname, 'lambda-handler'), + runtime: Runtime.PYTHON_3_8, + }); + + new CfnOutput(this, 'FunctionArn', { + value: fn.functionArn, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-lambda-python-38'); +app.synth(); diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index dcccaeececaf5..a08b04b31bb8c 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -302,7 +302,7 @@ from lambda function, the Amazon EFS access point will be required. The following sample allows the lambda function to mount the Amazon EFS access point to `/mnt/msg` in the runtime environment and access the filesystem with the POSIX identity defined in `posixUser`. ```ts -// create a new Amaozn EFS filesystem +// create a new Amazon EFS filesystem const fileSystem = new efs.FileSystem(stack, 'Efs', { vpc }); // create a new access point from the filesystem diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index d450e85ca913a..92b6d64774751 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -332,6 +332,18 @@ export class Function extends FunctionBase { ...this.currentVersionOptions, }); + // override the version's logical ID with a lazy string which includes the + // hash of the function itself, so a new version resource is created when + // the function configuration changes. + const cfn = this._currentVersion.node.defaultChild as CfnResource; + const originalLogicalId = this.stack.resolve(cfn.logicalId) as string; + + cfn.overrideLogicalId(Lazy.stringValue({ produce: _ => { + const hash = calculateFunctionHash(this); + const logicalId = trimFromStart(originalLogicalId, 255 - 32); + return `${logicalId}${hash}`; + }})); + return this._currentVersion; } @@ -739,23 +751,6 @@ export class Function extends FunctionBase { return this._logGroup; } - protected prepare() { - super.prepare(); - - // if we have a current version resource, override it's logical id - // so that it includes the hash of the function code and it's configuration. - if (this._currentVersion) { - const stack = Stack.of(this); - const cfn = this._currentVersion.node.defaultChild as CfnResource; - const originalLogicalId: string = stack.resolve(cfn.logicalId); - - const hash = calculateFunctionHash(this); - - const logicalId = trimFromStart(originalLogicalId, 255 - 32); - cfn.overrideLogicalId(`${logicalId}${hash}`); - } - } - private renderEnvironment() { if (!this.environment || Object.keys(this.environment).length === 0) { return undefined; diff --git a/packages/@aws-cdk/aws-lambda/lib/runtime.ts b/packages/@aws-cdk/aws-lambda/lib/runtime.ts index 207d8071d996e..c8da723f06e82 100644 --- a/packages/@aws-cdk/aws-lambda/lib/runtime.ts +++ b/packages/@aws-cdk/aws-lambda/lib/runtime.ts @@ -6,6 +6,12 @@ export interface LambdaRuntimeProps { * @default false */ readonly supportsInlineCode?: boolean; + + /** + * The Docker image name to be used for bundling in this runtime. + * @default - the latest docker image "amazon/aws-sam-cli-build-image-" from https://hub.docker.com/u/amazon + */ + readonly bundlingDockerImage?: string; } export enum RuntimeFamily { @@ -113,17 +119,23 @@ export class Runtime { /** * The .NET Core 2.1 runtime (dotnetcore2.1) */ - public static readonly DOTNET_CORE_2_1 = new Runtime('dotnetcore2.1', RuntimeFamily.DOTNET_CORE); + public static readonly DOTNET_CORE_2_1 = new Runtime('dotnetcore2.1', RuntimeFamily.DOTNET_CORE, { + bundlingDockerImage: 'lambci/lambda:build-dotnetcore2.1', + }); /** * The .NET Core 3.1 runtime (dotnetcore3.1) */ - public static readonly DOTNET_CORE_3_1 = new Runtime('dotnetcore3.1', RuntimeFamily.DOTNET_CORE); + public static readonly DOTNET_CORE_3_1 = new Runtime('dotnetcore3.1', RuntimeFamily.DOTNET_CORE, { + bundlingDockerImage: 'lambci/lambda:build-dotnetcore3.1', + }); /** * The Go 1.x runtime (go1.x) */ - public static readonly GO_1_X = new Runtime('go1.x', RuntimeFamily.GO); + public static readonly GO_1_X = new Runtime('go1.x', RuntimeFamily.GO, { + bundlingDockerImage: 'lambci/lambda:build-go1.x', + }); /** * The Ruby 2.5 runtime (ruby2.5) @@ -158,9 +170,6 @@ export class Runtime { /** * The bundling Docker image for this runtime. - * Points to the AWS SAM build image for this runtime. - * - * @see https://github.com/awslabs/aws-sam-cli */ public readonly bundlingDockerImage: BundlingDockerImage; @@ -168,7 +177,8 @@ export class Runtime { this.name = name; this.supportsInlineCode = !!props.supportsInlineCode; this.family = family; - this.bundlingDockerImage = BundlingDockerImage.fromRegistry(`amazon/aws-sam-cli-build-image-${name}`); + const imageName = props.bundlingDockerImage ?? `amazon/aws-sam-cli-build-image-${name}`; + this.bundlingDockerImage = BundlingDockerImage.fromRegistry(imageName); Runtime.ALL.push(this); } diff --git a/packages/@aws-cdk/aws-lambda/test/integ.current-version.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.current-version.expected.json index 1a3ea891ebc6d..bf269c8f6d555 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.current-version.expected.json +++ b/packages/@aws-cdk/aws-lambda/test/integ.current-version.expected.json @@ -85,7 +85,7 @@ "MyLambdaServiceRole4539ECB6" ] }, - "MyLambdaCurrentVersionE7A382CC86b18af374d6e380aa07074d2490e2df": { + "MyLambdaCurrentVersionE7A382CC721de083c6b4b6360a9c534b79eb610e": { "Type": "AWS::Lambda::Version", "Properties": { "FunctionName": { @@ -103,7 +103,7 @@ }, "Qualifier": { "Fn::GetAtt": [ - "MyLambdaCurrentVersionE7A382CC86b18af374d6e380aa07074d2490e2df", + "MyLambdaCurrentVersionE7A382CC721de083c6b4b6360a9c534b79eb610e", "Version" ] }, @@ -118,7 +118,7 @@ }, "FunctionVersion": { "Fn::GetAtt": [ - "MyLambdaCurrentVersionE7A382CC86b18af374d6e380aa07074d2490e2df", + "MyLambdaCurrentVersionE7A382CC721de083c6b4b6360a9c534b79eb610e", "Version" ] }, @@ -140,4 +140,4 @@ "Description": "Artifact hash for asset \"8811a2632ac5564a08fd269e159298f7e497f259578b0dc5e927a1f48ab24d34\"" } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/test.function.ts b/packages/@aws-cdk/aws-lambda/test/test.function.ts index 52cbb1b77a06b..3e06a63b06e30 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.function.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.function.ts @@ -1,5 +1,5 @@ import * as path from 'path'; -import { expect, haveOutput, haveResource } from '@aws-cdk/assert'; +import { expect, haveResource, SynthUtils } from '@aws-cdk/assert'; import { ProfilingGroup } from '@aws-cdk/aws-codeguruprofiler'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as efs from '@aws-cdk/aws-efs'; @@ -390,18 +390,13 @@ export = testCase({ }); // THEN - expect(stack1).to(haveOutput({ - outputName: 'CurrentVersionArn', - outputValue: { - Ref: 'MyFunctionCurrentVersion197490AF1a9a73cf5c46aec5e40fb202042eb60b', - }, - })); - expect(stack2).to(haveOutput({ - outputName: 'CurrentVersionArn', - outputValue: { - Ref: 'MyFunctionCurrentVersion197490AF8360a045031060e3117269037b7bffd6', - }, - })); + const template1 = SynthUtils.synthesize(stack1).template; + const template2 = SynthUtils.synthesize(stack2).template; + + // these functions are different in their configuration but the original + // logical ID of the version would be the same unless the logical ID + // includes the hash of function's configuration. + test.notDeepEqual(template1.Outputs.CurrentVersionArn.Value, template2.Outputs.CurrentVersionArn.Value); test.done(); }, }, diff --git a/packages/@aws-cdk/aws-lambda/test/test.lambda-version.ts b/packages/@aws-cdk/aws-lambda/test/test.lambda-version.ts index 69e0f62e9c668..b7f283164b715 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.lambda-version.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.lambda-version.ts @@ -134,7 +134,7 @@ export = { }, 'FunctionVersion': { 'Fn::GetAtt': [ - 'FnCurrentVersion17A89ABB19ed45993ff69fd011ae9fd4ab6e2005', + 'FnCurrentVersion17A89ABBab5c765f3c55e4e61583b51b00a95742', 'Version', ], }, diff --git a/packages/@aws-cdk/aws-lambda/test/test.runtime.ts b/packages/@aws-cdk/aws-lambda/test/test.runtime.ts index df5cf230ce15c..67294babdec9d 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.runtime.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.runtime.ts @@ -75,4 +75,21 @@ export = testCase({ test.done(); }, + 'overridde to bundlingDockerImage points to the correct image'(test: Test) { + // GIVEN + const runtime = new lambda.Runtime('my-runtime-name', undefined, { + bundlingDockerImage: 'my-docker-image', + }); + + // THEN + test.equal(runtime.bundlingDockerImage.image, 'my-docker-image'); + + test.done(); + }, + 'dotnetcore and go have overridden images'(test: Test) { + test.equal(lambda.Runtime.DOTNET_CORE_3_1.bundlingDockerImage.image, 'lambci/lambda:build-dotnetcore3.1'); + test.equal(lambda.Runtime.DOTNET_CORE_2_1.bundlingDockerImage.image, 'lambci/lambda:build-dotnetcore2.1'); + test.equal(lambda.Runtime.GO_1_X.bundlingDockerImage.image, 'lambci/lambda:build-go1.x'); + test.done(); + }, }); diff --git a/packages/@aws-cdk/aws-s3-deployment/README.md b/packages/@aws-cdk/aws-s3-deployment/README.md index a90ea32f1ae20..2f4dbe82cb0ba 100644 --- a/packages/@aws-cdk/aws-s3-deployment/README.md +++ b/packages/@aws-cdk/aws-s3-deployment/README.md @@ -91,7 +91,7 @@ new BucketDeployment(this, 'BucketDeployment', { }); new BucketDeployment(this, 'HTMLBucketDeployment', { - sources: [Source.asset('./website', { exclude: ['!index.html'] })], + sources: [Source.asset('./website', { exclude: ['*', '!index.html'] })], destinationBucket: bucket, cacheControl: [CacheControl.fromString('max-age=0,no-cache,no-store,must-revalidate')], prune: false, 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 76563c2b4cccc..5e68f2b786cd4 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,4 +1,5 @@ // import { SynthUtils } from '@aws-cdk/assert'; +import { ResourcePart } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; @@ -84,13 +85,11 @@ test('permissions are added as a dependency to the notifications resource when u const lambdaDestination = new s3n.LambdaDestination(fn); - bucket.addEventNotification(s3.EventType.OBJECT_CREATED, lambdaDestination, { prefix: 'v1/'}); - - const notifications = stack.node.findAll().filter(c => c.node.id === 'Notifications')[0]; - const dependencies = notifications!.node.dependencies; - - expect(dependencies[0].target.node.id).toEqual('AllowBucketNotificationsFromMyBucket'); + bucket.addEventNotification(s3.EventType.OBJECT_CREATED, lambdaDestination, { prefix: 'v1/' }); + expect(stack).toHaveResource('Custom::S3BucketNotifications', { + DependsOn: [ 'SingletonLambdauuidAllowBucketNotificationsFromMyBucket1464DCBA' ], + }, ResourcePart.CompleteDefinition); }); test('add multiple event notifications using a singleton function', () => { 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 7a5a3cf25b96d..cadf2609692f0 100644 --- a/packages/@aws-cdk/aws-s3-notifications/test/notifications.test.ts +++ b/packages/@aws-cdk/aws-s3-notifications/test/notifications.test.ts @@ -290,8 +290,6 @@ test('a notification destination can specify a set of dependencies that must be bucket.addObjectCreatedNotification(dest); - cdk.ConstructNode.prepare(stack.node); - expect(SynthUtils.synthesize(stack).template.Resources.BucketNotifications8F2E257D).toEqual({ Type: 'Custom::S3BucketNotifications', Properties: { 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 d1e4f1abf1bc9..57dbe8b66bf17 100644 --- a/packages/@aws-cdk/aws-s3-notifications/test/queue.test.ts +++ b/packages/@aws-cdk/aws-s3-notifications/test/queue.test.ts @@ -136,6 +136,6 @@ test('if the queue is encrypted with a custom kms key, the key resource policy i ], Version: '2012-10-17', }, - Description: 'Created by Queue', + Description: 'Created by Default/Queue', }); }); diff --git a/packages/@aws-cdk/aws-s3/README.md b/packages/@aws-cdk/aws-s3/README.md index d136089a22528..01165967f3dbc 100644 --- a/packages/@aws-cdk/aws-s3/README.md +++ b/packages/@aws-cdk/aws-s3/README.md @@ -224,6 +224,54 @@ const bucket = new Bucket(this, 'MyBucket', { [S3 Server access logging]: https://docs.aws.amazon.com/AmazonS3/latest/dev/ServerLogs.html +### S3 Inventory + +An [inventory](https://docs.aws.amazon.com/AmazonS3/latest/dev/storage-inventory.html) contains a list of the objects in the source bucket and metadata for each object. The inventory lists are stored in the destination bucket as a CSV file compressed with GZIP, as an Apache optimized row columnar (ORC) file compressed with ZLIB, or as an Apache Parquet (Parquet) file compressed with Snappy. + +You can configure multiple inventory lists for a bucket. You can configure what object metadata to include in the inventory, whether to list all object versions or only current versions, where to store the inventory list file output, and whether to generate the inventory on a daily or weekly basis. + +```ts +const inventoryBucket = new s3.Bucket(this, 'InventoryBucket'); + +const dataBucket = new s3.Bucket(this, 'DataBucket', { + inventories: [ + { + frequency: s3.InventoryFrequency.DAILY, + includeObjectVersions: s3.InventoryObjectVersion.CURRENT, + destination: { + bucket: inventoryBucket, + }, + }, + { + frequency: s3.InventoryFrequency.WEEKLY, + includeObjectVersions: s3.InventoryObjectVersion.ALL, + destination: { + bucket: inventoryBucket, + prefix: 'with-all-versions', + }, + } + ] +}); +``` + +If the destination bucket is created as part of the same CDK application, the necessary permissions will be automatically added to the bucket policy. +However, if you use an imported bucket (i.e `Bucket.fromXXX()`), you'll have to make sure it contains the following policy document: + +``` +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "InventoryAndAnalyticsExamplePolicy", + "Effect": "Allow", + "Principal": { "Service": "s3.amazonaws.com" }, + "Action": "s3:PutObject", + "Resource": ["arn:aws:s3:::destinationBucket/*"] + } + ] +} +``` + ### Website redirection You can use the two following properties to specify the bucket [redirection policy]. Please note that these methods cannot both be applied to the same bucket. diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 9c49dde1be853..29327a1685bf6 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -1,8 +1,8 @@ +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 { Construct, Fn, IResource, Lazy, RemovalPolicy, Resource, Stack, Token } from '@aws-cdk/core'; -import { EOL } from 'os'; import { BucketPolicy } from './bucket-policy'; import { IBucketNotificationDestination } from './destination'; import { BucketNotifications } from './notifications-resource'; @@ -828,6 +828,130 @@ export interface RedirectTarget { readonly protocol?: RedirectProtocol; } +/** + * All supported inventory list formats. + */ +export enum InventoryFormat { + /** + * Generate the inventory list as CSV. + */ + CSV = 'CSV', + /** + * Generate the inventory list as Parquet. + */ + PARQUET = 'Parquet', + /** + * Generate the inventory list as Parquet. + */ + ORC = 'ORC', +} + +/** + * All supported inventory frequencies. + */ +export enum InventoryFrequency { + /** + * A report is generated every day. + */ + DAILY = 'Daily', + /** + * A report is generated every Sunday (UTC timezone) after the initial report. + */ + WEEKLY = 'Weekly' +} + +/** + * Inventory version support. + */ +export enum InventoryObjectVersion { + /** + * Includes all versions of each object in the report. + */ + ALL = 'All', + /** + * Includes only the current version of each object in the report. + */ + CURRENT = 'Current', +} + +/** + * The destination of the inventory. + */ +export interface InventoryDestination { + /** + * Bucket where all inventories will be saved in. + */ + readonly bucket: IBucket; + /** + * The prefix to be used when saving the inventory. + * + * @default - No prefix. + */ + readonly prefix?: string; + /** + * The account ID that owns the destination S3 bucket. + * If no account ID is provided, the owner is not validated before exporting data. + * It's recommended to set an account ID to prevent problems if the destination bucket ownership changes. + * + * @default - No account ID. + */ + readonly bucketOwner?: string; +} + +/** + * Specifies the inventory configuration of an S3 Bucket. + * + * @see https://docs.aws.amazon.com/AmazonS3/latest/dev/storage-inventory.html + */ +export interface Inventory { + /** + * The destination of the inventory. + */ + readonly destination: InventoryDestination; + /** + * The inventory will only include objects that meet the prefix filter criteria. + * + * @default - No objects prefix + */ + readonly objectsPrefix?: string; + /** + * The format of the inventory. + * + * @default InventoryFormat.CSV + */ + readonly format?: InventoryFormat; + /** + * Whether the inventory is enabled or disabled. + * + * @default true + */ + readonly enabled?: boolean; + /** + * The inventory configuration ID. + * + * @default - generated ID. + */ + readonly inventoryId?: string; + /** + * Frequency at which the inventory should be generated. + * + * @default InventoryFrequency.WEEKLY + */ + readonly frequency?: InventoryFrequency; + /** + * If the inventory should contain all the object versions or only the current one. + * + * @default InventoryObjectVersion.ALL + */ + readonly includeObjectVersions?: InventoryObjectVersion; + /** + * A list of optional fields to be included in the inventory result. + * + * @default - No optional fields. + */ + readonly optionalFields?: string[]; +} + export interface BucketProps { /** * The kind of server-side encryption to apply to this bucket. @@ -966,6 +1090,15 @@ export interface BucketProps { * @default - No log file prefix */ readonly serverAccessLogsPrefix?: string; + + /** + * The inventory configuration of the bucket. + * + * @see https://docs.aws.amazon.com/AmazonS3/latest/dev/storage-inventory.html + * + * @default - No inventory configuration + */ + readonly inventories?: Inventory[]; } /** @@ -1055,6 +1188,7 @@ export class Bucket extends BucketBase { private readonly notifications: BucketNotifications; private readonly metrics: BucketMetrics[] = []; private readonly cors: CorsRule[] = []; + private readonly inventories: Inventory[] = []; constructor(scope: Construct, id: string, props: BucketProps = {}) { super(scope, id, { @@ -1079,6 +1213,7 @@ export class Bucket extends BucketBase { corsConfiguration: Lazy.anyValue({ produce: () => this.parseCorsConfiguration() }), accessControl: Lazy.stringValue({ produce: () => this.accessControl }), loggingConfiguration: this.parseServerAccessLogs(props), + inventoryConfigurations: Lazy.anyValue({ produce: () => this.parseInventoryConfiguration() }), }); resource.applyRemovalPolicy(props.removalPolicy); @@ -1107,6 +1242,10 @@ export class Bucket extends BucketBase { props.serverAccessLogsBucket.allowLogDelivery(); } + for (const inventory of props.inventories ?? []) { + this.addInventory(inventory); + } + // Add all bucket metric configurations rules (props.metrics || []).forEach(this.addMetric.bind(this)); // Add all cors configuration rules @@ -1204,6 +1343,15 @@ export class Bucket extends BucketBase { return this.addEventNotification(EventType.OBJECT_REMOVED, dest, ...filters); } + /** + * Add an inventory configuration. + * + * @param inventory configuration to add + */ + public addInventory(inventory: Inventory): void { + this.inventories.push(inventory); + } + private validateBucketName(physicalName: string): void { const bucketName = physicalName; if (!bucketName || Token.isUnresolved(bucketName)) { @@ -1460,6 +1608,50 @@ export class Bucket extends BucketBase { this.accessControl = BucketAccessControl.LOG_DELIVERY_WRITE; } + + private parseInventoryConfiguration(): CfnBucket.InventoryConfigurationProperty[] | undefined { + if (!this.inventories || this.inventories.length === 0) { + return undefined; + } + + return this.inventories.map((inventory, index) => { + const format = inventory.format ?? InventoryFormat.CSV; + const frequency = inventory.frequency ?? InventoryFrequency.WEEKLY; + const id = inventory.inventoryId ?? `${this.node.id}Inventory${index}`; + + if (inventory.destination.bucket instanceof Bucket) { + inventory.destination.bucket.addToResourcePolicy(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['s3:PutObject'], + resources: [ + inventory.destination.bucket.bucketArn, + inventory.destination.bucket.arnForObjects(`${inventory.destination.prefix ?? ''}*`), + ], + principals: [new iam.ServicePrincipal('s3.amazonaws.com')], + conditions: { + ArnLike: { + 'aws:SourceArn': this.bucketArn, + }, + }, + })); + } + + return { + id, + destination: { + bucketArn: inventory.destination.bucket.bucketArn, + bucketAccountId: inventory.destination.bucketOwner, + prefix: inventory.destination.prefix, + format, + }, + enabled: inventory.enabled ?? true, + includedObjectVersions: inventory.includeObjectVersions ?? InventoryObjectVersion.ALL, + scheduleFrequency: frequency, + optionalFields: inventory.optionalFields, + prefix: inventory.objectsPrefix, + }; + }); + } } /** 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 new file mode 100644 index 0000000000000..f5610756ad71e --- /dev/null +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-inventory.expected.json @@ -0,0 +1,158 @@ +{ + "Resources": { + "InventoryBucketA869B8CB": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "InventoryBucketPolicyEDF94353": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "InventoryBucketA869B8CB" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:PutObject", + "Condition": { + "ArnLike": { + "aws:SourceArn": { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "s3.amazonaws.com" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "InventoryBucketA869B8CB", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "InventoryBucketA869B8CB", + "Arn" + ] + }, + "/reports*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket", + "Properties": { + "InventoryConfigurations": [ + { + "Destination": { + "BucketArn": { + "Fn::GetAtt": [ + "InventoryBucketA869B8CB", + "Arn" + ] + }, + "Format": "Parquet", + "Prefix": "reports" + }, + "Enabled": true, + "Id": "MyBucketInventory0", + "IncludedObjectVersions": "All", + "ScheduleFrequency": "Daily" + }, + { + "Destination": { + "BucketArn": { + "Fn::GetAtt": [ + "SecondBucketAC350874", + "Arn" + ] + }, + "Format": "CSV" + }, + "Enabled": true, + "Id": "MyBucketInventory1", + "IncludedObjectVersions": "All", + "ScheduleFrequency": "Weekly" + } + ] + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "SecondBucketAC350874": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "SecondBucketPolicy844C4343": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "SecondBucketAC350874" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:PutObject", + "Condition": { + "ArnLike": { + "aws:SourceArn": { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "s3.amazonaws.com" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "SecondBucketAC350874", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "SecondBucketAC350874", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + } + } +} diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-inventory.ts b/packages/@aws-cdk/aws-s3/test/integ.bucket-inventory.ts new file mode 100644 index 0000000000000..3bea814804e87 --- /dev/null +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-inventory.ts @@ -0,0 +1,37 @@ +#!/usr/bin/env node +import * as cdk from '@aws-cdk/core'; +import * as s3 from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-s3'); + +const inventoryBucket = new s3.Bucket(stack, 'InventoryBucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, +}); + +const myBucket = new s3.Bucket(stack, 'MyBucket', { + inventories: [ + { + destination: { + bucket: inventoryBucket, + prefix: 'reports', + }, + frequency: s3.InventoryFrequency.DAILY, + format: s3.InventoryFormat.PARQUET, + }, + ], + removalPolicy: cdk.RemovalPolicy.DESTROY, +}); + +const secondInventoryBucket = new s3.Bucket(stack, 'SecondBucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, +}); + +myBucket.addInventory({ + destination: { + bucket: secondInventoryBucket, + }, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-s3/test/test.bucket.ts b/packages/@aws-cdk/aws-s3/test/test.bucket.ts index 3f177974fdbc1..8d78477e073d1 100644 --- a/packages/@aws-cdk/aws-s3/test/test.bucket.ts +++ b/packages/@aws-cdk/aws-s3/test/test.bucket.ts @@ -1,9 +1,9 @@ -import { expect, haveResource, haveResourceLike, SynthUtils } from '@aws-cdk/assert'; +import { EOL } from 'os'; +import { expect, haveResource, haveResourceLike, SynthUtils, arrayWith, objectLike } from '@aws-cdk/assert'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; -import { EOL } from 'os'; import * as s3 from '../lib'; // to make it easy to copy & paste from output: @@ -610,13 +610,20 @@ export = { test.done(); }, - 'import can also be used to import arbitrary ARNs'(test: Test) { + 'import does not create any resources'(test: Test) { const stack = new cdk.Stack(); const bucket = s3.Bucket.fromBucketAttributes(stack, 'ImportedBucket', { bucketArn: 'arn:aws:s3:::my-bucket' }); bucket.addToResourcePolicy(new iam.PolicyStatement({ resources: ['*'], actions: ['*'] })); // at this point we technically didn't create any resources in the consuming stack. expect(stack).toMatch({}); + test.done(); + }, + + 'import can also be used to import arbitrary ARNs'(test: Test) { + const stack = new cdk.Stack(); + const bucket = s3.Bucket.fromBucketAttributes(stack, 'ImportedBucket', { bucketArn: 'arn:aws:s3:::my-bucket' }); + bucket.addToResourcePolicy(new iam.PolicyStatement({ resources: ['*'], actions: ['*'] })); // but now we can reference the bucket // you can even use the bucket name, which will be extracted from the arn provided. @@ -867,7 +874,7 @@ export = { 'MyBucketKeyC17130CF': { 'Type': 'AWS::KMS::Key', 'Properties': { - 'Description': 'Created by MyBucket', + 'Description': 'Created by Default/MyBucket', 'KeyPolicy': { 'Statement': [ { @@ -1971,4 +1978,56 @@ export = { test.done(); }, + + 'Defaults for an inventory bucket'(test: Test) { + // Given + const stack = new cdk.Stack(); + + const inventoryBucket = new s3.Bucket(stack, 'InventoryBucket'); + new s3.Bucket(stack, 'MyBucket', { + inventories: [ + { + destination: { + bucket: inventoryBucket, + }, + }, + ], + }); + + expect(stack).to(haveResourceLike('AWS::S3::Bucket', { + InventoryConfigurations: [ + { + Enabled: true, + IncludedObjectVersions: 'All', + ScheduleFrequency: 'Weekly', + Destination: { + Format: 'CSV', + BucketArn: { 'Fn::GetAtt': ['InventoryBucketA869B8CB', 'Arn'] }, + }, + Id: 'MyBucketInventory0', + }, + ], + })); + + expect(stack).to(haveResourceLike('AWS::S3::BucketPolicy', { + Bucket: { Ref: 'InventoryBucketA869B8CB'}, + PolicyDocument: { + Statement: arrayWith(objectLike({ + Action: 's3:PutObject', + Principal: { Service: 's3.amazonaws.com' }, + Resource: [ + { + 'Fn::GetAtt': ['InventoryBucketA869B8CB', 'Arn'], + }, + { + 'Fn::Join': ['', [{'Fn::GetAtt': ['InventoryBucketA869B8CB', 'Arn']}, '/*']], + }, + ], + })), + }, + })); + + test.done(); + }, + }; diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.secret-rotation.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.secret-rotation.ts index 73eed329f232d..463853ad8af08 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/test.secret-rotation.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/test.secret-rotation.ts @@ -59,7 +59,7 @@ export = { })); expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { - GroupDescription: 'SecretRotation/SecurityGroup', + GroupDescription: 'Default/SecretRotation/SecurityGroup', })); expect(stack).to(haveResource('AWS::Serverless::Application', { 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 77c38a5259061..e60c7001f9850 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts @@ -887,7 +887,7 @@ test('throws with mutliple subscriptions of the same subscriber', () => { topic.addSubscription(new subs.SqsSubscription(queue)); expect(() => topic.addSubscription(new subs.SqsSubscription(queue))) - .toThrowError(/A subscription with id \"MyTopic\" already exists under the scope MyQueue/); + .toThrowError(/A subscription with id \"MyTopic\" already exists under the scope Default\/MyQueue/); }); test('with filter policy', () => { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json index 634506837f8d5..bf007c4e55c32 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json @@ -63,13 +63,39 @@ "Statement": [ { "Action": [ - "ecs:CreateCluster", "ecs:DeregisterContainerInstance", - "ecs:DiscoverPollEndpoint", - "ecs:Poll", "ecs:RegisterContainerInstance", - "ecs:StartTelemetrySession", - "ecs:Submit*", + "ecs:Submit*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Ec2ClusterEE43E89D", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:Poll", + "ecs:StartTelemetrySession" + ], + "Effect": "Allow", + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "Ec2ClusterEE43E89D", + "Arn" + ] + } + } + } + }, + { + "Action": [ + "ecs:DiscoverPollEndpoint", "ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents" @@ -243,7 +269,17 @@ "ecs:DescribeTasks" ], "Effect": "Allow", - "Resource": "*" + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "Ec2ClusterEE43E89D", + "Arn" + ] + } + } + } }, { "Action": [ diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-task.expected.json index 0a57d22dc1fb6..09e4369720880 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-task.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-task.expected.json @@ -63,13 +63,39 @@ "Statement": [ { "Action": [ - "ecs:CreateCluster", "ecs:DeregisterContainerInstance", - "ecs:DiscoverPollEndpoint", - "ecs:Poll", "ecs:RegisterContainerInstance", - "ecs:StartTelemetrySession", - "ecs:Submit*", + "ecs:Submit*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "FargateCluster7CCD5F93", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:Poll", + "ecs:StartTelemetrySession" + ], + "Effect": "Allow", + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "FargateCluster7CCD5F93", + "Arn" + ] + } + } + } + }, + { + "Action": [ + "ecs:DiscoverPollEndpoint", "ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents" @@ -243,7 +269,17 @@ "ecs:DescribeTasks" ], "Effect": "Allow", - "Resource": "*" + "Resource": "*", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "FargateCluster7CCD5F93", + "Arn" + ] + } + } + } }, { "Action": [ diff --git a/packages/@aws-cdk/cfnspec/spec-source/570_Athena_Workgroup_Tags_patch.json b/packages/@aws-cdk/cfnspec/spec-source/570_Athena_Workgroup_Tags_patch.json new file mode 100644 index 0000000000000..6f9224d738886 --- /dev/null +++ b/packages/@aws-cdk/cfnspec/spec-source/570_Athena_Workgroup_Tags_patch.json @@ -0,0 +1,36 @@ +{ + "PropertyTypes": { + "AWS::Athena::WorkGroup.Tags": { + "patch": { + "description": "Corrects tag specification for AWS::Athena::WorkGroup.Tags", + "operations": [ + { + "op": "remove", + "path": "/Properties" + }, + { + "op": "add", + "path": "/ItemType", + "value": "Tag" + }, + { + "op": "add", + "path": "/Required", + "value": false + }, + { + "op": "add", + "path": "/Type", + "value": "List" + }, + { + "op": "add", + "path": "/UpdateType", + "value": "Mutable" + } + ] + } + } + } +} + diff --git a/packages/@aws-cdk/cloudformation-include/README.md b/packages/@aws-cdk/cloudformation-include/README.md index 535846de608fb..0e51dc0402f16 100644 --- a/packages/@aws-cdk/cloudformation-include/README.md +++ b/packages/@aws-cdk/cloudformation-include/README.md @@ -13,11 +13,12 @@ This module contains a set of classes whose goal is to facilitate working with existing CloudFormation templates in the CDK. It can be thought of as an extension of the capabilities of the -[`CfnInclude` class](../@aws-cdk/core/lib/cfn-include.ts). +[`CfnInclude` class](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.CfnInclude.html). ## Basic usage -Assume we have a file with an existing template. It could be in JSON format, in a file `my-template.json`: +Assume we have a file with an existing template. +It could be in JSON format, in a file `my-template.json`: ```json { @@ -52,7 +53,7 @@ const cfnTemplate = new cfn_inc.CfnInclude(this, 'Template', { }); ``` -Or, if our template is YAML, we can use +Or, if your template uses YAML: ```typescript const cfnTemplate = new cfn_inc.CfnInclude(this, 'Template', { @@ -60,7 +61,7 @@ const cfnTemplate = new cfn_inc.CfnInclude(this, 'Template', { }); ``` -This will add all resources from `my-template.json` into the CDK application, +This will add all resources from `my-template.json` / `my-template.yaml` into the CDK application, preserving their original logical IDs from the template file. Any resource from the included template can be retrieved by referring to it by its logical ID from the template. @@ -74,6 +75,12 @@ const cfnBucket = cfnTemplate.getResource('Bucket') as s3.CfnBucket; // cfnBucket is of type s3.CfnBucket ``` +Note that any resources not present in the latest version of the CloudFormation schema +at the time of publishing the version of this module that you depend on, +including [Custom Resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cfn-customresource.html), +will be returned as instances of the class `CfnResource`, +and so cannot be cast to a different resource type. + Any modifications made to that resource will be reflected in the resulting CDK template; for example, the name of the bucket can be changed: @@ -99,20 +106,16 @@ role.addToPolicy(new iam.PolicyStatement({ ``` If you need, you can also convert the CloudFormation resource to a higher-level -resource by importing it by its name: +resource by importing it: ```typescript const bucket = s3.Bucket.fromBucketName(this, 'L2Bucket', cfnBucket.ref); // bucket is of type s3.IBucket ``` -Note that [Custom Resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cfn-customresource.html) -will be of type CfnResource, and hence won't need to be casted. -This holds for any resource that isn't in the CloudFormation schema. - ## Parameters -If your template uses [CloudFormation Parameters] (https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html), +If your template uses [CloudFormation Parameters](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html), you can retrieve them from your template: ```typescript @@ -176,27 +179,26 @@ For example, if you have the following parent template: "ChildStack": { "Type": "AWS::CloudFormation::Stack", "Properties": { - "TemplateURL": "https://my-s3-template-source.s3.amazonaws.com/child-import-stack.json" + "TemplateURL": "https://my-s3-template-source.s3.amazonaws.com/child-stack.json" } } } } ``` -where the child template pointed to by `https://my-s3-template-source.s3.amazonaws.com/child-import-stack.json` is: +where the child template pointed to by `https://my-s3-template-source.s3.amazonaws.com/child-stack.json` is: ```json { "Resources": { "MyBucket": { "Type": "AWS::S3::Bucket" - } } } } ``` -You can include both the parent stack and the nested stack in your CDK Application as follows: +You can include both the parent stack and the nested stack in your CDK application as follows: ```typescript const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', { @@ -209,30 +211,30 @@ const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', { }); ``` -Now you can access the ChildStack nested stack and included template with: +The included nested stack can be accessed with the `getNestedStack` method: ```typescript const includedChildStack = parentTemplate.getNestedStack('ChildStack'); const childStack: core.NestedStack = includedChildStack.stack; -const childStackTemplate: cfn_inc.CfnInclude = includedChildStack.includedTemplate; +const childTemplate: cfn_inc.CfnInclude = includedChildStack.includedTemplate; ``` Now you can reference resources from `ChildStack` and modify them like any other included template: ```typescript -const bucket = childStackTemplate.getResource('MyBucket') as s3.CfnBucket; -bucket.bucketName = 'my-new-bucket-name'; +const cfnBucket = childTemplate.getResource('MyBucket') as s3.CfnBucket; +cfnBucket.bucketName = 'my-new-bucket-name'; -const bucketReadRole = new iam.Role(childStack, 'MyRole', { +const role = new iam.Role(childStack, 'MyRole', { assumedBy: new iam.AccountRootPrincipal(), }); -bucketReadRole.addToPolicy(new iam.PolicyStatement({ +role.addToPolicy(new iam.PolicyStatement({ actions: [ 's3:GetObject*', 's3:GetBucket*', 's3:List*', ], - resources: [bucket.attrArn], + resources: [cfnBucket.attrArn], })); ``` diff --git a/packages/@aws-cdk/cloudformation-include/test/serverless-transform.test.ts b/packages/@aws-cdk/cloudformation-include/test/serverless-transform.test.ts new file mode 100644 index 0000000000000..3d4d0f977893d --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/serverless-transform.test.ts @@ -0,0 +1,70 @@ +import * as path from 'path'; +import '@aws-cdk/assert/jest'; +import * as core from '@aws-cdk/core'; +import * as inc from '../lib'; +import * as futils from '../lib/file-utils'; + +/* eslint-disable quote-props */ +/* eslint-disable quotes */ + +describe('CDK Include for templates with SAM transform', () => { + let stack: core.Stack; + + beforeEach(() => { + stack = new core.Stack(); + }); + + 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( + loadTestFileToJsObject('only-minimal-sam-function-codeuri-as-s3location.yaml'), + ); + }); + + 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( + loadTestFileToJsObject('only-sam-function-policies-array-ddb-crud.yaml'), + ); + }); + + 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( + loadTestFileToJsObject('only-minimal-sam-function-codeuri-as-param.yaml'), + ); + }); + + 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( + loadTestFileToJsObject('only-minimal-sam-function-codeuri-bucket-as-param.yaml'), + ); + }); + + 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( + loadTestFileToJsObject('only-sam-function-policies-array-ddb-crud-if.yaml'), + ); + }); +}); + +function includeTestTemplate(scope: core.Construct, testTemplate: string): inc.CfnInclude { + return new inc.CfnInclude(scope, 'MyScope', { + templateFile: _testTemplateFilePath(testTemplate), + }); +} + +function loadTestFileToJsObject(testTemplate: string): any { + return futils.readYamlSync(_testTemplateFilePath(testTemplate)); +} + +function _testTemplateFilePath(testTemplate: string) { + return path.join(__dirname, 'test-templates', 'sam', testTemplate); +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/only-minimal-sam-function-codeuri-as-param.yaml b/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/only-minimal-sam-function-codeuri-as-param.yaml new file mode 100644 index 0000000000000..5337644447d8f --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/only-minimal-sam-function-codeuri-as-param.yaml @@ -0,0 +1,13 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Parameters: + CodeLocation: + Type: String +Resources: + MicroserviceHttpEndpoint: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: + Ref: CodeLocation diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/only-minimal-sam-function-codeuri-as-s3location.yaml b/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/only-minimal-sam-function-codeuri-as-s3location.yaml new file mode 100644 index 0000000000000..0c6b8a60f367b --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/only-minimal-sam-function-codeuri-as-s3location.yaml @@ -0,0 +1,11 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Resources: + MicroserviceHttpEndpoint: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: + Bucket: awsserverlessrepo-changesets-1f9ifp952i9h0 + Key: 123456789012/arn:aws:serverlessrepo:us-east-1:077246666028:applications-microservice-http-endpoint-versions-1.0.4/dc38a8c1-d27f-44f3-b545-4cfff4f8b865 diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/only-minimal-sam-function-codeuri-bucket-as-param.yaml b/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/only-minimal-sam-function-codeuri-bucket-as-param.yaml new file mode 100644 index 0000000000000..edcec53936097 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/only-minimal-sam-function-codeuri-bucket-as-param.yaml @@ -0,0 +1,15 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Parameters: + CodeLocation: + Type: String +Resources: + MicroserviceHttpEndpoint: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: + Bucket: + Ref: CodeLocation + Key: my-key diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/only-sam-function-policies-array-ddb-crud-if.yaml b/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/only-sam-function-policies-array-ddb-crud-if.yaml new file mode 100644 index 0000000000000..f8d6a15bf5cdb --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/only-sam-function-policies-array-ddb-crud-if.yaml @@ -0,0 +1,26 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Conditions: + SomeCondition: + Fn::Equals: [1, 2] +Parameters: + TableNameParameter: + Type: String +Resources: + MicroserviceHttpEndpoint: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: + Bucket: awsserverlessrepo-changesets-1f9ifp952i9h0 + Key: 828671620168/arn:aws:serverlessrepo:us-east-1:077246666028:applications-microservice-http-endpoint-versions-1.0.4/dc38a8c1-d27f-44f3-b545-4cfff4f8b865 + Policies: + - Fn::If: + - SomeCondition + - DynamoDBCrudPolicy: + TableName: + Ref: TableNameParameter + - DynamoDBCrudPolicy: + TableName: + Ref: TableNameParameter diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/only-sam-function-policies-array-ddb-crud.yaml b/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/only-sam-function-policies-array-ddb-crud.yaml new file mode 100644 index 0000000000000..6b68a0a2fdec3 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/only-sam-function-policies-array-ddb-crud.yaml @@ -0,0 +1,16 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Parameters: + TableNameParameter: + Type: String +Resources: + MicroserviceHttpEndpoint: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: my-code-uri + Policies: + - DynamoDBCrudPolicy: + TableName: + Ref: TableNameParameter diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/user-data.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/user-data.json new file mode 100644 index 0000000000000..81f926fcbf3cf --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/user-data.json @@ -0,0 +1,30 @@ +{ + "Resources": { + "LaunchConfig": { + "Type" : "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": "ami-01e24be29428c15b2", + "InstanceType": "t1.micro", + "UserData": { + "Fn::Base64": { + "Fn::Join": ["", [ + "#!/bin/bash -xe\n", + "yum update -y aws-cfn-bootstrap\n", + + "/opt/aws/bin/cfn-init -v ", + " --stack ", { "Ref" : "AWS::StackName" }, + " --resource LaunchConfig ", + " --configsets wordpress_install ", + " --region ", { "Ref" : "AWS::Region" }, "\n", + + "/opt/aws/bin/cfn-signal -e $? ", + " --stack ", { "Ref" : "AWS::StackName" }, + " --resource WebServerGroup ", + " --region ", { "Ref" : "AWS::Region" }, "\n" + ]] + } + } + } + } + } +} 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 329cda9e759c8..845aa001ba3d2 100644 --- a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts @@ -184,6 +184,14 @@ describe('CDK Include', () => { ); }); + test('can ingest a UserData script, and output it unchanged', () => { + includeTestTemplate(stack, 'user-data.json'); + + expect(stack).toMatchTemplate( + loadTestFileToJsObject('user-data.json'), + ); + }); + test('can ingest a template with intrinsic functions and conditions, and output it unchanged', () => { includeTestTemplate(stack, 'functions-and-conditions.json'); diff --git a/packages/@aws-cdk/core/README.md b/packages/@aws-cdk/core/README.md index 65f9067ff32d4..d2a240166995c 100644 --- a/packages/@aws-cdk/core/README.md +++ b/packages/@aws-cdk/core/README.md @@ -746,7 +746,7 @@ CloudFormation [mappings][cfn-mappings] are created and queried using the `CfnMappings` class: ```ts -const mapping = new CfnMapping(this, 'MappingTable', { +const regionTable = new CfnMapping(this, 'RegionTable', { mapping: { regionName: { 'us-east-1': 'US East (N. Virginia)', @@ -757,7 +757,17 @@ const mapping = new CfnMapping(this, 'MappingTable', { } }); -mapping.findInMap('regionName', Aws.REGION); +regionTable.findInMap('regionName', Aws.REGION); +``` + +This will yield the following template: + +```yaml +Mappings: + RegionTable: + regionName: + us-east-1: US East (N. Virginia) + us-east-2: US East (Ohio) ``` [cfn-mappings]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html diff --git a/packages/@aws-cdk/core/lib/asset-staging.ts b/packages/@aws-cdk/core/lib/asset-staging.ts index d042ec90b1f4b..c42cf35f2b5a8 100644 --- a/packages/@aws-cdk/core/lib/asset-staging.ts +++ b/packages/@aws-cdk/core/lib/asset-staging.ts @@ -5,8 +5,9 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; import { AssetHashType, AssetOptions } from './assets'; import { BundlingOptions } from './bundling'; -import { Construct, ISynthesisSession } from './construct-compat'; import { FileSystem, FingerprintOptions } from './fs'; +import { Stage } from './stage'; +import { Construct } from './construct-compat'; const STAGING_TMP = '.cdk.staging'; @@ -103,15 +104,22 @@ export class AssetStaging extends Construct { } this.sourceHash = this.assetHash; + + const outdir = Stage.of(this)?.outdir; + if (!outdir) { + throw new Error('unable to determine cloud assembly output directory. Assets must be defined indirectly within a "Stage" or an "App" scope'); + } + + this.stageAsset(outdir); } - protected synthesize(session: ISynthesisSession) { + private stageAsset(outdir: string) { // Staging is disabled if (!this.relativePath) { return; } - const targetPath = path.join(session.assembly.outdir, this.relativePath); + const targetPath = path.join(outdir, this.relativePath); // Already staged if (fs.existsSync(targetPath)) { diff --git a/packages/@aws-cdk/core/lib/cfn-parse.ts b/packages/@aws-cdk/core/lib/cfn-parse.ts index c821b66971073..5872399e0efee 100644 --- a/packages/@aws-cdk/core/lib/cfn-parse.ts +++ b/packages/@aws-cdk/core/lib/cfn-parse.ts @@ -7,8 +7,10 @@ import { } from './cfn-resource-policy'; import { CfnTag } from './cfn-tag'; import { ICfnFinder } from './from-cfn'; +import { Lazy } from './lazy'; import { CfnReference } from './private/cfn-reference'; import { IResolvable } from './resolvable'; +import { Mapper, Validator } from './runtime'; import { isResolvableObject, Token } from './token'; /** @@ -77,36 +79,40 @@ export class FromCloudFormation { } // in all other cases, delegate to the standard mapping logic - return this.getArray(value, this.getString); + return this.getArray(this.getString)(value); } - public static getArray(value: any, mapper: (arg: any) => T): T[] { - if (!Array.isArray(value)) { - // break the type system, and just return the given value, - // which hopefully will be reported as invalid by the validator - // of the property we're transforming - // (unless it's a deploy-time value, - // which we can't map over at build time anyway) - return value; - } + public static getArray(mapper: (arg: any) => T): (x: any) => T[] { + return (value: any) => { + if (!Array.isArray(value)) { + // break the type system, and just return the given value, + // which hopefully will be reported as invalid by the validator + // of the property we're transforming + // (unless it's a deploy-time value, + // which we can't map over at build time anyway) + return value; + } - return value.map(mapper); + return value.map(mapper); + }; } - public static getMap(value: any, mapper: (arg: any) => T): { [key: string]: T } { - if (typeof value !== 'object') { - // if the input is not a map (= object in JS land), - // just return it, and let the validator of this property handle it - // (unless it's a deploy-time value, - // which we can't map over at build time anyway) - return value; - } + public static getMap(mapper: (arg: any) => T): (x: any) => { [key: string]: T } { + return (value: any) => { + if (typeof value !== 'object') { + // if the input is not a map (= object in JS land), + // just return it, and let the validator of this property handle it + // (unless it's a deploy-time value, + // which we can't map over at build time anyway) + return value; + } - const ret: { [key: string]: T } = {}; - for (const [key, val] of Object.entries(value)) { - ret[key] = mapper(val); - } - return ret; + const ret: { [key: string]: T } = {}; + for (const [key, val] of Object.entries(value)) { + ret[key] = mapper(val); + } + return ret; + }; } public static getCfnTag(tag: any): CfnTag { @@ -117,6 +123,23 @@ export class FromCloudFormation { value: tag.Value, }; } + + /** + * Return a function that, when applied to a value, will return the first validly deserialized one + */ + public static getTypeUnion(validators: Validator[], mappers: Mapper[]): (x: any) => any { + return (value: any): any => { + for (let i = 0; i < validators.length; i++) { + const candidate = mappers[i](value); + if (validators[i](candidate).isSuccess) { + return candidate; + } + } + + // if nothing matches, just return the input unchanged, and let validators catch it + return value; + }; + } } /** @@ -359,7 +382,11 @@ export class CfnParser { // where the first element is the delimiter, // and the second is the list of elements to join const value = this.parseValue(object[key]); - return Fn.join(value[0], value[1]); + // wrap the array as a Token, + // as otherwise Fn.join() will try to concatenate + // the non-token parts, + // causing a diff with the original template + return Fn.join(value[0], Lazy.listValue({ produce: () => value[1] })); } case 'Fn::Cidr': { const value = this.parseValue(object[key]); diff --git a/packages/@aws-cdk/core/lib/cfn-resource.ts b/packages/@aws-cdk/core/lib/cfn-resource.ts index 39b60a3d248b7..9af3aa5c16a21 100644 --- a/packages/@aws-cdk/core/lib/cfn-resource.ts +++ b/packages/@aws-cdk/core/lib/cfn-resource.ts @@ -233,6 +233,11 @@ export class CfnResource extends CfnRefElement { * and the dependency will automatically be transferred to the relevant scope. */ public addDependsOn(target: CfnResource) { + // skip this dependency if the target is not part of the output + if (!target.shouldSynthesize()) { + return; + } + addDependency(this, target, `"${this.node.path}" depends on "${target.node.path}"`); } @@ -278,6 +283,10 @@ export class CfnResource extends CfnRefElement { * @internal */ public _toCloudFormation(): object { + if (!this.shouldSynthesize()) { + return { }; + } + try { const ret = { Resources: { @@ -362,6 +371,17 @@ export class CfnResource extends CfnRefElement { protected validateProperties(_properties: any) { // Nothing } + + /** + * Can be overridden by subclasses to determine if this resource will be rendered + * into the cloudformation template. + * + * @returns `true` if the resource should be included or `false` is the resource + * should be omitted. + */ + protected shouldSynthesize() { + return true; + } } export enum TagType { diff --git a/packages/@aws-cdk/core/lib/nested-stack.ts b/packages/@aws-cdk/core/lib/nested-stack.ts index 4d87c148958f0..7e52427467039 100644 --- a/packages/@aws-cdk/core/lib/nested-stack.ts +++ b/packages/@aws-cdk/core/lib/nested-stack.ts @@ -187,7 +187,7 @@ export class NestedStack extends Stack { return false; } - const cfn = JSON.stringify((this as any)._toCloudFormation()); + const cfn = JSON.stringify(this._toCloudFormation()); const templateHash = crypto.createHash('sha256').update(cfn).digest('hex'); const templateLocation = this._parentStack.addFileAsset({ diff --git a/packages/@aws-cdk/core/lib/private/prepare-app.ts b/packages/@aws-cdk/core/lib/private/prepare-app.ts index de5ef433fb1ad..ad900acf803c0 100644 --- a/packages/@aws-cdk/core/lib/private/prepare-app.ts +++ b/packages/@aws-cdk/core/lib/private/prepare-app.ts @@ -16,10 +16,6 @@ import { resolveReferences } from './refs'; * @param root The root of the construct tree. */ export function prepareApp(root: IConstruct) { - if (root.node.scope && !Stage.isStage(root)) { - throw new Error('prepareApp can only be called on a stage or a root construct'); - } - // apply dependencies between resources in depending subtrees for (const dependency of root.node.dependencies) { const targetCfnResources = findCfnResources(dependency.target); diff --git a/packages/@aws-cdk/core/lib/private/synthesis.ts b/packages/@aws-cdk/core/lib/private/synthesis.ts index ea6fbf7b05ffa..296ba171efde9 100644 --- a/packages/@aws-cdk/core/lib/private/synthesis.ts +++ b/packages/@aws-cdk/core/lib/private/synthesis.ts @@ -1,8 +1,10 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as constructs from 'constructs'; import { Construct, IConstruct, SynthesisOptions, ValidationError } from '../construct-compat'; +import { Stack } from '../stack'; import { Stage, StageSynthesisOptions } from '../stage'; import { prepareApp } from './prepare-app'; +import { TreeMetadata } from './tree-metadata'; export function synthesize(root: IConstruct, options: SynthesisOptions = { }): cxapi.CloudAssembly { // we start by calling "synth" on all nested assemblies (which will take care of all their children) @@ -58,6 +60,7 @@ function synthNestedAssemblies(root: IConstruct, options: StageSynthesisOptions) * twice for the same construct. */ function invokeAspects(root: IConstruct) { + let nestedAspectWarning = false; recurse(root, []); function recurse(construct: IConstruct, inheritedAspects: constructs.IAspect[]) { @@ -65,10 +68,17 @@ function invokeAspects(root: IConstruct) { const node: NodeWithAspectPrivatesHangingOut = construct.node._actualNode as any; const allAspectsHere = [...inheritedAspects ?? [], ...node._aspects]; - + const nodeAspectsCount = node._aspects.length; for (const aspect of allAspectsHere) { if (node.invokedAspects.includes(aspect)) { continue; } + aspect.visit(construct); + // if an aspect was added to the node while invoking another aspect it will not be invoked, emit a warning + // the `nestedAspectWarning` flag is used to prevent the warning from being emitted for every child + if (!nestedAspectWarning && nodeAspectsCount !== node._aspects.length) { + construct.node.addWarning('We detected an Aspect was added via another Aspect, and will not be applied'); + nestedAspectWarning = true; + } node.invokedAspects.push(aspect); } @@ -95,10 +105,22 @@ function prepareTree(root: IConstruct) { * Stop at Assembly boundaries. */ function synthesizeTree(root: IConstruct, builder: cxapi.CloudAssemblyBuilder) { - visit(root, 'post', construct => construct.onSynthesize({ - outdir: builder.outdir, - assembly: builder, - })); + visit(root, 'post', construct => { + const session = { + outdir: builder.outdir, + assembly: builder, + }; + + if (construct instanceof Stack) { + construct._synthesizeTemplate(session); + } else if (construct instanceof TreeMetadata) { + construct._synthesizeTree(session); + } + + // this will soon be deprecated and removed in 2.x + // see https://github.com/aws/aws-cdk-rfcs/issues/192 + construct.onSynthesize(session); + }); } /** diff --git a/packages/@aws-cdk/core/lib/private/tree-metadata.ts b/packages/@aws-cdk/core/lib/private/tree-metadata.ts index a40481faa9b2d..b2f288d97b4b4 100644 --- a/packages/@aws-cdk/core/lib/private/tree-metadata.ts +++ b/packages/@aws-cdk/core/lib/private/tree-metadata.ts @@ -20,7 +20,11 @@ export class TreeMetadata extends Construct { super(scope, 'Tree'); } - protected synthesize(session: ISynthesisSession) { + /** + * Create tree.json + * @internal + */ + public _synthesizeTree(session: ISynthesisSession) { const lookup: { [path: string]: Node } = { }; const visit = (construct: IConstruct): Node => { diff --git a/packages/@aws-cdk/core/lib/stack.ts b/packages/@aws-cdk/core/lib/stack.ts index 6af8d3b075150..f913377b54a20 100644 --- a/packages/@aws-cdk/core/lib/stack.ts +++ b/packages/@aws-cdk/core/lib/stack.ts @@ -80,7 +80,7 @@ export interface StackProps { * } * }); * - * // both of these stavks will use the stage's account/region: + * // both of these stacks will use the stage's account/region: * // `.account` and `.region` will resolve to the concrete values as above * new MyStack(myStage, 'Stack1'); * new YourStack(myStage, 'Stack1'); @@ -182,7 +182,7 @@ export class Stack extends Construct implements ITaggable { /** * Options for CloudFormation template (like version, transform, description). */ - public readonly templateOptions: ITemplateOptions = {}; + public readonly templateOptions: ITemplateOptions; /** * The AWS region into which this stack will be deployed (e.g. `us-west-2`). @@ -284,29 +284,42 @@ export class Stack extends Construct implements ITaggable { /** * Other stacks this stack depends on */ - private readonly _stackDependencies: { [uniqueId: string]: StackDependency } = { }; + private readonly _stackDependencies: { [uniqueId: string]: StackDependency }; /** * Lists all missing contextual information. * This is returned when the stack is synthesized under the 'missing' attribute * and allows tooling to obtain the context and re-synthesize. */ - private readonly _missingContext = new Array(); + private readonly _missingContext: cxschema.MissingContext[]; private readonly _stackName: string; /** * Creates a new stack. * - * @param scope Parent of this stack, usually a Program instance. + * @param scope Parent of this stack, usually an `App` or a `Stage`, but could be any construct. * @param id The construct ID of this stack. If `stackName` is not explicitly * defined, this id (and any parent IDs) will be used to determine the * physical ID of the stack. * @param props Stack properties. */ public constructor(scope?: Construct, id?: string, props: StackProps = {}) { - // For unit test convenience parents are optional, so bypass the type check when calling the parent. - super(scope!, id!); + // For unit test scope and id are optional for stacks, but we still want an App + // as the parent because apps implement much of the synthesis logic. + scope = scope ?? new App({ + autoSynth: false, + outdir: FileSystem.mkdtemp('cdk-test-app-'), + }); + + // "Default" is a "hidden id" from a `node.uniqueId` perspective + id = id ?? 'Default'; + + super(scope, id); + + this._missingContext = new Array(); + this._stackDependencies = { }; + this.templateOptions = { }; Object.defineProperty(this, STACK_SYMBOL, { value: true }); @@ -687,6 +700,32 @@ export class Stack extends Construct implements ITaggable { } } + /** + * Synthesizes the cloudformation template into a cloud assembly. + * @internal + */ + public _synthesizeTemplate(session: ISynthesisSession): void { + // In principle, stack synthesis is delegated to the + // StackSynthesis object. + // + // However, some parts of synthesis currently use some private + // methods on Stack, and I don't really see the value in refactoring + // this right now, so some parts still happen here. + const builder = session.assembly; + + // write the CloudFormation template as a JSON file + const outPath = path.join(builder.outdir, this.templateFile); + const text = JSON.stringify(this._toCloudFormation(), undefined, 2); + fs.writeFileSync(outPath, text); + + for (const ctx of this._missingContext) { + builder.addMissing(ctx); + } + + // Delegate adding artifacts to the Synthesizer + this.synthesizer.synthesizeStackArtifacts(session); + } + /** * Returns the naming scheme used to allocate logical IDs. By default, uses * the `HashedAddressingScheme` but this method can be overridden to customize @@ -748,28 +787,6 @@ export class Stack extends Construct implements ITaggable { } } - protected synthesize(session: ISynthesisSession): void { - // In principle, stack synthesis is delegated to the - // StackSynthesis object. - // - // However, some parts of synthesis currently use some private - // methods on Stack, and I don't really see the value in refactoring - // this right now, so some parts still happen here. - const builder = session.assembly; - - // write the CloudFormation template as a JSON file - const outPath = path.join(builder.outdir, this.templateFile); - const text = JSON.stringify(this._toCloudFormation(), undefined, 2); - fs.writeFileSync(outPath, text); - - for (const ctx of this._missingContext) { - builder.addMissing(ctx); - } - - // Delegate adding artifacts to the Synthesizer - this.synthesizer.synthesizeStackArtifacts(session); - } - /** * Returns the CloudFormation template for this stack by traversing * the tree and invoking _toCloudFormation() on all Entity objects. @@ -917,7 +934,7 @@ export class Stack extends Construct implements ITaggable { // In unit tests our Stack (which is the only component) may not have an // id, so in that case just pretend it's "Stack". if (ids.length === 1 && !ids[0]) { - ids[0] = 'Stack'; + throw new Error('unexpected: stack id must always be defined'); } return makeStackName(ids); @@ -947,18 +964,22 @@ function mergeSection(section: string, val1: any, val2: any): any { throw new Error(`Conflicting CloudFormation template versions provided: '${val1}' and '${val2}`); } return val1 ?? val2; - case 'Resources': - case 'Conditions': - case 'Parameters': - case 'Outputs': - case 'Mappings': - case 'Metadata': case 'Transform': - return mergeObjectsWithoutDuplicates(section, val1, val2); + return mergeSets(val1, val2); default: - throw new Error(`CDK doesn't know how to merge two instances of the CFN template section '${section}' - ` + - 'please remove one of them from your code'); + return mergeObjectsWithoutDuplicates(section, val1, val2); + } +} + +function mergeSets(val1: any, val2: any): any { + const array1 = val1 == null ? [] : (Array.isArray(val1) ? val1 : [val1]); + const array2 = val2 == null ? [] : (Array.isArray(val2) ? val2 : [val2]); + for (const value of array2) { + if (!array1.includes(value)) { + array1.push(value); + } } + return array1.length === 1 ? array1[0] : array1; } function mergeObjectsWithoutDuplicates(section: string, dest: any, src: any): any { @@ -1070,6 +1091,8 @@ import { DefaultStackSynthesizer, IStackSynthesizer, LegacyStackSynthesizer } fr import { Stage } from './stage'; import { ITaggable, TagManager } from './tag-manager'; import { Token } from './token'; +import { FileSystem } from './fs'; +import { App } from './app'; interface StackDependency { stack: Stack; diff --git a/packages/@aws-cdk/core/lib/stage.ts b/packages/@aws-cdk/core/lib/stage.ts index 59a466499bc9a..a8a0d1d18523a 100644 --- a/packages/@aws-cdk/core/lib/stage.ts +++ b/packages/@aws-cdk/core/lib/stage.ts @@ -143,6 +143,13 @@ export class Stage extends Construct { this.stageName = [ this.parentStage?.stageName, id ].filter(x => x).join('-'); } + /** + * The cloud assembly output directory. + */ + public get outdir() { + return this._assemblyBuilder.outdir; + } + /** * Artifact ID of the assembly if it is a nested stage. The root stage (app) * will return an empty string. @@ -163,7 +170,7 @@ export class Stage extends Construct { * calls will return the same assembly. */ public synth(options: StageSynthesisOptions = { }): cxapi.CloudAssembly { - if (!this.assembly) { + if (!this.assembly || options.force) { const runtimeInfo = this.node.tryGetContext(cxapi.DISABLE_VERSION_REPORTING) ? undefined : collectRuntimeInformation(); this.assembly = synthesize(this, { skipValidation: options.skipValidation, @@ -198,4 +205,12 @@ export interface StageSynthesisOptions { * @default - false */ readonly skipValidation?: boolean; + + /** + * Force a re-synth, even if the stage has already been synthesized. + * This is used by tests to allow for incremental verification of the output. + * Do not use in production. + * @default false + */ + readonly force?: boolean; } diff --git a/packages/@aws-cdk/core/test/fs/test.fs.ts b/packages/@aws-cdk/core/test/fs/test.fs.ts index 11a03a653eaf5..7616eaef536fc 100644 --- a/packages/@aws-cdk/core/test/fs/test.fs.ts +++ b/packages/@aws-cdk/core/test/fs/test.fs.ts @@ -27,7 +27,9 @@ export = { test.equal(p, fs.realpathSync(p)); test.equal(fs.readFileSync(p, 'utf8'), 'tmpdir-test'); - test.ok(tmpdirStub.calledOnce); // cached result + // check that tmpdir() is called either 0 times (in which case it was + // proabably cached from before) or once (for this test). + test.ok(tmpdirStub.callCount < 2); fs.unlinkSync(p); fs.unlinkSync(symlinkTmp); diff --git a/packages/@aws-cdk/core/test/test.aspect.ts b/packages/@aws-cdk/core/test/test.aspect.ts index 5c9a3ae9b57fe..5b13b2a547b5b 100644 --- a/packages/@aws-cdk/core/test/test.aspect.ts +++ b/packages/@aws-cdk/core/test/test.aspect.ts @@ -1,7 +1,8 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { Test } from 'nodeunit'; import { App } from '../lib'; import { IAspect } from '../lib/aspect'; -import { Construct, ConstructNode, IConstruct } from '../lib/construct-compat'; +import { Construct, IConstruct } from '../lib/construct-compat'; class MyConstruct extends Construct { public static IsMyConstruct(x: any): x is MyConstruct { @@ -17,15 +18,60 @@ class VisitOnce implements IAspect { } } } + +class MyAspect implements IAspect { + public visit(node: IConstruct): void { + node.node.addMetadata('foo', 'bar'); + } +} + export = { 'Aspects are invoked only once'(test: Test) { const app = new App(); const root = new MyConstruct(app, 'MyConstruct'); root.node.applyAspect(new VisitOnce()); - ConstructNode.prepare(root.node); + app.synth(); test.deepEqual(root.visitCounter, 1); - ConstructNode.prepare(root.node); + app.synth(); test.deepEqual(root.visitCounter, 1); test.done(); }, + + 'Warn if an Aspect is added via another Aspect'(test: Test) { + const app = new App(); + const root = new MyConstruct(app, 'MyConstruct'); + const child = new MyConstruct(root, 'ChildConstruct'); + root.node.applyAspect({ + visit(construct: IConstruct) { + construct.node.applyAspect({ + visit(inner: IConstruct) { + inner.node.addMetadata('test', 'would-be-ignored'); + }, + }); + }, + }); + app.synth(); + test.deepEqual(root.node.metadata[0].type, cxschema.ArtifactMetadataEntryType.WARN); + test.deepEqual(root.node.metadata[0].data, 'We detected an Aspect was added via another Aspect, and will not be applied'); + // warning is not added to child construct + test.equal(child.node.metadata.length, 0); + test.done(); + }, + + 'Do not warn if an Aspect is added directly (not by another aspect)'(test: Test) { + const app = new App(); + const root = new MyConstruct(app, 'Construct'); + const child = new MyConstruct(root, 'ChildConstruct'); + root.node.applyAspect(new MyAspect()); + app.synth(); + test.deepEqual(root.node.metadata[0].type, 'foo'); + test.deepEqual(root.node.metadata[0].data, 'bar'); + test.deepEqual(root.node.metadata[0].type, 'foo'); + test.deepEqual(child.node.metadata[0].data, 'bar'); + // no warning is added + test.equal(root.node.metadata.length, 1); + test.equal(child.node.metadata.length, 1); + test.done(); + }, + }; diff --git a/packages/@aws-cdk/core/test/test.cfn-resource.ts b/packages/@aws-cdk/core/test/test.cfn-resource.ts index 41d360296ec1d..5033af4b21598 100644 --- a/packages/@aws-cdk/core/test/test.cfn-resource.ts +++ b/packages/@aws-cdk/core/test/test.cfn-resource.ts @@ -97,4 +97,33 @@ export = nodeunit.testCase({ test.done(); }, + + 'subclasses can override "shouldSynthesize" to lazy-determine if the resource should be included'(test: nodeunit.Test) { + // GIVEN + class HiddenCfnResource extends core.CfnResource { + protected shouldSynthesize() { + return false; + } + } + + const app = new core.App(); + const stack = new core.Stack(app, 'TestStack'); + const subtree = new core.Construct(stack, 'subtree'); + + // WHEN + new HiddenCfnResource(subtree, 'R1', { type: 'Foo::R1' }); + const r2 = new core.CfnResource(stack, 'R2', { type: 'Foo::R2' }); + + // also try to take a dependency on the parent of `r1` and expect the dependency not to materialize + r2.node.addDependency(subtree); + + // THEN - only R2 is synthesized + test.deepEqual(app.synth().getStackByName(stack.stackName).template, { + Resources: { R2: { Type: 'Foo::R2' } }, + + // No DependsOn! + }); + + test.done(); + }, }); diff --git a/packages/@aws-cdk/core/test/test.context.ts b/packages/@aws-cdk/core/test/test.context.ts index d83f41ba9feaf..b19c2514b1786 100644 --- a/packages/@aws-cdk/core/test/test.context.ts +++ b/packages/@aws-cdk/core/test/test.context.ts @@ -1,6 +1,7 @@ import { Test } from 'nodeunit'; -import { Construct, ConstructNode, Stack } from '../lib'; +import { Construct, Stack } from '../lib'; import { ContextProvider } from '../lib/context-provider'; +import { synthesize } from '../lib/private/synthesis'; export = { 'AvailabilityZoneProvider returns a list with dummy values if the context is not available'(test: Test) { @@ -172,7 +173,7 @@ export = { * Get the expected context key from a stack with missing parameters */ function expectedContextKey(stack: Stack): string { - const missing = ConstructNode.synth(stack.node).manifest.missing; + const missing = synthesize(stack).manifest.missing; if (!missing || missing.length !== 1) { throw new Error('Expecting assembly to include a single missing context report'); } diff --git a/packages/@aws-cdk/core/test/test.resource.ts b/packages/@aws-cdk/core/test/test.resource.ts index 34f07c02ab56a..bebaf2e8be1a8 100644 --- a/packages/@aws-cdk/core/test/test.resource.ts +++ b/packages/@aws-cdk/core/test/test.resource.ts @@ -1,8 +1,9 @@ import * as cxapi from '@aws-cdk/cx-api'; import { Test } from 'nodeunit'; import { App, App as Root, CfnCondition, - CfnDeletionPolicy, CfnResource, Construct, ConstructNode, + CfnDeletionPolicy, CfnResource, Construct, Fn, RemovalPolicy, Stack } from '../lib'; +import { synthesize } from '../lib/private/synthesis'; import { toCloudFormation } from './util'; export = { @@ -131,7 +132,8 @@ export = { r2.node.addDependency(r1); r2.node.addDependency(r3); - ConstructNode.prepare(stack.node); + synthesize(stack); + test.deepEqual(toCloudFormation(stack), { Resources: { Counter1: { @@ -358,7 +360,8 @@ export = { dependingResource.node.addDependency(c1, c2); dependingResource.node.addDependency(c3); - ConstructNode.prepare(stack.node); + synthesize(stack); + test.deepEqual(toCloudFormation(stack), { Resources: { MyC1R1FB2A562F: { Type: 'T1' }, MyC1R2AE2B5066: { Type: 'T2' }, @@ -650,7 +653,7 @@ export = { test.deepEqual(toCloudFormation(stack), { Resources: { ParentMyResource4B1FDBCC: { Type: 'MyResourceType', - Metadata: { [cxapi.PATH_METADATA_KEY]: 'Parent/MyResource' } } } }); + Metadata: { [cxapi.PATH_METADATA_KEY]: 'Default/Parent/MyResource' } } } }); test.done(); }, diff --git a/packages/@aws-cdk/core/test/test.rule.ts b/packages/@aws-cdk/core/test/test.rule.ts index 34a3915daf152..8fa7ae9920fc7 100644 --- a/packages/@aws-cdk/core/test/test.rule.ts +++ b/packages/@aws-cdk/core/test/test.rule.ts @@ -29,4 +29,20 @@ export = { test.done(); }, + + 'a template can contain multiple Rules'(test: Test) { + const stack = new Stack(); + + new CfnRule(stack, 'Rule1'); + new CfnRule(stack, 'Rule2'); + + test.deepEqual(toCloudFormation(stack), { + Rules: { + Rule1: {}, + Rule2: {}, + }, + }); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/core/test/test.stack.ts b/packages/@aws-cdk/core/test/test.stack.ts index f60ec0d8a2513..c6a3be31af013 100644 --- a/packages/@aws-cdk/core/test/test.stack.ts +++ b/packages/@aws-cdk/core/test/test.stack.ts @@ -2,8 +2,9 @@ import * as cxapi from '@aws-cdk/cx-api'; import { Test } from 'nodeunit'; import { App, CfnCondition, CfnInclude, CfnOutput, CfnParameter, - CfnResource, Construct, ConstructNode, Lazy, ScopedAws, Stack, Tag, validateString } from '../lib'; + CfnResource, Construct, Lazy, ScopedAws, Stack, Tag, validateString, ISynthesisSession } from '../lib'; import { Intrinsic } from '../lib/private/intrinsic'; +import { resolveReferences } from '../lib/private/refs'; import { PostResolveToken } from '../lib/util'; import { toCloudFormation } from './util'; @@ -128,9 +129,9 @@ export = { const o = new CfnOutput(stack, 'MyOutput', { value: 'boom' }); const c = new CfnCondition(stack, 'MyCondition'); - test.equal(stack.node.findChild(p.node.path), p); - test.equal(stack.node.findChild(o.node.path), o); - test.equal(stack.node.findChild(c.node.path), c); + test.equal(stack.node.findChild(p.node.id), p); + test.equal(stack.node.findChild(o.node.id), o); + test.equal(stack.node.findChild(c.node.id), c); test.done(); }, @@ -410,7 +411,8 @@ export = { new CfnTest(stack, 'MyThing', { type: 'AWS::Type' }); // THEN - ConstructNode.prepare(stack.node); + resolveReferences(app); + test.done(); }, @@ -524,7 +526,7 @@ export = { new CfnParameter(stack1, 'SomeParameter', { type: 'String', default: account2 }); test.throws(() => { - ConstructNode.prepare(app.node); + app.synth(); // eslint-disable-next-line max-len }, "'Stack2' depends on 'Stack1' (Stack2/SomeParameter -> Stack1.AWS::AccountId). Adding this dependency (Stack1/SomeParameter -> Stack2.AWS::AccountId) would create a cyclic reference."); @@ -541,7 +543,7 @@ export = { // WHEN new CfnParameter(stack2, 'SomeParameter', { type: 'String', default: account1 }); - ConstructNode.prepare(app.node); + app.synth(); // THEN test.deepEqual(stack2.dependencies.map(s => s.node.id), ['Stack1']); @@ -560,7 +562,7 @@ export = { new CfnParameter(stack2, 'SomeParameter', { type: 'String', default: account1 }); test.throws(() => { - ConstructNode.prepare(app.node); + app.synth(); }, /Stack "Stack2" cannot consume a cross reference from stack "Stack1"/); test.done(); @@ -866,6 +868,25 @@ export = { test.done(); }, + + 'users can (still) override "synthesize()" in stack'(test: Test) { + let called = false; + + class MyStack extends Stack { + synthesize(session: ISynthesisSession) { + called = true; + test.ok(session.outdir); + test.equal(session.assembly.outdir, session.outdir); + } + } + + const app = new App(); + new MyStack(app, 'my-stack'); + + app.synth(); + test.ok(called, 'synthesize() not called for Stack'); + test.done(); + }, }; class StackWithPostProcessor extends Stack { diff --git a/packages/@aws-cdk/core/test/test.tag-aspect.ts b/packages/@aws-cdk/core/test/test.tag-aspect.ts index fd1b5e96f280d..cd533bd537e67 100644 --- a/packages/@aws-cdk/core/test/test.tag-aspect.ts +++ b/packages/@aws-cdk/core/test/test.tag-aspect.ts @@ -1,5 +1,6 @@ import { Test } from 'nodeunit'; -import { CfnResource, CfnResourceProps, Construct, ConstructNode, RemoveTag, Stack, Tag, TagManager, TagType } from '../lib'; +import { CfnResource, CfnResourceProps, Construct, RemoveTag, Stack, Tag, TagManager, TagType } from '../lib'; +import { synthesize } from '../lib/private/synthesis'; class TaggableResource extends CfnResource { public readonly tags: TagManager; @@ -55,7 +56,7 @@ export = { }); res.node.applyAspect(new Tag('foo', 'bar')); - ConstructNode.synth(root.node); + synthesize(root); test.deepEqual(res.tags.renderTags(), [{key: 'foo', value: 'bar'}]); test.deepEqual(res2.tags.renderTags(), [{key: 'foo', value: 'bar'}]); @@ -75,7 +76,7 @@ export = { res.node.applyAspect(new Tag('foo', 'foobar')); res.node.applyAspect(new Tag('foo', 'baz')); res2.node.applyAspect(new Tag('foo', 'good')); - ConstructNode.prepare(root.node); + synthesize(root); test.deepEqual(res.tags.renderTags(), [{key: 'foo', value: 'baz'}]); test.deepEqual(res2.tags.renderTags(), [{key: 'foo', value: 'good'}]); test.done(); @@ -99,7 +100,7 @@ export = { res.node.applyAspect(new Tag('first', 'there is only 1')); res.node.applyAspect(new RemoveTag('root')); res.node.applyAspect(new RemoveTag('doesnotexist')); - ConstructNode.prepare(root.node); + synthesize(root); test.deepEqual(res.tags.renderTags(), [{key: 'first', value: 'there is only 1'}]); test.deepEqual(map.tags.renderTags(), {first: 'there is only 1'}); @@ -126,7 +127,8 @@ export = { Tag.add(res, 'first', 'there is only 1'); Tag.remove(res, 'root'); Tag.remove(res, 'doesnotexist'); - ConstructNode.prepare(root.node); + + synthesize(root); test.deepEqual(res.tags.renderTags(), [{key: 'first', value: 'there is only 1'}]); test.deepEqual(map.tags.renderTags(), {first: 'there is only 1'}); @@ -141,11 +143,11 @@ export = { }); res.node.applyAspect(new Tag('foo', 'bar')); - ConstructNode.prepare(root.node); + synthesize(root); test.deepEqual(res.tags.renderTags(), [{key: 'foo', value: 'bar'}]); - ConstructNode.prepare(root.node); + synthesize(root); test.deepEqual(res.tags.renderTags(), [{key: 'foo', value: 'bar'}]); - ConstructNode.prepare(root.node); + synthesize(root); test.deepEqual(res.tags.renderTags(), [{key: 'foo', value: 'bar'}]); test.done(); }, @@ -159,7 +161,7 @@ export = { }); res.node.applyAspect(new RemoveTag('key')); res2.node.applyAspect(new Tag('key', 'value')); - ConstructNode.prepare(root.node); + synthesize(root); test.deepEqual(res.tags.renderTags(), undefined); test.deepEqual(res2.tags.renderTags(), undefined); test.done(); @@ -174,7 +176,7 @@ export = { }); res.node.applyAspect(new RemoveTag('key', {priority: 0})); res2.node.applyAspect(new Tag('key', 'value')); - ConstructNode.prepare(root.node); + synthesize(root); test.deepEqual(res.tags.renderTags(), undefined); test.deepEqual(res2.tags.renderTags(), [{key: 'key', value: 'value'}]); test.done(); @@ -217,7 +219,7 @@ export = { }, }); aspectBranch.node.applyAspect(new Tag('aspects', 'rule')); - ConstructNode.prepare(root.node); + synthesize(root); test.deepEqual(aspectBranch.testProperties().tags, [{key: 'aspects', value: 'rule'}, {key: 'cfn', value: 'is cool'}]); test.deepEqual(asgResource.testProperties().tags, [ {key: 'aspects', value: 'rule', propagateAtLaunch: true}, diff --git a/packages/@aws-cdk/core/test/util.ts b/packages/@aws-cdk/core/test/util.ts index b10c547de6f65..8378d5ad26a3e 100644 --- a/packages/@aws-cdk/core/test/util.ts +++ b/packages/@aws-cdk/core/test/util.ts @@ -1,7 +1,8 @@ -import { ConstructNode, Stack } from '../lib'; +import { Stack } from '../lib'; +import { synthesize } from '../lib/private/synthesis'; export function toCloudFormation(stack: Stack): any { - return ConstructNode.synth(stack.node, { skipValidation: true }).getStackByName(stack.stackName).template; + return synthesize(stack, { skipValidation: true }).getStackByName(stack.stackName).template; } export function reEnableStackTraceCollection(): any { diff --git a/packages/@aws-cdk/pipelines/lib/pipeline.ts b/packages/@aws-cdk/pipelines/lib/pipeline.ts index 03120bf7c9866..f20021b34d1a8 100644 --- a/packages/@aws-cdk/pipelines/lib/pipeline.ts +++ b/packages/@aws-cdk/pipelines/lib/pipeline.ts @@ -102,6 +102,8 @@ export class CdkPipeline extends Construct { pipeline: this._pipeline, projectName: maybeSuffix(props.pipelineName, '-publish'), }); + + this.node.applyAspect({ visit: () => this._assets.removeAssetsStageIfEmpty() }); } /** @@ -178,14 +180,6 @@ export class CdkPipeline extends Construct { return ret; } - protected onPrepare() { - super.onPrepare(); - - // TODO: Support this in a proper way in the upstream library. For now, we - // "un-add" the Assets stage if it turns out to be empty. - this._assets.removeAssetsStageIfEmpty(); - } - /** * Return all StackDeployActions in an ordered list */ diff --git a/packages/@aws-cdk/pipelines/lib/stage.ts b/packages/@aws-cdk/pipelines/lib/stage.ts index 2441da072cede..6b379f686fe2e 100644 --- a/packages/@aws-cdk/pipelines/lib/stage.ts +++ b/packages/@aws-cdk/pipelines/lib/stage.ts @@ -54,6 +54,8 @@ export class CdkStage extends Construct { this.pipelineStage = props.pipelineStage; this.cloudAssemblyArtifact = props.cloudAssemblyArtifact; this.host = props.host; + + this.node.applyAspect({ visit: () => this.prepareStage() }); } /** @@ -175,7 +177,7 @@ export class CdkStage extends Construct { * after creation, nor is there a way to specify relative priorities, which * is a limitation that we should take away in the base library. */ - protected prepare() { + private prepareStage() { // FIXME: Make sure this only gets run once. There seems to be an issue in the reconciliation // loop that may trigger this more than once if it throws an error somewhere, and the exception // that gets thrown here will then override the actual failure. diff --git a/tools/cfn2ts/lib/codegen.ts b/tools/cfn2ts/lib/codegen.ts index 9d555dc0fdde0..6624053395dc2 100644 --- a/tools/cfn2ts/lib/codegen.ts +++ b/tools/cfn2ts/lib/codegen.ts @@ -545,72 +545,70 @@ export default class CodeGenerator { // class used for the visitor class FromCloudFormationFactoryVisitor implements genspec.PropertyVisitor { - constructor( - private readonly baseExpression: string, - private readonly optionalProperty: boolean, - private readonly cfnPropName: string, - private readonly depth: number = 1) { - } - public visitAtom(type: genspec.CodeName): string { const specType = type.specName && self.spec.PropertyTypes[type.specName.fqn]; if (specType && !schema.isRecordType(specType)) { return genspec.typeDispatch(resource, specType, this); } else { - const optionalPreamble = this.optionalProperty - ? `${this.baseExpression} == null ? undefined : ` - : ''; - const suffix = schema.isTagPropertyName(this.cfnPropName) - // Properties that have names considered to denote tags - // have their type generated without a union with IResolvable. - // However, we can't possibly know that when generating the factory - // for that struct, and (in theory, at least) - // the same type can be used as the value of multiple properties, - // some of which do not have a tag-compatible name, - // so there is no way to pass allowReturningIResolvable=false correctly. - // Do the simple thing in that case, and just cast to any. - ? ' as any' - : ''; - return `${optionalPreamble}${genspec.fromCfnFactoryName(type).fqn}(${this.baseExpression})${suffix}`; + return genspec.fromCfnFactoryName(type).fqn; } } public visitList(itemType: genspec.CodeName): string { - const arg = `prop${this.depth}`; return itemType.className === 'string' // an array of strings is a special case, // because it might need to be encoded as a Token directly // (and not an array of tokens), for example, // when a Ref expression references a parameter of type CommaDelimitedList - ? `${CFN_PARSE}.FromCloudFormation.getStringArray(${this.baseExpression})` - : `${CFN_PARSE}.FromCloudFormation.getArray(${this.baseExpression}, (${arg}: any) => ` + - `${this.deeperCopy(arg).visitAtom(itemType)})`; + ? `${CFN_PARSE}.FromCloudFormation.getStringArray` + : `${CFN_PARSE}.FromCloudFormation.getArray(${this.visitAtom(itemType)})`; } public visitMap(itemType: genspec.CodeName): string { - const arg = `prop${this.depth}`; - return `${CFN_PARSE}.FromCloudFormation.getMap(${this.baseExpression}, (${arg}: any) => ` + - `${this.deeperCopy(arg).visitAtom(itemType)})`; + return `${CFN_PARSE}.FromCloudFormation.getMap(${this.visitAtom(itemType)})`; } - public visitAtomUnion(_types: genspec.CodeName[]): string { - return this.baseExpression; - } + public visitAtomUnion(types: genspec.CodeName[]): string { + const validatorNames = types.map(type => genspec.validatorName(type).fqn).join(', '); + const mappers = types.map(type => this.visitAtom(type)).join(', '); - public visitListOrAtom(_scalarTypes: genspec.CodeName[], _itemTypes: genspec.CodeName[]): any { - return this.baseExpression; + return `${CFN_PARSE}.FromCloudFormation.getTypeUnion([${validatorNames}], [${mappers}])`; } - public visitUnionList(_itemTypes: genspec.CodeName[]): string { - return this.baseExpression; + public visitUnionList(itemTypes: genspec.CodeName[]): string { + const validatorNames = itemTypes.map(type => genspec.validatorName(type).fqn).join(', '); + const mappers = itemTypes.map(type => this.visitAtom(type)).join(', '); + + return `${CFN_PARSE}.FromCloudFormation.getArray(` + + `${CFN_PARSE}.FromCloudFormation.getTypeUnion([${validatorNames}], [${mappers}])` + + ')'; } - public visitUnionMap(_itemTypes: genspec.CodeName[]): string { - return this.baseExpression; + public visitUnionMap(itemTypes: genspec.CodeName[]): string { + const validatorNames = itemTypes.map(type => genspec.validatorName(type).fqn).join(', '); + const mappers = itemTypes.map(type => this.visitAtom(type)).join(', '); + + return `${CFN_PARSE}.FromCloudFormation.getMap(` + + `${CFN_PARSE}.FromCloudFormation.getTypeUnion([${validatorNames}], [${mappers}])` + + ')'; } - private deeperCopy(baseExpression: string): FromCloudFormationFactoryVisitor { - return new FromCloudFormationFactoryVisitor(baseExpression, false, this.cfnPropName, this.depth + 1); + public visitListOrAtom(scalarTypes: genspec.CodeName[], itemTypes: genspec.CodeName[]): any { + const scalarValidatorNames = scalarTypes.map(type => genspec.validatorName(type).fqn).join(', '); + const itemValidatorNames = itemTypes.map(type => genspec.validatorName(type).fqn).join(', '); + + const scalarTypesMappers = scalarTypes.map(type => this.visitAtom(type)).join(', '); + const scalarMapper = `${CFN_PARSE}.FromCloudFormation.getTypeUnion([${scalarValidatorNames}], [${scalarTypesMappers}])`; + + const itemTypeMappers = itemTypes.map(type => this.visitAtom(type)).join(', '); + const listMapper = `${CFN_PARSE}.FromCloudFormation.getArray(` + + `${CFN_PARSE}.FromCloudFormation.getTypeUnion([${itemValidatorNames}], [${itemTypeMappers}])` + + ')'; + + const scalarValidator = `${CORE}.unionValidator(${scalarValidatorNames})`; + const listValidator = `${CORE}.listValidator(${CORE}.unionValidator(${itemValidatorNames}))`; + + return `${CFN_PARSE}.FromCloudFormation.getTypeUnion([${scalarValidator}, ${listValidator}], [${scalarMapper}, ${listMapper}])`; } } @@ -619,10 +617,24 @@ export default class CodeGenerator { const propSpec = propSpecs[cfnName]; const simpleCfnPropAccessExpr = `properties.${cfnName}`; - const mapperExpression = genspec.typeDispatch(resource, propSpec, - new FromCloudFormationFactoryVisitor(simpleCfnPropAccessExpr, !propSpec.Required, cfnName)); - self.code.line(`${propName}: ${mapperExpression},`); + const deserializer = genspec.typeDispatch(resource, propSpec, new FromCloudFormationFactoryVisitor()); + const deserialized = `${deserializer}(${simpleCfnPropAccessExpr})`; + let valueExpression = propSpec.Required ? deserialized : `${simpleCfnPropAccessExpr} != null ? ${deserialized} : undefined`; + + if (schema.isTagPropertyName(cfnName)) { + // Properties that have names considered to denote tags + // have their type generated without a union with IResolvable. + // However, we can't possibly know that when generating the factory + // for that struct, and (in theory, at least) + // the same type can be used as the value of multiple properties, + // some of which do not have a tag-compatible name, + // so there is no way to pass allowReturningIResolvable=false correctly. + // Do the simple thing in that case, and just cast to any. + valueExpression += ' as any'; + } + + self.code.line(`${propName}: ${valueExpression},`); }); // close the return object brace this.code.unindent('};'); @@ -935,4 +947,4 @@ interface EmitPropertyProps { propName: string; spec: schema.Property; additionalDocs: string; -} +} \ No newline at end of file