diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 5b55d55f4c38a..ca3eec11b4ce0 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -484,7 +484,7 @@ iamUser.attachInlinePolicy(new iam.Policy(this, 'AllowBooks', { new iam.PolicyStatement({ actions: [ 'execute-api:Invoke' ], effect: iam.Effect.Allow, - resources: [ getBooks.methodArn() ] + resources: [ getBooks.methodArn ] }) ] })) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 41eb58d9b0a22..658dfcf3e6a85 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -14,6 +14,7 @@ --- + [Amazon Cognito](https://docs.aws.amazon.com/cognito/latest/developerguide/what-is-amazon-cognito.html) provides authentication, authorization, and user management for your web and mobile apps. Your users can sign in directly with a user name and password, or through a third party such as Facebook, Amazon, Google or Apple. @@ -319,6 +320,8 @@ Lambda triggers can either be specified as part of the `UserPool` initialization on the construct, as so - ```ts +import * as lambda from '@aws-cdk/aws-lambda'; + const authChallengeFn = new lambda.Function(this, 'authChallengeFn', { // ... }); @@ -566,7 +569,7 @@ const signInUrl = domain.signInUrl(client, { }) ``` -Exisiting domains can be imported into CDK apps using `UserPoolDomain.fromDomainName()` API +Existing domains can be imported into CDK apps using `UserPoolDomain.fromDomainName()` API ```ts const stack = new Stack(app, 'my-stack'); diff --git a/packages/@aws-cdk/aws-config/README.md b/packages/@aws-cdk/aws-config/README.md index c4dfa580b49e3..88224b7b5b63c 100644 --- a/packages/@aws-cdk/aws-config/README.md +++ b/packages/@aws-cdk/aws-config/README.md @@ -64,28 +64,30 @@ new CustomRule(this, 'CustomRule', { By default rules are triggered by changes to all [resources](https://docs.aws.amazon.com/config/latest/developerguide/resource-config-reference.html#supported-resources). -Use the `scopeToResource()`, `scopeToResources()` or `scopeToTag()` APIs to restrict +Use the `Scope` APIs (`fromResource()`, `fromResources()` or `fromTag()`) to restrict the scope of both managed and custom rules: ```ts -const sshRule = new ManagedRule(this, 'SSH', { - identifier: 'INCOMING_SSH_DISABLED' -}); +import * as config from '@aws-cdk/aws-config'; -// Restrict to a specific security group -rule.scopeToResource('AWS::EC2::SecurityGroup', 'sg-1234567890abcdefgh'); +const sshRule = new config.ManagedRule(this, 'SSH', { + identifier: 'INCOMING_SSH_DISABLED', + scope: config.Scope.fromResource(config.ResourceType.EC2_SECURITY_GROUP, 'sg-1234567890abcdefgh'), // restrict to specific security group +}); -const customRule = new CustomRule(this, 'CustomRule', { +const customRule = new config.CustomRule(this, 'CustomRule', { lambdaFunction: myFn, configurationChanges: true + scope: config.Scope.fromResources([config.ResourceType.CLOUDFORMATION_STACK, config.Resource.S3_BUCKET]), // restrict to all CloudFormation stacks and S3 buckets }); -// Restrict to a specific tag -customRule.scopeToTag('Cost Center', 'MyApp'); +const customRule = new config.CustomRule(this, 'CustomRule', { + lambdaFunction: myFn, + configurationChanges: true + scope: config.Scope.fromTag('Cost Center', 'MyApp'), // restrict to a specific tag +}); ``` -Only one type of scope restriction can be added to a rule (the last call to `scopeToXxx()` sets the scope). - #### Events To define Amazon CloudWatch event rules, use the `onComplianceChange()` or `onReEvaluationStatus()` methods: @@ -116,10 +118,9 @@ const fn = new lambda.Function(this, 'CustomFunction', { const customRule = new config.CustomRule(this, 'Custom', { configurationChanges: true, lambdaFunction: fn, + scope: config.Scope.fromResource([config.Scope.EC2_INSTANCE]), }); -customRule.scopeToResource('AWS::EC2::Instance'); - // A rule to detect stack drifts const driftRule = new config.CloudFormationStackDriftDetectionCheck(this, 'Drift'); diff --git a/packages/@aws-cdk/aws-config/lib/managed-rules.ts b/packages/@aws-cdk/aws-config/lib/managed-rules.ts index 69ddd7463aac5..19ac39406e281 100644 --- a/packages/@aws-cdk/aws-config/lib/managed-rules.ts +++ b/packages/@aws-cdk/aws-config/lib/managed-rules.ts @@ -2,7 +2,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as sns from '@aws-cdk/aws-sns'; import { Duration, Lazy, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { ManagedRule, RuleProps } from './rule'; +import { ManagedRule, ResourceType, RuleProps, Scope } from './rule'; /** * Construction properties for a AccessKeysRotated @@ -82,7 +82,7 @@ export class CloudFormationStackDriftDetectionCheck extends ManagedRule { }, }); - this.scopeToResource('AWS::CloudFormation::Stack', props.ownStackOnly ? Stack.of(this).stackId : undefined); + this.scope = Scope.fromResource( ResourceType.CLOUDFORMATION_STACK, props.ownStackOnly ? Stack.of(this).stackId : undefined ); this.role = props.role || new iam.Role(this, 'Role', { assumedBy: new iam.ServicePrincipal('config.amazonaws.com'), @@ -126,8 +126,7 @@ export class CloudFormationStackNotificationCheck extends ManagedRule { (params, topic, idx) => ({ ...params, [`snsTopic${idx + 1}`]: topic.topicArn }), {}, ), + scope: Scope.fromResources([ResourceType.CLOUDFORMATION_STACK]), }); - - this.scopeToResource('AWS::CloudFormation::Stack'); } } diff --git a/packages/@aws-cdk/aws-config/lib/rule.ts b/packages/@aws-cdk/aws-config/lib/rule.ts index 9984a22fdac61..673f686f3c69c 100644 --- a/packages/@aws-cdk/aws-config/lib/rule.ts +++ b/packages/@aws-cdk/aws-config/lib/rule.ts @@ -110,49 +110,45 @@ abstract class RuleNew extends RuleBase { */ public abstract readonly configRuleComplianceType: string; - protected scope?: CfnConfigRule.ScopeProperty; + protected scope?: Scope; protected isManaged?: boolean; protected isCustomWithChanges?: boolean; +} - /** - * Restrict scope of changes to a specific resource. - * - * @see https://docs.aws.amazon.com/config/latest/developerguide/resource-config-reference.html#supported-resources - * - * @param type the resource type - * @param identifier the resource identifier - */ - public scopeToResource(type: string, identifier?: string) { - this.scope = { - complianceResourceId: identifier, - complianceResourceTypes: [type], - }; +/** + * Determines which resources trigger an evaluation of an AWS Config rule. + */ +export class Scope { + /** restricts scope of changes to a specific resource type or resource identifier */ + public static fromResource(resourceType: ResourceType, resourceId?: string) { + return new Scope(resourceId, [resourceType]); } - - /** - * Restrict scope of changes to specific resource types. - * - * @see https://docs.aws.amazon.com/config/latest/developerguide/resource-config-reference.html#supported-resources - * - * @param types resource types - */ - public scopeToResources(...types: string[]) { - this.scope = { - complianceResourceTypes: types, - }; + /** restricts scope of changes to specific resource types */ + public static fromResources(resourceTypes: ResourceType[]) { + return new Scope(undefined, resourceTypes); + } + /** restricts scope of changes to a specific tag */ + public static fromTag(key: string, value?: string) { + return new Scope(undefined, undefined, key, value); } - /** - * Restrict scope of changes to a specific tag. - * - * @param key the tag key - * @param value the tag value - */ - public scopeToTag(key: string, value?: string) { - this.scope = { - tagKey: key, - tagValue: value, - }; + /** Resource types that will trigger evaluation of a rule */ + public readonly resourceTypes?: ResourceType[]; + + /** ID of the only AWS resource that will trigger evaluation of a rule */ + public readonly resourceId?: string; + + /** tag key applied to resources that will trigger evaluation of a rule */ + public readonly key?: string; + + /** tag value applied to resources that will trigger evaluation of a rule */ + public readonly value?: string; + + private constructor(resourceId?: string, resourceTypes?: ResourceType[], tagKey?: string, tagValue?: string) { + this.resourceTypes = resourceTypes; + this.resourceId = resourceId; + this.key = tagKey; + this.value = tagValue; } } @@ -217,7 +213,14 @@ export interface RuleProps { * * @default MaximumExecutionFrequency.TWENTY_FOUR_HOURS */ - readonly maximumExecutionFrequency?: MaximumExecutionFrequency + readonly maximumExecutionFrequency?: MaximumExecutionFrequency; + + /** + * Defines which resources trigger an evaluation for an AWS Config rule. + * + * @default - evaluations for the rule are triggered when any resource in the recording group changes. + */ + readonly scope?: Scope; } /** @@ -255,12 +258,14 @@ export class ManagedRule extends RuleNew { physicalName: props.configRuleName, }); + this.scope = props.scope; + const rule = new CfnConfigRule(this, 'Resource', { configRuleName: this.physicalName, description: props.description, inputParameters: props.inputParameters, maximumExecutionFrequency: props.maximumExecutionFrequency, - scope: Lazy.anyValue({ produce: () => this.scope }), + scope: Lazy.anyValue({ produce: () => renderScope(this.scope) }), // scope can use values such as stack id (see CloudFormationStackDriftDetectionCheck) source: { owner: 'AWS', sourceIdentifier: props.identifier, @@ -327,6 +332,7 @@ export class CustomRule extends RuleNew { } const sourceDetails: any[] = []; + this.scope = props.scope; if (props.configurationChanges) { sourceDetails.push({ @@ -365,7 +371,7 @@ export class CustomRule extends RuleNew { description: props.description, inputParameters: props.inputParameters, maximumExecutionFrequency: props.maximumExecutionFrequency, - scope: Lazy.anyValue({ produce: () => this.scope }), + scope: Lazy.anyValue({ produce: () => renderScope(this.scope) }), // scope can use values such as stack id (see CloudFormationStackDriftDetectionCheck) source: { owner: 'CUSTOM_LAMBDA', sourceDetails, @@ -383,3 +389,211 @@ export class CustomRule extends RuleNew { } } } + +/** + * Resources types that are supported by AWS Config + * @see https://docs.aws.amazon.com/config/latest/developerguide/resource-config-reference.html + */ +export class ResourceType { + /** API Gateway Stage */ + public static readonly APIGATEWAY_STAGE = new ResourceType('AWS::ApiGateway::Stage'); + /** API Gatewayv2 Stage */ + public static readonly APIGATEWAYV2_STAGE = new ResourceType('AWS::ApiGatewayV2::Stage'); + /** API Gateway REST API */ + public static readonly APIGATEWAY_REST_API = new ResourceType('AWS::ApiGateway::RestApi'); + /** API Gatewayv2 API */ + public static readonly APIGATEWAYV2_API = new ResourceType('AWS::ApiGatewayV2::Api'); + /** Amazon CloudFront Distribution */ + public static readonly CLOUDFRONT_DISTRIBUTION = new ResourceType('AWS::CloudFront::Distribution'); + /** Amazon CloudFront streaming distribution */ + public static readonly CLOUDFRONT_STREAMING_DISTRIBUTION = new ResourceType('AWS::CloudFront::StreamingDistribution'); + /** Amazon CloudWatch Alarm */ + public static readonly CLOUDWATCH_ALARM = new ResourceType('AWS::CloudWatch::Alarm'); + /** Amazon DynamoDB Table */ + public static readonly DYNAMODB_TABLE = new ResourceType('AWS::DynamoDB::Table'); + /** Elastic Block Store (EBS) volume */ + public static readonly EBS_VOLUME = new ResourceType('AWS::EC2::Volume'); + /** EC2 host */ + public static readonly EC2_HOST = new ResourceType('AWS::EC2::Host'); + /** EC2 Elastic IP */ + public static readonly EC2_EIP = new ResourceType('AWS::EC2::EIP'); + /** EC2 instance */ + public static readonly EC2_INSTANCE = new ResourceType('AWS::EC2::Instance'); + /** EC2 security group */ + public static readonly EC2_SECURITY_GROUP = new ResourceType('AWS::EC2::SecurityGroup'); + /** EC2 NAT gateway */ + public static readonly EC2_NAT_GATEWAY = new ResourceType('AWS::EC2::NatGateway'); + /** EC2 Egress only internet gateway */ + public static readonly EC2_EGRESS_ONLY_INTERNET_GATEWAY = new ResourceType('AWS::EC2::EgressOnlyInternetGateway'); + /** EC2 flow log */ + public static readonly EC2_FLOW_LOG = new ResourceType('AWS::EC2::FlowLog'); + /** EC2 VPC endpoint */ + public static readonly EC2_VPC_ENDPOINT = new ResourceType('AWS::EC2::VPCEndpoint'); + /** EC2 VPC endpoint service */ + public static readonly EC2_VPC_ENDPOINT_SERVICE = new ResourceType('AWS::EC2::VPCEndpointService'); + /** EC2 VPC peering connection */ + public static readonly EC2_VPC_PEERING_CONNECTION = new ResourceType('AWS::EC2::VPCPeeringConnection'); + /** Amazon ElasticSearch domain */ + public static readonly ELASTICSEARCH_DOMAIN = new ResourceType('AWS::Elasticsearch::Domain'); + /** Amazon QLDB ledger */ + public static readonly QLDB_LEDGER = new ResourceType('AWS::QLDB::Ledger'); + /** Amazon Redshift cluster */ + public static readonly REDSHIFT_CLUSTER = new ResourceType('AWS::Redshift::Cluster'); + /** Amazon Redshift cluster parameter group */ + public static readonly REDSHIFT_CLUSTER_PARAMETER_GROUP = new ResourceType('AWS::Redshift::ClusterParameterGroup'); + /** Amazon Redshift cluster security group */ + public static readonly REDSHIFT_CLUSTER_SECURITY_GROUP = new ResourceType('AWS::Redshift::ClusterSecurityGroup'); + /** Amazon Redshift cluster snapshot */ + public static readonly REDSHIFT_CLUSTER_SNAPSHOT = new ResourceType('AWS::Redshift::ClusterSnapshot'); + /** Amazon Redshift cluster subnet group */ + public static readonly REDSHIFT_CLUSTER_SUBNET_GROUP = new ResourceType('AWS::Redshift::ClusterSubnetGroup'); + /** Amazon Redshift event subscription */ + public static readonly REDSHIFT_EVENT_SUBSCRIPTION = new ResourceType('AWS::Redshift::EventSubscription'); + /** Amazon RDS database instance */ + public static readonly RDS_DB_INSTANCE = new ResourceType('AWS::RDS::DBInstance'); + /** Amazon RDS database security group */ + public static readonly RDS_DB_SECURITY_GROUP = new ResourceType('AWS::RDS::DBSecurityGroup'); + /** Amazon RDS database snapshot */ + public static readonly RDS_DB_SNAPSHOT = new ResourceType('AWS::RDS::DBSnapshot'); + /** Amazon RDS database subnet group */ + public static readonly RDS_DB_SUBNET_GROUP = new ResourceType('AWS::RDS::DBSubnetGroup'); + /** Amazon RDS event subscription */ + public static readonly RDS_EVENT_SUBSCRIPTION = new ResourceType('AWS::RDS::EventSubscription'); + /** Amazon RDS database cluster */ + public static readonly RDS_DB_CLUSTER = new ResourceType('AWS::RDS::DBCluster'); + /** Amazon RDS database cluster snapshot */ + public static readonly RDS_DB_CLUSTER_SNAPSHOT = new ResourceType('AWS::RDS::DBClusterSnapshot'); + /** Amazon SQS queue */ + public static readonly SQS_QUEUE = new ResourceType('AWS::SQS::Queue'); + /** Amazon SNS topic */ + public static readonly SNS_TOPIC = new ResourceType('AWS::SNS::Topic'); + /** Amazon S3 bucket */ + public static readonly S3_BUCKET = new ResourceType('AWS::S3::Bucket'); + /** Amazon S3 account public access block */ + public static readonly S3_ACCOUNT_PUBLIC_ACCESS_BLOCK = new ResourceType('AWS::S3::AccountPublicAccessBlock'); + /** Amazon EC2 customer gateway */ + public static readonly EC2_CUSTOMER_GATEWAY = new ResourceType('AWS::EC2::CustomerGateway'); + /** Amazon EC2 internet gateway */ + public static readonly EC2_INTERNET_GATEWAY = new ResourceType('AWS::EC2::CustomerGateway'); + /** Amazon EC2 network ACL */ + public static readonly EC2_NETWORK_ACL = new ResourceType('AWS::EC2::NetworkAcl'); + /** Amazon EC2 route table */ + public static readonly EC2_ROUTE_TABLE = new ResourceType('AWS::EC2::RouteTable'); + /** Amazon EC2 subnet table */ + public static readonly EC2_SUBNET = new ResourceType('AWS::EC2::Subnet'); + /** Amazon EC2 VPC */ + public static readonly EC2_VPC = new ResourceType('AWS::EC2::VPC'); + /** Amazon EC2 VPN connection */ + public static readonly EC2_VPN_CONNECTION = new ResourceType('AWS::EC2::VPNConnection'); + /** Amazon EC2 VPN gateway */ + public static readonly EC2_VPN_GATEWAY = new ResourceType('AWS::EC2::VPNGateway'); + /** AWS Auto Scaling group */ + public static readonly AUTO_SCALING_GROUP = new ResourceType('AWS::AutoScaling::AutoScalingGroup'); + /** AWS Auto Scaling launch configuration */ + public static readonly AUTO_SCALING_LAUNCH_CONFIGURATION = new ResourceType('AWS::AutoScaling::LaunchConfiguration'); + /** AWS Auto Scaling policy */ + public static readonly AUTO_SCALING_POLICY = new ResourceType('AWS::AutoScaling::ScalingPolicy'); + /** AWS Auto Scaling scheduled action */ + public static readonly AUTO_SCALING_SCHEDULED_ACTION = new ResourceType('AWS::AutoScaling::ScheduledAction'); + /** AWS Certificate manager certificate */ + public static readonly ACM_CERTIFICATE = new ResourceType('AWS::ACM::Certificate'); + /** AWS CloudFormation stack */ + public static readonly CLOUDFORMATION_STACK = new ResourceType('AWS::CloudFormation::Stack'); + /** AWS CloudTrail trail */ + public static readonly CLOUDTRAIL_TRAIL = new ResourceType('AWS::CloudTrail::Trail'); + /** AWS CodeBuild project */ + public static readonly CODEBUILD_PROJECT = new ResourceType('AWS::CodeBuild::Project'); + /** AWS CodePipeline pipeline */ + public static readonly CODEPIPELINE_PIPELINE = new ResourceType('AWS::CodePipeline::Pipeline'); + /** AWS Elastic Beanstalk (EB) application */ + public static readonly ELASTIC_BEANSTALK_APPLICATION = new ResourceType('AWS::ElasticBeanstalk::Application'); + /** AWS Elastic Beanstalk (EB) application version */ + public static readonly ELASTIC_BEANSTALK_APPLICATION_VERSION = new ResourceType('AWS::ElasticBeanstalk::ApplicationVersion'); + /** AWS Elastic Beanstalk (EB) environment */ + public static readonly ELASTIC_BEANSTALK_ENVIRONMENT = new ResourceType('AWS::ElasticBeanstalk::Environment'); + /** AWS IAM user */ + public static readonly IAM_USER = new ResourceType('AWS::IAM::User'); + /** AWS IAM group */ + public static readonly IAM_GROUP = new ResourceType('AWS::IAM::Group'); + /** AWS IAM role */ + public static readonly IAM_ROLE = new ResourceType('AWS::IAM::Role'); + /** AWS IAM policy */ + public static readonly IAM_POLICY = new ResourceType('AWS::IAM::Policy'); + /** AWS KMS Key */ + public static readonly KMS_KEY = new ResourceType('AWS::KMS::Key'); + /** AWS Lambda function */ + public static readonly LAMBDA_FUNCTION = new ResourceType('AWS::Lambda::Function'); + /**AWS Secrets Manager secret */ + public static readonly SECRETS_MANAGER_SECRET = new ResourceType('AWS::SecretsManager::Secret'); + /** AWS Service Catalog CloudFormation product */ + public static readonly SERVICE_CATALOG_CLOUDFORMATION_PRODUCT = new ResourceType('AWS::ServiceCatalog::CloudFormationProduct'); + /** AWS Service Catalog CloudFormation provisioned product */ + public static readonly SERVICE_CATALOG_CLOUDFORMATION_PROVISIONED_PRODUCT = new ResourceType( + 'AWS::ServiceCatalog::CloudFormationProvisionedProduct'); + /** AWS Service Catalog portfolio */ + public static readonly SERVICE_CATALOG_PORTFOLIO = new ResourceType('AWS::ServiceCatalog::Portfolio'); + /** AWS Shield protection */ + public static readonly SHIELD_PROTECTION = new ResourceType('AWS::Shield::Protection'); + /** AWS Shield regional protection */ + public static readonly SHIELD_REGIONAL_PROTECTION = new ResourceType('AWS::ShieldRegional::Protection'); + /** AWS Systems Manager managed instance inventory */ + public static readonly SYSTEMS_MANAGER_MANAGED_INSTANCE_INVENTORY = new ResourceType('AWS::SSM::ManagedInstanceInventory'); + /** AWS Systems Manager patch compliance */ + public static readonly SYSTEMS_MANAGER_PATCH_COMPLIANCE = new ResourceType('AWS::SSM::PatchCompliance'); + /** AWS Systems Manager association compliance */ + public static readonly SYSTEMS_MANAGER_ASSOCIATION_COMPLIANCE = new ResourceType('AWS::SSM::AssociationCompliance'); + /** AWS Systems Manager file data */ + public static readonly SYSTEMS_MANAGER_FILE_DATA = new ResourceType('AWS::SSM::FileData'); + /** AWS WAF rate based rule */ + public static readonly WAF_RATE_BASED_RULE = new ResourceType('AWS::WAF::RateBasedRule'); + /** AWS WAF rule */ + public static readonly WAF_RULE = new ResourceType('AWS::WAF::Rule'); + /** AWS WAF web ACL */ + public static readonly WAF_WEB_ACL = new ResourceType('AWS::WAF::WebACL'); + /** AWS WAF rule group */ + public static readonly WAF_RULE_GROUP = new ResourceType('AWS::WAF::RuleGroup'); + /** AWS WAF regional rate based rule */ + public static readonly WAF_REGIONAL_RATE_BASED_RULE = new ResourceType('AWS::WAFRegional::RateBasedRule'); + /** AWS WAF regional rule */ + public static readonly WAF_REGIONAL_RULE = new ResourceType('AWS::WAFRegional::Rule'); + /** AWS WAF web ACL */ + public static readonly WAF_REGIONAL_WEB_ACL = new ResourceType('AWS::WAFRegional::WebACL'); + /** AWS WAF regional rule group */ + public static readonly WAF_REGIONAL_RULE_GROUP = new ResourceType('AWS::WAFRegional::RuleGroup'); + /** AWS WAFv2 web ACL */ + public static readonly WAFV2_WEB_ACL = new ResourceType('AWS::WAFv2::WebACL'); + /** AWS WAFv2 rule group */ + public static readonly WAFV2_RULE_GROUP = new ResourceType('AWS::WAFv2::RuleGroup'); + /** AWS WAFv2 managed rule set */ + public static readonly WAFV2_MANAGED_RULE_SET = new ResourceType('AWS::WAFv2::ManagedRuleSet'); + /** AWS X-Ray encryption configuration */ + public static readonly XRAY_ENCRYPTION_CONFIGURATION = new ResourceType('AWS::XRay::EncryptionConfig'); + /** AWS ELB classic load balancer */ + public static readonly ELB_LOAD_BALANCER = new ResourceType('AWS::ElasticLoadBalancing::LoadBalancer'); + /** AWS ELBv2 network load balancer or AWS ELBv2 application load balancer */ + public static readonly ELBV2_LOAD_BALANCER = new ResourceType('AWS::ElasticLoadBalancingV2::LoadBalancer'); + + /** A custom resource type to support future cases. */ + public static of(type: string): ResourceType { + return new ResourceType(type); + } + + /** + * Valid value of resource type. + */ + public readonly complianceResourceType: string; + + private constructor(type: string) { + this.complianceResourceType = type; + } + +} + +function renderScope(scope?: Scope): CfnConfigRule.ScopeProperty | undefined { + return scope ? { + complianceResourceId: scope.resourceId, + complianceResourceTypes: scope.resourceTypes?.map(resource => resource.complianceResourceType), + tagKey: scope.key, + tagValue: scope.value, + } : undefined; +} diff --git a/packages/@aws-cdk/aws-config/test/integ.rule.lit.ts b/packages/@aws-cdk/aws-config/test/integ.rule.lit.ts index 5c15530c6c87a..a6da1c44d622c 100644 --- a/packages/@aws-cdk/aws-config/test/integ.rule.lit.ts +++ b/packages/@aws-cdk/aws-config/test/integ.rule.lit.ts @@ -18,13 +18,12 @@ class ConfigStack extends cdk.Stack { runtime: lambda.Runtime.NODEJS_10_X, }); - const customRule = new config.CustomRule(this, 'Custom', { + new config.CustomRule(this, 'Custom', { configurationChanges: true, lambdaFunction: fn, + scope: config.Scope.fromResources([config.ResourceType.EC2_INSTANCE]), }); - customRule.scopeToResource('AWS::EC2::Instance'); - // A rule to detect stacks drifts const driftRule = new config.CloudFormationStackDriftDetectionCheck(this, 'Drift'); diff --git a/packages/@aws-cdk/aws-config/test/integ.scoped-rule.ts b/packages/@aws-cdk/aws-config/test/integ.scoped-rule.ts index aee8392f402f0..33f14ab96dc86 100644 --- a/packages/@aws-cdk/aws-config/test/integ.scoped-rule.ts +++ b/packages/@aws-cdk/aws-config/test/integ.scoped-rule.ts @@ -12,11 +12,10 @@ const fn = new lambda.Function(stack, 'CustomFunction', { runtime: lambda.Runtime.NODEJS_10_X, }); -const customRule = new config.CustomRule(stack, 'Custom', { +new config.CustomRule(stack, 'Custom', { lambdaFunction: fn, periodic: true, + scope: config.Scope.fromResources([config.ResourceType.EC2_INSTANCE]), }); -customRule.scopeToResource('AWS::EC2::Instance'); - app.synth(); diff --git a/packages/@aws-cdk/aws-config/test/test.rule.ts b/packages/@aws-cdk/aws-config/test/test.rule.ts index c13dacf2c8a3f..a738edae663aa 100644 --- a/packages/@aws-cdk/aws-config/test/test.rule.ts +++ b/packages/@aws-cdk/aws-config/test/test.rule.ts @@ -140,12 +140,12 @@ export = { 'scope to resource'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const rule = new config.ManagedRule(stack, 'Rule', { - identifier: 'AWS_SUPER_COOL', - }); // WHEN - rule.scopeToResource('AWS::EC2::Instance', 'i-1234'); + new config.ManagedRule(stack, 'Rule', { + identifier: 'AWS_SUPER_COOL', + scope: config.Scope.fromResource(config.ResourceType.EC2_INSTANCE, 'i-1234'), + }); // THEN expect(stack).to(haveResource('AWS::Config::ConfigRule', { @@ -163,12 +163,12 @@ export = { 'scope to resources'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const rule = new config.ManagedRule(stack, 'Rule', { - identifier: 'AWS_SUPER_COOL', - }); // WHEN - rule.scopeToResources('AWS::S3::Bucket', 'AWS::CloudFormation::Stack'); + new config.ManagedRule(stack, 'Rule', { + identifier: 'AWS_SUPER_COOL', + scope: config.Scope.fromResources([config.ResourceType.S3_BUCKET, config.ResourceType.CLOUDFORMATION_STACK]), + }); // THEN expect(stack).to(haveResource('AWS::Config::ConfigRule', { @@ -186,12 +186,12 @@ export = { 'scope to tag'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const rule = new config.ManagedRule(stack, 'Rule', { - identifier: 'RULE', - }); // WHEN - rule.scopeToTag('key', 'value'); + new config.ManagedRule(stack, 'Rule', { + identifier: 'RULE', + scope: config.Scope.fromTag('key', 'value'), + }); // THEN expect(stack).to(haveResource('AWS::Config::ConfigRule', { @@ -213,14 +213,12 @@ export = { runtime: lambda.Runtime.NODEJS_10_X, }); - // WHEN - const rule = new config.CustomRule(stack, 'Rule', { + // THEN + test.doesNotThrow(() => new config.CustomRule(stack, 'Rule', { lambdaFunction: fn, periodic: true, - }); - - // THEN - test.doesNotThrow(() => rule.scopeToResource('resource')); + scope: config.Scope.fromResources([config.ResourceType.of('resource')]), + })); test.done(); }, diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index 01672fae19a40..0a3114dc34d27 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -894,6 +894,11 @@ new ec2.FlowLog(this, 'FlowLog', { resourceType: ec2.FlowLogResourceType.fromVpc(vpc), destination: ec2.FlowLogDestination.toS3(bucket) }); + +new ec2.FlowLog(this, 'FlowLogWithKeyPrefix', { + resourceType: ec2.FlowLogResourceType.fromVpc(vpc), + destination: ec2.FlowLogDestination.toS3(bucket, 'prefix/') +}); ``` ## User Data diff --git a/packages/@aws-cdk/aws-ec2/lib/instance-types.ts b/packages/@aws-cdk/aws-ec2/lib/instance-types.ts index d6ce443bb6291..18875b32671da 100644 --- a/packages/@aws-cdk/aws-ec2/lib/instance-types.ts +++ b/packages/@aws-cdk/aws-ec2/lib/instance-types.ts @@ -193,6 +193,16 @@ export enum InstanceClass { */ COMPUTE5_NVME_DRIVE = 'c5d', + /** + * Compute optimized instances based on AMD EPYC, 5th generation. + */ + COMPUTE5_AMD = 'c5a', + + /** + * Compute optimized instances based on AMD EPYC, 5th generation + */ + C5A = 'c5a', + /** * Compute optimized instances with local NVME drive, 5th generation */ diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-flow-logs.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-flow-logs.ts index bcfb8602aa595..0edf1867236a7 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-flow-logs.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-flow-logs.ts @@ -129,10 +129,11 @@ export abstract class FlowLogDestination { /** * Use S3 as the destination */ - public static toS3(bucket?: s3.IBucket): FlowLogDestination { + public static toS3(bucket?: s3.IBucket, keyPrefix?: string): FlowLogDestination { return new S3Destination({ logDestinationType: FlowLogDestinationType.S3, s3Bucket: bucket, + keyPrefix, }); } @@ -175,6 +176,13 @@ export interface FlowLogDestinationConfig { * @default - undefined */ readonly s3Bucket?: s3.IBucket; + + /** + * S3 bucket key prefix to publish the flow logs to + * + * @default - undefined + */ + readonly keyPrefix?: string; } /** @@ -198,6 +206,7 @@ class S3Destination extends FlowLogDestination { return { logDestinationType: FlowLogDestinationType.S3, s3Bucket, + keyPrefix: this.props.keyPrefix, }; } } @@ -344,6 +353,11 @@ export class FlowLog extends FlowLogBase { */ public readonly bucket?: s3.IBucket; + /** + * S3 bucket key prefix to publish the flow logs under + */ + readonly keyPrefix?: string; + /** * The iam role used to publish logs to CloudWatch */ @@ -365,6 +379,12 @@ export class FlowLog extends FlowLogBase { this.logGroup = destinationConfig.logGroup; this.bucket = destinationConfig.s3Bucket; this.iamRole = destinationConfig.iamRole; + this.keyPrefix = destinationConfig.keyPrefix; + + let logDestination: string | undefined = undefined; + if (this.bucket) { + logDestination = this.keyPrefix ? this.bucket.arnForObjects(this.keyPrefix) : this.bucket.bucketArn; + } const flowLog = new CfnFlowLog(this, 'FlowLog', { deliverLogsPermissionArn: this.iamRole ? this.iamRole.roleArn : undefined, @@ -375,7 +395,7 @@ export class FlowLog extends FlowLogBase { trafficType: props.trafficType ? props.trafficType : FlowLogTrafficType.ALL, - logDestination: this.bucket ? this.bucket.bucketArn : undefined, + logDestination, }); this.flowLogId = flowLog.ref; diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpc-flow-logs.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.vpc-flow-logs.expected.json index cedf8d1bdea84..ab9eb13b2c415 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpc-flow-logs.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpc-flow-logs.expected.json @@ -550,6 +550,37 @@ ] } }, + "VPCFlowLogsS3KeyPrefixFlowLogB57F1746": { + "Type": "AWS::EC2::FlowLog", + "Properties": { + "ResourceId": { + "Ref": "VPCB9E5F0B4" + }, + "ResourceType": "VPC", + "TrafficType": "ALL", + "LogDestination": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/prefix/" + ] + ] + }, + "LogDestinationType": "s3", + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/VPC" + } + ] + } + }, "FlowLogsCWIAMRole017AD736": { "Type": "AWS::IAM::Role", "Properties": { @@ -634,6 +665,11 @@ "Ref": "FlowLogsCWLogGroup0398E8F8" } } + }, + "Bucket83908E77": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpc-flow-logs.ts b/packages/@aws-cdk/aws-ec2/test/integ.vpc-flow-logs.ts index debba35a82a3e..c9b87c51bf7fd 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpc-flow-logs.ts +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpc-flow-logs.ts @@ -1,5 +1,6 @@ /// !cdk-integ * -import { App, Stack, StackProps } from '@aws-cdk/core'; +import * as s3 from '@aws-cdk/aws-s3'; +import { App, RemovalPolicy, Stack, StackProps } from '@aws-cdk/core'; import { FlowLog, FlowLogDestination, FlowLogResourceType, Vpc } from '../lib'; const app = new App(); @@ -17,6 +18,14 @@ class TestStack extends Stack { vpc.addFlowLog('FlowLogsS3', { destination: FlowLogDestination.toS3(), }); + + const bucket = new s3.Bucket(this, 'Bucket', { + removalPolicy: RemovalPolicy.DESTROY, + }); + + vpc.addFlowLog('FlowLogsS3KeyPrefix', { + destination: FlowLogDestination.toS3(bucket, 'prefix/'), + }); } } diff --git a/packages/@aws-cdk/aws-ec2/test/vpc-flow-logs.test.ts b/packages/@aws-cdk/aws-ec2/test/vpc-flow-logs.test.ts index 965c538bab5a0..8d341924c3bad 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc-flow-logs.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc-flow-logs.test.ts @@ -79,6 +79,26 @@ nodeunitShim({ })); test.done(); }, + 'with s3 as the destination, allows use of key prefix'(test: Test) { + const stack = getTestStack(); + + new FlowLog(stack, 'FlowLogs', { + resourceType: FlowLogResourceType.fromNetworkInterfaceId('eni-123456'), + destination: FlowLogDestination.toS3( + new s3.Bucket(stack, 'TestBucket', { + bucketName: 'testbucket', + }), + 'FlowLogs/', + ), + }); + + expect(stack).notTo(haveResource('AWS::Logs::LogGroup')); + expect(stack).notTo(haveResource('AWS::IAM::Role')); + expect(stack).to(haveResource('AWS::S3::Bucket', { + BucketName: 'testbucket', + })); + test.done(); + }, 'with s3 as the destination and all the defaults set, it successfully creates all the resources'( test: Test, ) { diff --git a/packages/@aws-cdk/aws-lambda-python/README.md b/packages/@aws-cdk/aws-lambda-python/README.md index 8f0387cb2e430..97229dc225fe3 100644 --- a/packages/@aws-cdk/aws-lambda-python/README.md +++ b/packages/@aws-cdk/aws-lambda-python/README.md @@ -32,11 +32,38 @@ All other properties of `lambda.Function` are supported, see also the [AWS Lambd ### Module Dependencies -If `requirements.txt` exists at the entry path, the construct will handle installing +If `requirements.txt` or `Pipfile` exists at the entry path, the construct will handle installing all required modules in a [Lambda compatible Docker container](https://hub.docker.com/r/amazon/aws-sam-cli-build-image-python3.7) according to the `runtime`. + +**Lambda with a requirements.txt** ``` . ├── lambda_function.py # exports a function named 'handler' ├── requirements.txt # has to be present at the entry path ``` + +**Lambda with a Pipfile** +``` +. +├── lambda_function.py # exports a function named 'handler' +├── Pipfile # has to be present at the entry path +├── Pipfile.lock # your lock file +``` + +**Lambda Layer Support** + +You may create a python-based lambda layer with `PythonLayerVersion`. If `PythonLayerVersion` detects a `requirements.txt` +or `Pipfile` at the entry path, then `PythonLayerVersion` will include the dependencies inline with your code in the +layer. + +```ts +new lambda.PythonFunction(this, 'MyFunction', { + entry: '/path/to/my/function', + layers: [ + new lambda.PythonLayerVersion(this, 'MyLayer', { + entry: '/path/to/my/layer', // point this to your library's directory + }), + ], +}); +``` diff --git a/packages/@aws-cdk/aws-lambda-python/lib/Dockerfile.dependencies b/packages/@aws-cdk/aws-lambda-python/lib/Dockerfile.dependencies new file mode 100644 index 0000000000000..4d02d028b187b --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/lib/Dockerfile.dependencies @@ -0,0 +1,18 @@ +# The correct AWS SAM build image based on the runtime of the function will be +# passed as build arg. The default allows to do `docker build .` when testing. +ARG IMAGE=amazon/aws-sam-cli-build-image-python3.7 +FROM $IMAGE + +# Ensure rsync is installed +RUN yum -q list installed rsync &>/dev/null || yum install -y rsync + +# Install pipenv so we can create a requirements.txt if we detect pipfile +RUN pip install pipenv + +# Install the dependencies in a cacheable layer +WORKDIR /var/dependencies +COPY Pipfile* requirements.tx[t] ./ +RUN [ -f 'Pipfile' ] && pipenv lock -r >requirements.txt; \ + [ -f 'requirements.txt' ] && pip install -r requirements.txt -t .; + +CMD [ "python" ] diff --git a/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts b/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts index 15d3126c48067..937d420397773 100644 --- a/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts +++ b/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts @@ -3,6 +3,16 @@ import * as path from 'path'; import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; +/** + * Dependency files to exclude from the asset hash. + */ +export const DEPENDENCY_EXCLUDES = ['*.pyc']; + +/** + * The location in the image that the bundler image caches dependencies. + */ +export const BUNDLER_DEPENDENCIES_CACHE = '/var/dependencies'; + /** * Options for bundling */ @@ -16,29 +26,44 @@ export interface BundlingOptions { * The runtime of the lambda function */ readonly runtime: lambda.Runtime; + + /** + * Output path suffix ('python' for a layer, '.' otherwise) + */ + readonly outputPathSuffix: string; } /** * Produce bundled Lambda asset code */ export function bundle(options: BundlingOptions): lambda.AssetCode { - // Bundling image derived from runtime bundling image (AWS SAM docker image) - const image = cdk.BundlingDockerImage.fromAsset(__dirname, { - buildArgs: { - IMAGE: options.runtime.bundlingDockerImage.image, - }, - }); - - let installer = options.runtime === lambda.Runtime.PYTHON_2_7 ? Installer.PIP : Installer.PIP3; + const { entry, runtime, outputPathSuffix } = options; - let hasRequirements = fs.existsSync(path.join(options.entry, 'requirements.txt')); + const hasDeps = hasDependencies(entry); - let depsCommand = chain([ - hasRequirements ? `${installer} install -r requirements.txt -t ${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}` : '', - `rsync -r . ${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}`, + const depsCommand = chain([ + hasDeps ? `rsync -r ${BUNDLER_DEPENDENCIES_CACHE}/. ${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}/${outputPathSuffix}` : '', + `rsync -r . ${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}/${outputPathSuffix}`, ]); - return lambda.Code.fromAsset(options.entry, { + // Determine which dockerfile to use. When dependencies are present, we use a + // Dockerfile that can create a cacheable layer. We can't use this Dockerfile + // if there aren't dependencies or the Dockerfile will complain about missing + // sources. + const dockerfile = hasDeps + ? 'Dockerfile.dependencies' + : 'Dockerfile'; + + const image = cdk.BundlingDockerImage.fromAsset(entry, { + buildArgs: { + IMAGE: runtime.bundlingDockerImage.image, + }, + file: path.join(__dirname, dockerfile), + }); + + return lambda.Code.fromAsset(entry, { + assetHashType: cdk.AssetHashType.BUNDLE, + exclude: DEPENDENCY_EXCLUDES, bundling: { image, command: ['bash', '-c', depsCommand], @@ -46,9 +71,20 @@ export function bundle(options: BundlingOptions): lambda.AssetCode { }); } -enum Installer { - PIP = 'pip', - PIP3 = 'pip3', +/** + * Checks to see if the `entry` directory contains a type of dependency that + * we know how to install. + */ +export function hasDependencies(entry: string): boolean { + if (fs.existsSync(path.join(entry, 'Pipfile'))) { + return true; + } + + if (fs.existsSync(path.join(entry, 'requirements.txt'))) { + return true; + } + + return false; } function chain(commands: string[]): string { diff --git a/packages/@aws-cdk/aws-lambda-python/lib/function.ts b/packages/@aws-cdk/aws-lambda-python/lib/function.ts index 7b3fbb1d71fe8..77f794704e967 100644 --- a/packages/@aws-cdk/aws-lambda-python/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda-python/lib/function.ts @@ -64,9 +64,9 @@ export class PythonFunction extends lambda.Function { ...props, runtime, code: bundle({ - ...props, - entry, runtime, + entry, + outputPathSuffix: '.', }), handler: `${index.slice(0, -3)}.${handler}`, }); diff --git a/packages/@aws-cdk/aws-lambda-python/lib/index.ts b/packages/@aws-cdk/aws-lambda-python/lib/index.ts index 2653adb2a89e8..5459e812abb91 100644 --- a/packages/@aws-cdk/aws-lambda-python/lib/index.ts +++ b/packages/@aws-cdk/aws-lambda-python/lib/index.ts @@ -1 +1,2 @@ export * from './function'; +export * from './layer'; diff --git a/packages/@aws-cdk/aws-lambda-python/lib/layer.ts b/packages/@aws-cdk/aws-lambda-python/lib/layer.ts new file mode 100644 index 0000000000000..8b090eed6a989 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/lib/layer.ts @@ -0,0 +1,54 @@ +import * as path from 'path'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk from '@aws-cdk/core'; +import { bundle } from './bundling'; + +/** + * Properties for PythonLayerVersion + */ +export interface PythonLayerVersionProps extends lambda.LayerVersionOptions { + /** + * The path to the root directory of the lambda layer. + */ + readonly entry: string; + + /** + * The runtimes compatible with the python layer. + * + * @default - All runtimes are supported. + */ + readonly compatibleRuntimes?: lambda.Runtime[]; +} + +/** + * A lambda layer version. + * + * @experimental + */ +export class PythonLayerVersion extends lambda.LayerVersion { + constructor(scope: cdk.Construct, id: string, props: PythonLayerVersionProps) { + const compatibleRuntimes = props.compatibleRuntimes ?? [lambda.Runtime.PYTHON_3_7]; + + // Ensure that all compatible runtimes are python + for (const runtime of compatibleRuntimes) { + if (runtime && runtime.family !== lambda.RuntimeFamily.PYTHON) { + throw new Error('Only `PYTHON` runtimes are supported.'); + } + } + + // Entry and defaults + const entry = path.resolve(props.entry); + // Pick the first compatibleRuntime to use for bundling or PYTHON_3_7 + const runtime = compatibleRuntimes[0] ?? lambda.Runtime.PYTHON_3_7; + + super(scope, id, { + ...props, + compatibleRuntimes, + code: bundle({ + entry, + runtime, + outputPathSuffix: 'python', + }), + }); + } +} 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..ca415f60e5476 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts @@ -1,79 +1,158 @@ import * as fs from 'fs'; +import * as path from 'path'; import { Code, Runtime } from '@aws-cdk/aws-lambda'; -import { bundle } from '../lib/bundling'; +import { hasDependencies, bundle } from '../lib/bundling'; jest.mock('@aws-cdk/aws-lambda'); const existsSyncOriginal = fs.existsSync; const existsSyncMock = jest.spyOn(fs, 'existsSync'); +jest.mock('child_process', () => ({ + spawnSync: jest.fn(() => { + return { + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('sha256:1234567890abcdef'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }; + }), +})); + beforeEach(() => { jest.clearAllMocks(); }); -test('Bundling', () => { +test('Bundling a function without dependencies', () => { + const entry = path.join(__dirname, 'lambda-handler-nodeps'); bundle({ - entry: '/project/folder', + entry: entry, runtime: Runtime.PYTHON_3_7, + outputPathSuffix: '.', }); // Correctly bundles - expect(Code.fromAsset).toHaveBeenCalledWith('/project/folder', { + expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({ bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'rsync -r . /asset-output', + 'rsync -r . /asset-output/.', ], }), - }); + })); // Searches for requirements.txt in entry - expect(existsSyncMock).toHaveBeenCalledWith('/project/folder/requirements.txt'); + expect(existsSyncMock).toHaveBeenCalledWith(path.join(entry, 'requirements.txt')); }); -test('Bundling with requirements.txt installed', () => { - existsSyncMock.mockImplementation((p: fs.PathLike) => { - if (/requirements.txt/.test(p.toString())) { - return true; - } - return existsSyncOriginal(p); +test('Bundling a function with requirements.txt installed', () => { + const entry = path.join(__dirname, 'lambda-handler'); + bundle({ + entry: entry, + runtime: Runtime.PYTHON_3_7, + outputPathSuffix: '.', }); + // Correctly bundles + expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({ + bundling: expect.objectContaining({ + command: [ + 'bash', '-c', + 'rsync -r /var/dependencies/. /asset-output/. && rsync -r . /asset-output/.', + ], + }), + })); + + // Searches for requirements.txt in entry + expect(existsSyncMock).toHaveBeenCalledWith(path.join(entry, 'requirements.txt')); +}); + +test('Bundling Python 2.7 with requirements.txt installed', () => { + const entry = path.join(__dirname, 'lambda-handler'); bundle({ - entry: '/project/folder', - runtime: Runtime.PYTHON_3_7, + entry: entry, + runtime: Runtime.PYTHON_2_7, + outputPathSuffix: '.', }); // Correctly bundles with requirements.txt pip installed - expect(Code.fromAsset).toHaveBeenCalledWith('/project/folder', { + expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({ bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'pip3 install -r requirements.txt -t /asset-output && rsync -r . /asset-output', + 'rsync -r /var/dependencies/. /asset-output/. && rsync -r . /asset-output/.', ], }), - }); + })); + + // Searches for requirements.txt in entry + expect(existsSyncMock).toHaveBeenCalledWith(path.join(entry, 'requirements.txt')); }); -test('Bundling Python 2.7 with requirements.txt installed', () => { - existsSyncMock.mockImplementation((p: fs.PathLike) => { - if (/requirements.txt/.test(p.toString())) { - return true; - } - return existsSyncOriginal(p); +test('Bundling a layer with dependencies', () => { + const entry = path.join(__dirname, 'lambda-handler'); + + bundle({ + entry: entry, + runtime: Runtime.PYTHON_2_7, + outputPathSuffix: 'python', }); + expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({ + bundling: expect.objectContaining({ + command: [ + 'bash', '-c', + 'rsync -r /var/dependencies/. /asset-output/python && rsync -r . /asset-output/python', + ], + }), + })); +}); + +test('Bundling a python code layer', () => { + const entry = path.join(__dirname, 'lambda-handler-nodeps'); + bundle({ - entry: '/project/folder', + entry: path.join(entry, '.'), runtime: Runtime.PYTHON_2_7, + outputPathSuffix: 'python', }); - // Correctly bundles with requirements.txt pip installed - expect(Code.fromAsset).toHaveBeenCalledWith('/project/folder', { + expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({ bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'pip install -r requirements.txt -t /asset-output && rsync -r . /asset-output', + 'rsync -r . /asset-output/python', ], }), + })); +}); + +describe('Dependency detection', () => { + test('Detects pipenv', () => { + existsSyncMock.mockImplementation((p: fs.PathLike) => { + if (/Pipfile/.test(p.toString())) { + return true; + } + return existsSyncOriginal(p); + }); + + expect(hasDependencies('/asset-input')).toEqual(true); + }); + + test('Detects requirements.txt', () => { + existsSyncMock.mockImplementation((p: fs.PathLike) => { + if (/requirements.txt/.test(p.toString())) { + return true; + } + return existsSyncOriginal(p); + }); + + expect(hasDependencies('/asset-input')).toEqual(true); + }); + + test('No known dependencies', () => { + existsSyncMock.mockImplementation(() => false); + expect(hasDependencies('/asset-input')).toEqual(false); }); }); diff --git a/packages/@aws-cdk/aws-lambda-python/test/function.test.ts b/packages/@aws-cdk/aws-lambda-python/test/function.test.ts index 9bc69a7757946..98bab1cf35be4 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/function.test.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/function.test.ts @@ -12,6 +12,7 @@ jest.mock('../lib/bundling', () => { }, bindToResource: () => { return; }, }), + hasDependencies: jest.fn().mockReturnValue(false), }; }); @@ -28,6 +29,7 @@ test('PythonFunction with defaults', () => { expect(bundle).toHaveBeenCalledWith(expect.objectContaining({ entry: expect.stringMatching(/@aws-cdk\/aws-lambda-python\/test\/lambda-handler$/), + outputPathSuffix: '.', })); expect(stack).toHaveResource('AWS::Lambda::Function', { @@ -44,6 +46,7 @@ test('PythonFunction with index in a subdirectory', () => { expect(bundle).toHaveBeenCalledWith(expect.objectContaining({ entry: expect.stringMatching(/@aws-cdk\/aws-lambda-python\/test\/lambda-handler-sub$/), + outputPathSuffix: '.', })); expect(stack).toHaveResource('AWS::Lambda::Function', { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json index ba3b9d456ca11..c75736f461667 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25S3Bucket5C76F19D" + "Ref": "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eS3Bucket40BC544C" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25S3VersionKey374DFF5D" + "Ref": "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eS3VersionKeyDD20BCAA" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25S3VersionKey374DFF5D" + "Ref": "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eS3VersionKeyDD20BCAA" } ] } @@ -87,17 +87,17 @@ } }, "Parameters": { - "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25S3Bucket5C76F19D": { + "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eS3Bucket40BC544C": { "Type": "String", - "Description": "S3 bucket for asset \"cc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25\"" + "Description": "S3 bucket for asset \"9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0e\"" }, - "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25S3VersionKey374DFF5D": { + "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eS3VersionKeyDD20BCAA": { "Type": "String", - "Description": "S3 key for asset version \"cc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25\"" + "Description": "S3 key for asset version \"9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0e\"" }, - "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25ArtifactHashB15DA742": { + "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eArtifactHash5F92CE57": { "Type": "String", - "Description": "Artifact hash for asset \"cc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25\"" + "Description": "Artifact hash for asset \"9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0e\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv-inline.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv-inline.expected.json new file mode 100644 index 0000000000000..19cc6231599e4 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv-inline.expected.json @@ -0,0 +1,110 @@ +{ + "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": "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bS3BucketDF70124D" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bS3VersionKey530C68B0" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bS3VersionKey530C68B0" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "myhandlerServiceRole77891068", + "Arn" + ] + }, + "Runtime": "python3.6" + }, + "DependsOn": [ + "myhandlerServiceRole77891068" + ] + } + }, + "Parameters": { + "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bS3BucketDF70124D": { + "Type": "String", + "Description": "S3 bucket for asset \"eef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255b\"" + }, + "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bS3VersionKey530C68B0": { + "Type": "String", + "Description": "S3 key for asset version \"eef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255b\"" + }, + "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bArtifactHashEE8E0CE9": { + "Type": "String", + "Description": "Artifact hash for asset \"eef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255b\"" + } + }, + "Outputs": { + "FunctionName": { + "Value": { + "Ref": "myhandlerD202FA8E" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv-inline.ts b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv-inline.ts new file mode 100644 index 0000000000000..17b17070e56e8 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv-inline.ts @@ -0,0 +1,29 @@ +import * as path from 'path'; +import { Runtime } from '@aws-cdk/aws-lambda'; +import { App, CfnOutput, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as lambda from '../lib'; + +/* + * Stack verification steps: + * aws lambda invoke --function-name --invocation-type Event --payload $(base64 <<<'"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-pipenv'), + runtime: Runtime.PYTHON_3_6, + }); + + new CfnOutput(this, 'FunctionName', { + value: fn.functionName, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-lambda-python-inline'); +app.synth(); diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py27.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py27.expected.json new file mode 100644 index 0000000000000..c0cb2b260d146 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py27.expected.json @@ -0,0 +1,110 @@ +{ + "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": "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014S3BucketB5A59BD8" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014S3VersionKey7657015C" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014S3VersionKey7657015C" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "myhandlerServiceRole77891068", + "Arn" + ] + }, + "Runtime": "python2.7" + }, + "DependsOn": [ + "myhandlerServiceRole77891068" + ] + } + }, + "Parameters": { + "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014S3BucketB5A59BD8": { + "Type": "String", + "Description": "S3 bucket for asset \"f37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014\"" + }, + "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014S3VersionKey7657015C": { + "Type": "String", + "Description": "S3 key for asset version \"f37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014\"" + }, + "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014ArtifactHash7768674B": { + "Type": "String", + "Description": "Artifact hash for asset \"f37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014\"" + } + }, + "Outputs": { + "FunctionName": { + "Value": { + "Ref": "myhandlerD202FA8E" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py27.ts b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py27.ts new file mode 100644 index 0000000000000..68f2b2f18f4b4 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py27.ts @@ -0,0 +1,29 @@ +import * as path from 'path'; +import { Runtime } from '@aws-cdk/aws-lambda'; +import { App, CfnOutput, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as lambda from '../lib'; + +/* + * Stack verification steps: + * aws lambda invoke --function-name --invocation-type Event --payload $(base64 <<<'"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-pipenv'), + runtime: Runtime.PYTHON_2_7, + }); + + new CfnOutput(this, 'FunctionName', { + value: fn.functionName, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-lambda-python-pipenv-py27'); +app.synth(); diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py38.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py38.expected.json new file mode 100644 index 0000000000000..07e4ea8cf45f9 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py38.expected.json @@ -0,0 +1,110 @@ +{ + "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": "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aS3Bucket31144813" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aS3VersionKeyB48E8383" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aS3VersionKeyB48E8383" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "myhandlerServiceRole77891068", + "Arn" + ] + }, + "Runtime": "python3.8" + }, + "DependsOn": [ + "myhandlerServiceRole77891068" + ] + } + }, + "Parameters": { + "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aS3Bucket31144813": { + "Type": "String", + "Description": "S3 bucket for asset \"3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846a\"" + }, + "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aS3VersionKeyB48E8383": { + "Type": "String", + "Description": "S3 key for asset version \"3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846a\"" + }, + "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aArtifactHash652F614E": { + "Type": "String", + "Description": "Artifact hash for asset \"3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846a\"" + } + }, + "Outputs": { + "FunctionName": { + "Value": { + "Ref": "myhandlerD202FA8E" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py38.ts b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py38.ts new file mode 100644 index 0000000000000..619dd270ec206 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py38.ts @@ -0,0 +1,29 @@ +import * as path from 'path'; +import { Runtime } from '@aws-cdk/aws-lambda'; +import { App, CfnOutput, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as lambda from '../lib'; + +/* + * Stack verification steps: + * aws lambda invoke --function-name --invocation-type Event --payload $(base64 <<<'"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-pipenv'), + runtime: Runtime.PYTHON_3_8, + }); + + new CfnOutput(this, 'FunctionName', { + value: fn.functionName, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-lambda-python-pipenv-py38'); +app.synth(); diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.expected.json new file mode 100644 index 0000000000000..bbf2630c2be48 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.expected.json @@ -0,0 +1,176 @@ +{ + "Resources": { + "SharedDACC02AA": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": { + "S3Bucket": { + "Ref": "AssetParameters83e524c4077f05f723f2fd9068d80291a91fda4706ae5f4e84a1462e99e83e71S3BucketD683DA42" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters83e524c4077f05f723f2fd9068d80291a91fda4706ae5f4e84a1462e99e83e71S3VersionKeyA0299125" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters83e524c4077f05f723f2fd9068d80291a91fda4706ae5f4e84a1462e99e83e71S3VersionKeyA0299125" + } + ] + } + ] + } + ] + ] + } + }, + "CompatibleRuntimes": [ + "python3.6" + ] + } + }, + "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": "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3Bucket89C9DB12" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3VersionKey435DAD55" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3VersionKey435DAD55" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "myhandlerServiceRole77891068", + "Arn" + ] + }, + "Runtime": "python3.6", + "Layers": [ + { + "Ref": "SharedDACC02AA" + } + ] + }, + "DependsOn": [ + "myhandlerServiceRole77891068" + ] + } + }, + "Parameters": { + "AssetParameters83e524c4077f05f723f2fd9068d80291a91fda4706ae5f4e84a1462e99e83e71S3BucketD683DA42": { + "Type": "String", + "Description": "S3 bucket for asset \"83e524c4077f05f723f2fd9068d80291a91fda4706ae5f4e84a1462e99e83e71\"" + }, + "AssetParameters83e524c4077f05f723f2fd9068d80291a91fda4706ae5f4e84a1462e99e83e71S3VersionKeyA0299125": { + "Type": "String", + "Description": "S3 key for asset version \"83e524c4077f05f723f2fd9068d80291a91fda4706ae5f4e84a1462e99e83e71\"" + }, + "AssetParameters83e524c4077f05f723f2fd9068d80291a91fda4706ae5f4e84a1462e99e83e71ArtifactHash8CBB58BE": { + "Type": "String", + "Description": "Artifact hash for asset \"83e524c4077f05f723f2fd9068d80291a91fda4706ae5f4e84a1462e99e83e71\"" + }, + "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3Bucket89C9DB12": { + "Type": "String", + "Description": "S3 bucket for asset \"71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218\"" + }, + "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3VersionKey435DAD55": { + "Type": "String", + "Description": "S3 key for asset version \"71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218\"" + }, + "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218ArtifactHash0EDF3CD0": { + "Type": "String", + "Description": "Artifact hash for asset \"71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218\"" + } + }, + "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.project.ts b/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.ts new file mode 100644 index 0000000000000..1259ba09bdfe1 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.ts @@ -0,0 +1,36 @@ +import * as path from 'path'; +import { Runtime } from '@aws-cdk/aws-lambda'; +import { App, CfnOutput, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +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 projectDirectory = path.join(__dirname, 'lambda-handler-project'); + const fn = new lambda.PythonFunction(this, 'my_handler', { + entry: path.join(projectDirectory, 'lambda'), + runtime: Runtime.PYTHON_3_6, + layers: [ + new lambda.PythonLayerVersion(this, 'Shared', { + entry: path.join(projectDirectory, 'shared'), + compatibleRuntimes: [Runtime.PYTHON_3_6], + }), + ], + }); + + new CfnOutput(this, 'FunctionArn', { + value: fn.functionArn, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-lambda-function-project'); +app.synth(); 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 index ff95a0ef74c51..fe005754fcfab 100644 --- 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 @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters7173f882be924a92ff6019021219c38f84fdf7f12205fc7558dab5c2fa03d134S3Bucket4FDAE558" + "Ref": "AssetParameters58da14e3fe62df26811cb369b51aa83cab6a27ea3e638e5952ed6439b9cd2f2fS3Bucket3D9CB240" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters7173f882be924a92ff6019021219c38f84fdf7f12205fc7558dab5c2fa03d134S3VersionKey09B41633" + "Ref": "AssetParameters58da14e3fe62df26811cb369b51aa83cab6a27ea3e638e5952ed6439b9cd2f2fS3VersionKeyA12EF729" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters7173f882be924a92ff6019021219c38f84fdf7f12205fc7558dab5c2fa03d134S3VersionKey09B41633" + "Ref": "AssetParameters58da14e3fe62df26811cb369b51aa83cab6a27ea3e638e5952ed6439b9cd2f2fS3VersionKeyA12EF729" } ] } @@ -87,17 +87,17 @@ } }, "Parameters": { - "AssetParameters7173f882be924a92ff6019021219c38f84fdf7f12205fc7558dab5c2fa03d134S3Bucket4FDAE558": { + "AssetParameters58da14e3fe62df26811cb369b51aa83cab6a27ea3e638e5952ed6439b9cd2f2fS3Bucket3D9CB240": { "Type": "String", - "Description": "S3 bucket for asset \"7173f882be924a92ff6019021219c38f84fdf7f12205fc7558dab5c2fa03d134\"" + "Description": "S3 bucket for asset \"58da14e3fe62df26811cb369b51aa83cab6a27ea3e638e5952ed6439b9cd2f2f\"" }, - "AssetParameters7173f882be924a92ff6019021219c38f84fdf7f12205fc7558dab5c2fa03d134S3VersionKey09B41633": { + "AssetParameters58da14e3fe62df26811cb369b51aa83cab6a27ea3e638e5952ed6439b9cd2f2fS3VersionKeyA12EF729": { "Type": "String", - "Description": "S3 key for asset version \"7173f882be924a92ff6019021219c38f84fdf7f12205fc7558dab5c2fa03d134\"" + "Description": "S3 key for asset version \"58da14e3fe62df26811cb369b51aa83cab6a27ea3e638e5952ed6439b9cd2f2f\"" }, - "AssetParameters7173f882be924a92ff6019021219c38f84fdf7f12205fc7558dab5c2fa03d134ArtifactHash9ED905F8": { + "AssetParameters58da14e3fe62df26811cb369b51aa83cab6a27ea3e638e5952ed6439b9cd2f2fArtifactHash3C7CD7C2": { "Type": "String", - "Description": "Artifact hash for asset \"7173f882be924a92ff6019021219c38f84fdf7f12205fc7558dab5c2fa03d134\"" + "Description": "Artifact hash for asset \"58da14e3fe62df26811cb369b51aa83cab6a27ea3e638e5952ed6439b9cd2f2f\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.expected.json new file mode 100644 index 0000000000000..c7c29700ae02f --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.expected.json @@ -0,0 +1,110 @@ +{ + "Resources": { + "functionServiceRoleEF216095": { + "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" + ] + ] + } + ] + } + }, + "functionF19B1A04": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters07cc0ee6a3f6e318f2dbf8ca3bf77166e807eecb5c1be4caf0d554e5d329c690S3BucketC1F94008" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters07cc0ee6a3f6e318f2dbf8ca3bf77166e807eecb5c1be4caf0d554e5d329c690S3VersionKeyE1B3B5F5" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters07cc0ee6a3f6e318f2dbf8ca3bf77166e807eecb5c1be4caf0d554e5d329c690S3VersionKeyE1B3B5F5" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "functionServiceRoleEF216095", + "Arn" + ] + }, + "Runtime": "python2.7" + }, + "DependsOn": [ + "functionServiceRoleEF216095" + ] + } + }, + "Parameters": { + "AssetParameters07cc0ee6a3f6e318f2dbf8ca3bf77166e807eecb5c1be4caf0d554e5d329c690S3BucketC1F94008": { + "Type": "String", + "Description": "S3 bucket for asset \"07cc0ee6a3f6e318f2dbf8ca3bf77166e807eecb5c1be4caf0d554e5d329c690\"" + }, + "AssetParameters07cc0ee6a3f6e318f2dbf8ca3bf77166e807eecb5c1be4caf0d554e5d329c690S3VersionKeyE1B3B5F5": { + "Type": "String", + "Description": "S3 key for asset version \"07cc0ee6a3f6e318f2dbf8ca3bf77166e807eecb5c1be4caf0d554e5d329c690\"" + }, + "AssetParameters07cc0ee6a3f6e318f2dbf8ca3bf77166e807eecb5c1be4caf0d554e5d329c690ArtifactHashF0B9DCD1": { + "Type": "String", + "Description": "Artifact hash for asset \"07cc0ee6a3f6e318f2dbf8ca3bf77166e807eecb5c1be4caf0d554e5d329c690\"" + } + }, + "Outputs": { + "Function": { + "Value": { + "Ref": "functionF19B1A04" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.ts b/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.ts new file mode 100644 index 0000000000000..12ee929de9da6 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.ts @@ -0,0 +1,68 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { Runtime } from '@aws-cdk/aws-lambda'; +import { App, CfnOutput, Stack, StackProps } from '@aws-cdk/core'; +import { Construct, ConstructOrder } from 'constructs'; +import * as lambda from '../lib'; + +/* + * Stack verification steps: + * aws lambda invoke --function-name --invocation-type Event --payload $(base64 <<<'"OK"') response.json + */ + +class TestStack extends Stack { + public readonly dependenciesAssetHash: string; + + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const fn = new lambda.PythonFunction(this, 'function', { + entry: workDir, + runtime: Runtime.PYTHON_2_7, + }); + + new CfnOutput(this, 'Function', { + value: fn.functionName, + }); + + // Find the asset hash of the dependencies + this.dependenciesAssetHash = (fn.node.findAll(ConstructOrder.POSTORDER) + .find(c => c.node.path.endsWith('Code')) as any) + .assetHash; + } +} + +// This is a special integration test that synths twice to show that docker +// picks up a change in requirements.txt + +// Create a working directory for messing around with requirements.txt +const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-integ')); +fs.copyFileSync(path.join(__dirname, 'lambda-handler', 'index.py'), path.join(workDir, 'index.py')); +const requirementsTxtPath = path.join(workDir, 'requirements.txt'); + +// Write a requirements.txt with an extraneous dependency (colorama) +const beforeDeps = 'requests==2.23.0\npillow==6.2.2\ncolorama==0.4.3\n'; +fs.writeFileSync(requirementsTxtPath, beforeDeps); + +// Synth the first time +const app = new App(); +const stack1 = new TestStack(app, 'cdk-integ-lambda-python-requirements-removed'); +app.synth(); + +// Then, write a requirements.txt without the extraneous dependency and synth again +const afterDeps = 'requests==2.23.0\npillow==6.2.2\n'; +fs.writeFileSync(requirementsTxtPath, afterDeps); + +// Synth the same stack a second time with different requirements.txt contents +const app2 = new App(); +const stack2 = new TestStack(app2, 'cdk-integ-lambda-python-requirements-removed'); +app2.synth(); + +if (!stack1.dependenciesAssetHash || !stack2.dependenciesAssetHash) { + throw new Error('The asset hashes are not both truthy'); +} + +if (stack1.dependenciesAssetHash === stack2.dependenciesAssetHash) { + throw new Error('Removing a dependency did not change the asset hash'); +} diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.expected.json index cd0057d2898c2..e65f882540762 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.expected.json @@ -296,7 +296,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25S3Bucket5C76F19D" + "Ref": "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eS3Bucket40BC544C" }, "S3Key": { "Fn::Join": [ @@ -309,7 +309,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25S3VersionKey374DFF5D" + "Ref": "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eS3VersionKeyDD20BCAA" } ] } @@ -322,7 +322,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25S3VersionKey374DFF5D" + "Ref": "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eS3VersionKeyDD20BCAA" } ] } @@ -368,17 +368,17 @@ } }, "Parameters": { - "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25S3Bucket5C76F19D": { + "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eS3Bucket40BC544C": { "Type": "String", - "Description": "S3 bucket for asset \"cc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25\"" + "Description": "S3 bucket for asset \"9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0e\"" }, - "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25S3VersionKey374DFF5D": { + "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eS3VersionKeyDD20BCAA": { "Type": "String", - "Description": "S3 key for asset version \"cc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25\"" + "Description": "S3 key for asset version \"9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0e\"" }, - "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25ArtifactHashB15DA742": { + "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eArtifactHash5F92CE57": { "Type": "String", - "Description": "Artifact hash for asset \"cc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25\"" + "Description": "Artifact hash for asset \"9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0e\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-nodeps/index.py b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-nodeps/index.py new file mode 100644 index 0000000000000..f118d0551afb8 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-nodeps/index.py @@ -0,0 +1,2 @@ +def handler(event, context): + print('No dependencies') diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/Pipfile b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/Pipfile new file mode 100644 index 0000000000000..2d72b30bc8be8 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/Pipfile @@ -0,0 +1,8 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[packages] +requests = "==2.23.0" +pillow = "==6.2.2" diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/Pipfile.lock b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/Pipfile.lock new file mode 100644 index 0000000000000..0113c74bf7363 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/Pipfile.lock @@ -0,0 +1,94 @@ +{ + "_meta": { + "hash": { + "sha256": "0f782e44e69391c98999b575a7f93228f06d22716a144d955386b9c6abec2040" + }, + "pipfile-spec": 6, + "requires": {}, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + ], + "version": "==2020.6.20" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" + }, + "pillow": { + "hashes": [ + "sha256:00e0bbe9923adc5cc38a8da7d87d4ce16cde53b8d3bba8886cb928e84522d963", + "sha256:03457e439d073770d88afdd90318382084732a5b98b0eb6f49454746dbaae701", + "sha256:0d5c99f80068f13231ac206bd9b2e80ea357f5cf9ae0fa97fab21e32d5b61065", + "sha256:1a3bc8e1db5af40a81535a62a591fafdb30a8a1b319798ea8052aa65ef8f06d2", + "sha256:2b4a94be53dff02af90760c10a2e3634c3c7703410f38c98154d5ce71fe63d20", + "sha256:3ba7d8f1d962780f86aa747fef0baf3211b80cb13310fff0c375da879c0656d4", + "sha256:3e81485cec47c24f5fb27acb485a4fc97376b2b332ed633867dc68ac3077998c", + "sha256:43ef1cff7ee57f9c8c8e6fa02a62eae9fa23a7e34418c7ce88c0e3fe09d1fb38", + "sha256:4adc3302df4faf77c63ab3a83e1a3e34b94a6a992084f4aa1cb236d1deaf4b39", + "sha256:535e8e0e02c9f1fc2e307256149d6ee8ad3aa9a6e24144b7b6e6fb6126cb0e99", + "sha256:5ccfcb0a34ad9b77ad247c231edb781763198f405a5c8dc1b642449af821fb7f", + "sha256:5dcbbaa3a24d091a64560d3c439a8962866a79a033d40eb1a75f1b3413bfc2bc", + "sha256:6e2a7e74d1a626b817ecb7a28c433b471a395c010b2a1f511f976e9ea4363e64", + "sha256:82859575005408af81b3e9171ae326ff56a69af5439d3fc20e8cb76cd51c8246", + "sha256:834dd023b7f987d6b700ad93dc818098d7eb046bd445e9992b3093c6f9d7a95f", + "sha256:87ef0eca169f7f0bc050b22f05c7e174a65c36d584428431e802c0165c5856ea", + "sha256:900de1fdc93764be13f6b39dc0dd0207d9ff441d87ad7c6e97e49b81987dc0f3", + "sha256:92b83b380f9181cacc994f4c983d95a9c8b00b50bf786c66d235716b526a3332", + "sha256:aa1b0297e352007ec781a33f026afbb062a9a9895bb103c8f49af434b1666880", + "sha256:aa4792ab056f51b49e7d59ce5733155e10a918baf8ce50f64405db23d5627fa2", + "sha256:b72c39585f1837d946bd1a829a4820ccf86e361f28cbf60f5d646f06318b61e2", + "sha256:bb7861e4618a0c06c40a2e509c1bea207eea5fd4320d486e314e00745a402ca5", + "sha256:bc149dab804291a18e1186536519e5e122a2ac1316cb80f506e855a500b1cdd4", + "sha256:c424d35a5259be559b64490d0fd9e03fba81f1ce8e5b66e0a59de97547351d80", + "sha256:cbd5647097dc55e501f459dbac7f1d0402225636deeb9e0a98a8d2df649fc19d", + "sha256:ccf16fe444cc43800eeacd4f4769971200982200a71b1368f49410d0eb769543", + "sha256:d3a98444a00b4643b22b0685dbf9e0ddcaf4ebfd4ea23f84f228adf5a0765bb2", + "sha256:d6b4dc325170bee04ca8292bbd556c6f5398d52c6149ca881e67daf62215426f", + "sha256:db9ff0c251ed066d367f53b64827cc9e18ccea001b986d08c265e53625dab950", + "sha256:e3a797a079ce289e59dbd7eac9ca3bf682d52687f718686857281475b7ca8e6a" + ], + "index": "pypi", + "version": "==6.2.2" + }, + "requests": { + "hashes": [ + "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", + "sha256:5d2d0ffbb515f39417009a46c14256291061ac01ba8f875b90cad137de83beb4", + "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" + ], + "index": "pypi", + "version": "==2.23.0" + }, + "urllib3": { + "hashes": [ + "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", + "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.25.10" + } + }, + "develop": {} +} diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/index.py b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/index.py new file mode 100644 index 0000000000000..c033f37560534 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/index.py @@ -0,0 +1,11 @@ +import requests +from PIL import Image + +def handler(event, context): + response = requests.get('https://a0.awsstatic.com/main/images/logos/aws_smile-header-desktop-en-white_59x35.png', stream=True) + img = Image.open(response.raw) + + print(response.status_code) + print(img.size) + + return response.status_code diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/lambda/index.py b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/lambda/index.py new file mode 100644 index 0000000000000..6ac592242c8fb --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/lambda/index.py @@ -0,0 +1,12 @@ +import requests +from PIL import Image +import shared + +def handler(event, context): + response = requests.get(shared.get_url(), stream=True) + img = Image.open(response.raw) + + print(response.status_code) + print(img.size) + + return response.status_code diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/shared/requirements.txt b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/shared/requirements.txt new file mode 100644 index 0000000000000..51c1bfbf03afe --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/shared/requirements.txt @@ -0,0 +1,2 @@ +requests==2.23.0 +pillow==6.2.2 diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/shared/shared.py b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/shared/shared.py new file mode 100644 index 0000000000000..b17623b83b881 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/shared/shared.py @@ -0,0 +1,2 @@ +def get_url() -> str: + return 'https://a0.awsstatic.com/main/images/logos/aws_smile-header-desktop-en-white_59x35.png' diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/requirements.txt b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/requirements.txt index 288e2971fb79a..7232ea84ef267 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/requirements.txt +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/requirements.txt @@ -1,2 +1,3 @@ requests==2.23.0 -pillow==7.2.0 +# Pillow 6.x so that python 2.7 and 3.x can both use this fixture +pillow==6.2.2 diff --git a/packages/@aws-cdk/aws-lambda-python/test/layer.test.ts b/packages/@aws-cdk/aws-lambda-python/test/layer.test.ts new file mode 100644 index 0000000000000..8ace2ec3f7a18 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/layer.test.ts @@ -0,0 +1,54 @@ +import '@aws-cdk/assert/jest'; +import * as path from 'path'; +import { Runtime } from '@aws-cdk/aws-lambda'; +import { Stack } from '@aws-cdk/core'; +import { hasDependencies, bundle } from '../lib/bundling'; +import { PythonLayerVersion } from '../lib/layer'; + +jest.mock('../lib/bundling', () => { + return { + bundle: jest.fn().mockReturnValue({ + bind: () => { + return { + s3Location: { + bucketName: 'bucket', + objectKey: 'key', + }, + }; + }, + bindToResource: () => { return; }, + }), + hasDependencies: jest.fn().mockReturnValue(true), + }; +}); + +const hasDependenciesMock = (hasDependencies as jest.Mock); + +let stack: Stack; +beforeEach(() => { + stack = new Stack(); + jest.clearAllMocks(); +}); + +test('Bundling a layer from files', () => { + hasDependenciesMock.mockReturnValue(false); + + const entry = path.join(__dirname, 'test/lambda-handler-project'); + new PythonLayerVersion(stack, 'layer', { + entry, + }); + + expect(bundle).toHaveBeenCalledWith(expect.objectContaining({ + entry, + outputPathSuffix: 'python', + })); +}); + +test('Fails when bundling a layer for a runtime not supported', () => { + expect(() => { + new PythonLayerVersion(stack, 'layer', { + entry: '/some/path', + compatibleRuntimes: [Runtime.PYTHON_2_7, Runtime.NODEJS], + }); + }).toThrow(/PYTHON.*support/); +}); diff --git a/packages/@aws-cdk/aws-lambda/lib/layers.ts b/packages/@aws-cdk/aws-lambda/lib/layers.ts index 12a92140b511a..4a58d5ce501ef 100644 --- a/packages/@aws-cdk/aws-lambda/lib/layers.ts +++ b/packages/@aws-cdk/aws-lambda/lib/layers.ts @@ -4,21 +4,10 @@ import { Code } from './code'; import { CfnLayerVersion, CfnLayerVersionPermission } from './lambda.generated'; import { Runtime } from './runtime'; -export interface LayerVersionProps { - /** - * The runtimes compatible with this Layer. - * - * @default - All runtimes are supported. - */ - readonly compatibleRuntimes?: Runtime[]; - - /** - * The content of this Layer. - * - * Using `Code.fromInline` is not supported. - */ - readonly code: Code; - +/** + * Non runtime options + */ +export interface LayerVersionOptions { /** * The description the this Lambda Layer. * @@ -41,6 +30,22 @@ export interface LayerVersionProps { readonly layerVersionName?: string; } +export interface LayerVersionProps extends LayerVersionOptions { + /** + * The runtimes compatible with this Layer. + * + * @default - All runtimes are supported. + */ + readonly compatibleRuntimes?: Runtime[]; + + /** + * The content of this Layer. + * + * Using `Code.fromInline` is not supported. + */ + readonly code: Code; +} + export interface ILayerVersion extends IResource { /** * The ARN of the Lambda Layer version that this Layer defines. diff --git a/packages/@aws-cdk/aws-secretsmanager/README.md b/packages/@aws-cdk/aws-secretsmanager/README.md index 4904814a1c4e9..286e83ea37b6b 100644 --- a/packages/@aws-cdk/aws-secretsmanager/README.md +++ b/packages/@aws-cdk/aws-secretsmanager/README.md @@ -69,7 +69,9 @@ then `Secret.grantRead` and `Secret.grantWrite` will also grant the role the relevant encrypt and decrypt permissions to the KMS key through the SecretsManager service principal. -### Rotating a Secret with a custom Lambda function +### Rotating a Secret + +#### Using a Custom Lambda Function A rotation schedule can be added to a Secret using a custom Lambda function: @@ -85,6 +87,31 @@ secret.addRotationSchedule('RotationSchedule', { See [Overview of the Lambda Rotation Function](https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotating-secrets-lambda-function-overview.html) on how to implement a Lambda Rotation Function. +#### Using a Hosted Lambda Function + +Use the `hostedRotation` prop to rotate a secret with a hosted Lambda function: + +```ts +const secret = new secretsmanager.Secret(this, 'Secret'); + +secret.addRotationSchedule('RotationSchedule', { + hostedRotation: secretsmanager.HostedRotation.mysqlSingleUser(), +}); +``` + +Hosted rotation is available for secrets representing credentials for MySQL, PostgreSQL, Oracle, +MariaDB, SQLServer, Redshift and MongoDB (both for the single and multi user schemes). + +When deployed in a VPC, the hosted rotation implements `ec2.IConnectable`: + +```ts +const myHostedRotation = secretsmanager.HostedRotation.mysqlSingleUser({ vpc: myVpc }); +secret.addRotationSchedule('RotationSchedule', { hostedRotation: myHostedRotation }); +dbConnections.allowDefaultPortFrom(hostedRotation); +``` + +See also [Automating secret creation in AWS CloudFormation](https://docs.aws.amazon.com/secretsmanager/latest/userguide/integrating_cloudformation.html). + ### Rotating database credentials Define a `SecretRotation` to rotate database credentials: diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts b/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts index 49d6170004e71..1243976963386 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts @@ -1,5 +1,6 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; import * as lambda from '@aws-cdk/aws-lambda'; -import { Duration, Resource } from '@aws-cdk/core'; +import { Duration, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { ISecret } from './secret'; import { CfnRotationSchedule } from './secretsmanager.generated'; @@ -9,9 +10,18 @@ import { CfnRotationSchedule } from './secretsmanager.generated'; */ export interface RotationScheduleOptions { /** - * The Lambda function that can rotate the secret. + * A Lambda function that can rotate the secret. + * + * @default - either `rotationLambda` or `hostedRotation` must be specified */ - readonly rotationLambda: lambda.IFunction; + readonly rotationLambda?: lambda.IFunction; + + /** + * Hosted rotation + * + * @default - either `rotationLambda` or `hostedRotation` must be specified + */ + readonly hostedRotation?: HostedRotation; /** * Specifies the number of days after the previous rotation before @@ -28,6 +38,23 @@ export interface RotationScheduleOptions { export interface RotationScheduleProps extends RotationScheduleOptions { /** * The secret to rotate. + * + * If hosted rotation is used, this must be a JSON string with the following format: + * + * ``` + * { + * "engine": , + * "host": , + * "username": , + * "password": , + * "dbname": , + * "port": , + * "masterarn": + * } + * ``` + * + * This is typically the case for a secret referenced from an `AWS::SecretsManager::SecretTargetAttachment` + * or an `ISecret` returned by the `attach()` method of `Secret`. */ readonly secret: ISecret; } @@ -39,12 +66,254 @@ export class RotationSchedule extends Resource { constructor(scope: Construct, id: string, props: RotationScheduleProps) { super(scope, id); + if ((!props.rotationLambda && !props.hostedRotation) || (props.rotationLambda && props.hostedRotation)) { + throw new Error('One of `rotationLambda` or `hostedRotation` must be specified.'); + } + new CfnRotationSchedule(this, 'Resource', { secretId: props.secret.secretArn, - rotationLambdaArn: props.rotationLambda.functionArn, + rotationLambdaArn: props.rotationLambda?.functionArn, + hostedRotationLambda: props.hostedRotation?.bind(props.secret, this), rotationRules: { automaticallyAfterDays: props.automaticallyAfter && props.automaticallyAfter.toDays() || 30, }, }); + + // Prevent secrets deletions when rotation is in place + props.secret.denyAccountRootDelete(); + } +} + +/** + * Single user hosted rotation options + */ +export interface SingleUserHostedRotationOptions { + /** + * A name for the Lambda created to rotate the secret + * + * @default - a CloudFormation generated name + */ + readonly functionName?: string; + + /** + * A list of security groups for the Lambda created to rotate the secret + * + * @default - a new security group is created + */ + readonly securityGroups?: ec2.ISecurityGroup[]; + + /** + * The VPC where the Lambda rotation function will run. + * + * @default - the Lambda is not deployed in a VPC + */ + readonly vpc?: ec2.IVpc; + + /** + * The type of subnets in the VPC where the Lambda rotation function will run. + * + * @default - the Vpc default strategy if not specified. + */ + readonly vpcSubnets?: ec2.SubnetSelection; +} + +/** + * Multi user hosted rotation options + */ +export interface MultiUserHostedRotationOptions extends SingleUserHostedRotationOptions { + /** + * The master secret for a multi user rotation scheme + */ + readonly masterSecret: ISecret; +} + +/** + * A hosted rotation + */ +export class HostedRotation implements ec2.IConnectable { + /** MySQL Single User */ + public static mysqlSingleUser(options: SingleUserHostedRotationOptions = {}) { + return new HostedRotation(HostedRotationType.MYSQL_SINGLE_USER, options); + } + + /** MySQL Multi User */ + public static mysqlMultiUser(options: MultiUserHostedRotationOptions) { + return new HostedRotation(HostedRotationType.MYSQL_MULTI_USER, options, options.masterSecret); + } + + /** PostgreSQL Single User */ + public static postgreSqlSingleUser(options: SingleUserHostedRotationOptions = {}) { + return new HostedRotation(HostedRotationType.POSTGRESQL_SINGLE_USER, options); + } + + /** PostgreSQL Multi User */ + public static postgreSqlMultiUser(options: MultiUserHostedRotationOptions) { + return new HostedRotation(HostedRotationType.POSTGRESQL_MULTI_USER, options, options.masterSecret); + } + + /** Oracle Single User */ + public static oracleSingleUser(options: SingleUserHostedRotationOptions = {}) { + return new HostedRotation(HostedRotationType.ORACLE_SINGLE_USER, options); + } + + /** Oracle Multi User */ + public static oracleMultiUser(options: MultiUserHostedRotationOptions) { + return new HostedRotation(HostedRotationType.ORACLE_MULTI_USER, options, options.masterSecret); + } + + /** MariaDB Single User */ + public static mariaDbSingleUser(options: SingleUserHostedRotationOptions = {}) { + return new HostedRotation(HostedRotationType.MARIADB_SINGLE_USER, options); } + + /** MariaDB Multi User */ + public static mariaDbMultiUser(options: MultiUserHostedRotationOptions) { + return new HostedRotation(HostedRotationType.MARIADB_MULTI_USER, options, options.masterSecret); + } + + /** SQL Server Single User */ + public static sqlServerSingleUser(options: SingleUserHostedRotationOptions = {}) { + return new HostedRotation(HostedRotationType.SQLSERVER_SINGLE_USER, options); + } + + /** SQL Server Multi User */ + public static sqlServerMultiUser(options: MultiUserHostedRotationOptions) { + return new HostedRotation(HostedRotationType.SQLSERVER_MULTI_USER, options, options.masterSecret); + } + + /** Redshift Single User */ + public static redshiftSingleUser(options: SingleUserHostedRotationOptions = {}) { + return new HostedRotation(HostedRotationType.REDSHIFT_SINGLE_USER, options); + } + + /** Redshift Multi User */ + public static redshiftMultiUser(options: MultiUserHostedRotationOptions) { + return new HostedRotation(HostedRotationType.REDSHIFT_MULTI_USER, options, options.masterSecret); + } + + /** MongoDB Single User */ + public static mongoDbSingleUser(options: SingleUserHostedRotationOptions = {}) { + return new HostedRotation(HostedRotationType.MONGODB_SINGLE_USER, options); + } + + /** MongoDB Multi User */ + public static mongoDbMultiUser(options: MultiUserHostedRotationOptions) { + return new HostedRotation(HostedRotationType.MONGODB_MULTI_USER, options, options.masterSecret); + } + + private _connections?: ec2.Connections; + + private constructor( + private readonly type: HostedRotationType, + private readonly props: SingleUserHostedRotationOptions | MultiUserHostedRotationOptions, + private readonly masterSecret?: ISecret, + ) { + if (type.isMultiUser && !masterSecret) { + throw new Error('The `masterSecret` must be specified when using the multi user scheme.'); + } + } + + /** + * Binds this hosted rotation to a secret + */ + public bind(secret: ISecret, scope: Construct): CfnRotationSchedule.HostedRotationLambdaProperty { + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-rotationschedule-hostedrotationlambda.html + Stack.of(scope).addTransform('AWS::SecretsManager-2020-07-23'); + + if (!this.props.vpc && this.props.securityGroups) { + throw new Error('`vpc` must be specified when specifying `securityGroups`.'); + } + + if (this.props.vpc) { + this._connections = new ec2.Connections({ + securityGroups: this.props.securityGroups || [new ec2.SecurityGroup(scope, 'SecurityGroup', { + vpc: this.props.vpc, + })], + }); + } + + // Prevent master secret deletion when rotation is in place + if (this.masterSecret) { + this.masterSecret.denyAccountRootDelete(); + } + + return { + rotationType: this.type.name, + kmsKeyArn: secret.encryptionKey?.keyArn, + masterSecretArn: this.masterSecret?.secretArn, + masterSecretKmsKeyArn: this.masterSecret?.encryptionKey?.keyArn, + rotationLambdaName: this.props.functionName, + vpcSecurityGroupIds: this._connections?.securityGroups?.map(s => s.securityGroupId).join(','), + vpcSubnetIds: this.props.vpc?.selectSubnets(this.props.vpcSubnets).subnetIds.join(','), + }; + } + + /** + * Security group connections for this hosted rotation + */ + public get connections() { + if (!this.props.vpc) { + throw new Error('Cannot use connections for a hosted rotation that is not deployed in a VPC'); + } + + // If we are in a vpc and bind() has been called _connections should be defined + if (!this._connections) { + throw new Error('Cannot use connections for a hosted rotation that has not been bound to a secret'); + } + + return this._connections; + } +} + +/** + * Hosted rotation type + */ +export class HostedRotationType { + /** MySQL Single User */ + public static readonly MYSQL_SINGLE_USER = new HostedRotationType('MySQLSingleUser'); + + /** MySQL Multi User */ + public static readonly MYSQL_MULTI_USER = new HostedRotationType('MySQLMultiUser', true); + + /** PostgreSQL Single User */ + public static readonly POSTGRESQL_SINGLE_USER = new HostedRotationType('PostgreSQLSingleUser'); + + /** PostgreSQL Multi User */ + public static readonly POSTGRESQL_MULTI_USER = new HostedRotationType('PostgreSQLMultiUser', true); + + /** Oracle Single User */ + public static readonly ORACLE_SINGLE_USER = new HostedRotationType('OracleSingleUser'); + + /** Oracle Multi User */ + public static readonly ORACLE_MULTI_USER = new HostedRotationType('OracleMultiUser', true); + + /** MariaDB Single User */ + public static readonly MARIADB_SINGLE_USER = new HostedRotationType('MariaDBSingleUser'); + + /** MariaDB Multi User */ + public static readonly MARIADB_MULTI_USER = new HostedRotationType('MariaDBMultiUser', true); + + /** SQL Server Single User */ + public static readonly SQLSERVER_SINGLE_USER = new HostedRotationType('SQLServerSingleUser') + + /** SQL Server Multi User */ + public static readonly SQLSERVER_MULTI_USER = new HostedRotationType('SQLServerMultiUser', true); + + /** Redshift Single User */ + public static readonly REDSHIFT_SINGLE_USER = new HostedRotationType('RedshiftSingleUser') + + /** Redshift Multi User */ + public static readonly REDSHIFT_MULTI_USER = new HostedRotationType('RedshiftMultiUser', true); + + /** MongoDB Single User */ + public static readonly MONGODB_SINGLE_USER = new HostedRotationType('MongoDBSingleUser'); + + /** MongoDB Multi User */ + public static readonly MONGODB_MULTI_USER = new HostedRotationType('MongoDBMultiUser', true); + + /** + * @param name The type of rotation + * @param isMultiUser Whether the rotation uses the mutli user scheme + */ + private constructor(public readonly name: string, public readonly isMultiUser?: boolean) {} } diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts index cdd51ff5cedbb..388933895af51 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts @@ -136,6 +136,7 @@ export class SecretRotationApplication { export interface SecretRotationProps { /** * The secret to rotate. It must be a JSON string with the following format: + * * ``` * { * "engine": , @@ -148,8 +149,8 @@ export interface SecretRotationProps { * } * ``` * - * This is typically the case for a secret referenced from an - * AWS::SecretsManager::SecretTargetAttachment or an `ISecret` returned by the `attach()` method of `Secret`. + * This is typically the case for a secret referenced from an `AWS::SecretsManager::SecretTargetAttachment` + * or an `ISecret` returned by the `attach()` method of `Secret`. * * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secrettargetattachment.html */ @@ -270,8 +271,7 @@ export class SecretRotation extends CoreConstruct { automaticallyAfter: props.automaticallyAfter, }); - // Prevent secrets deletions when rotation is in place - props.secret.denyAccountRootDelete(); + // Prevent master secret deletion when rotation is in place if (props.masterSecret) { props.masterSecret.denyAccountRootDelete(); } diff --git a/packages/@aws-cdk/aws-secretsmanager/test/integ.hosted-rotation.expected.json b/packages/@aws-cdk/aws-secretsmanager/test/integ.hosted-rotation.expected.json new file mode 100644 index 0000000000000..be2f63be0aa79 --- /dev/null +++ b/packages/@aws-cdk/aws-secretsmanager/test/integ.hosted-rotation.expected.json @@ -0,0 +1,61 @@ +{ + "Transform": "AWS::SecretsManager-2020-07-23", + "Resources": { + "SecretA720EF05": { + "Type": "AWS::SecretsManager::Secret", + "Properties": { + "GenerateSecretString": {} + } + }, + "SecretSchedule18F2CB66": { + "Type": "AWS::SecretsManager::RotationSchedule", + "Properties": { + "SecretId": { + "Ref": "SecretA720EF05" + }, + "HostedRotationLambda": { + "RotationType": "MySQLSingleUser" + }, + "RotationRules": { + "AutomaticallyAfterDays": 30 + } + } + }, + "SecretPolicy06C9821C": { + "Type": "AWS::SecretsManager::ResourcePolicy", + "Properties": { + "ResourcePolicy": { + "Statement": [ + { + "Action": "secretsmanager:DeleteSecret", + "Effect": "Deny", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "SecretId": { + "Ref": "SecretA720EF05" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-secretsmanager/test/integ.hosted-rotation.ts b/packages/@aws-cdk/aws-secretsmanager/test/integ.hosted-rotation.ts new file mode 100644 index 0000000000000..10109f91496cd --- /dev/null +++ b/packages/@aws-cdk/aws-secretsmanager/test/integ.hosted-rotation.ts @@ -0,0 +1,18 @@ +import * as cdk from '@aws-cdk/core'; +import * as secretsmanager from '../lib'; + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string) { + super(scope, id); + + const secret = new secretsmanager.Secret(this, 'Secret'); + + secret.addRotationSchedule('Schedule', { + hostedRotation: secretsmanager.HostedRotation.mysqlSingleUser(), + }); + } +} + +const app = new cdk.App(); +new TestStack(app, 'cdk-integ-secret-hosted-rotation'); +app.synth(); diff --git a/packages/@aws-cdk/aws-secretsmanager/test/rotation-schedule.test.ts b/packages/@aws-cdk/aws-secretsmanager/test/rotation-schedule.test.ts index d63859ce02f5f..56eff9534a776 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/rotation-schedule.test.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/rotation-schedule.test.ts @@ -1,4 +1,5 @@ import '@aws-cdk/assert/jest'; +import * as ec2 from '@aws-cdk/aws-ec2'; import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; import * as secretsmanager from '../lib'; @@ -8,7 +9,7 @@ beforeEach(() => { stack = new cdk.Stack(); }); -test('create a rotation schedule', () => { +test('create a rotation schedule with a rotation Lambda', () => { // GIVEN const secret = new secretsmanager.Secret(stack, 'Secret'); const rotationLambda = new lambda.Function(stack, 'Lambda', { @@ -39,3 +40,308 @@ test('create a rotation schedule', () => { }, }); }); + +describe('hosted rotation', () => { + test('single user not in a vpc', () => { + // GIVEN + const app = new cdk.App(); + stack = new cdk.Stack(app, 'TestStack'); + const secret = new secretsmanager.Secret(stack, 'Secret'); + + // WHEN + secret.addRotationSchedule('RotationSchedule', { + hostedRotation: secretsmanager.HostedRotation.mysqlSingleUser(), + }); + + // THEN + expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + SecretId: { + Ref: 'SecretA720EF05', + }, + HostedRotationLambda: { + RotationType: 'MySQLSingleUser', + }, + RotationRules: { + AutomaticallyAfterDays: 30, + }, + }); + + expect(app.synth().getStackByName(stack.stackName).template).toEqual(expect.objectContaining({ + Transform: 'AWS::SecretsManager-2020-07-23', + })); + + expect(stack).toHaveResource('AWS::SecretsManager::ResourcePolicy', { + ResourcePolicy: { + Statement: [ + { + Action: 'secretsmanager:DeleteSecret', + Effect: 'Deny', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + SecretId: { + Ref: 'SecretA720EF05', + }, + }); + }); + + test('multi user not in a vpc', () => { + // GIVEN + const secret = new secretsmanager.Secret(stack, 'Secret'); + const masterSecret = new secretsmanager.Secret(stack, 'MasterSecret'); + + // WHEN + secret.addRotationSchedule('RotationSchedule', { + hostedRotation: secretsmanager.HostedRotation.postgreSqlMultiUser({ + masterSecret, + }), + }); + + // THEN + expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + SecretId: { + Ref: 'SecretA720EF05', + }, + HostedRotationLambda: { + MasterSecretArn: { + Ref: 'MasterSecretA11BF785', + }, + RotationType: 'PostgreSQLMultiUser', + }, + RotationRules: { + AutomaticallyAfterDays: 30, + }, + }); + + expect(stack).toHaveResource('AWS::SecretsManager::ResourcePolicy', { + ResourcePolicy: { + Statement: [ + { + Action: 'secretsmanager:DeleteSecret', + Effect: 'Deny', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + SecretId: { + Ref: 'MasterSecretA11BF785', + }, + }); + }); + + test('single user in a vpc', () => { + // GIVEN + const vpc = new ec2.Vpc(stack, 'Vpc'); + const secret = new secretsmanager.Secret(stack, 'Secret'); + const dbSecurityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc }); + const dbConnections = new ec2.Connections({ + defaultPort: ec2.Port.tcp(3306), + securityGroups: [dbSecurityGroup], + }); + + // WHEN + const hostedRotation = secretsmanager.HostedRotation.mysqlSingleUser({ vpc }); + secret.addRotationSchedule('RotationSchedule', { hostedRotation }); + dbConnections.allowDefaultPortFrom(hostedRotation); + + // THEN + expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + SecretId: { + Ref: 'SecretA720EF05', + }, + HostedRotationLambda: { + RotationType: 'MySQLSingleUser', + VpcSecurityGroupIds: { + 'Fn::GetAtt': [ + 'SecretRotationScheduleSecurityGroup3F1F76EA', + 'GroupId', + ], + }, + VpcSubnetIds: { + 'Fn::Join': [ + '', + [ + { + Ref: 'VpcPrivateSubnet1Subnet536B997A', + }, + ',', + { + Ref: 'VpcPrivateSubnet2Subnet3788AAA1', + }, + ], + ], + }, + }, + RotationRules: { + AutomaticallyAfterDays: 30, + }, + }); + + expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + FromPort: 3306, + GroupId: { + 'Fn::GetAtt': [ + 'SecurityGroupDD263621', + 'GroupId', + ], + }, + SourceSecurityGroupId: { + 'Fn::GetAtt': [ + 'SecretRotationScheduleSecurityGroup3F1F76EA', + 'GroupId', + ], + }, + ToPort: 3306, + }); + }); + + test('single user in a vpc with security groups', () => { + // GIVEN + const vpc = new ec2.Vpc(stack, 'Vpc'); + const secret = new secretsmanager.Secret(stack, 'Secret'); + const dbSecurityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc }); + const dbConnections = new ec2.Connections({ + defaultPort: ec2.Port.tcp(3306), + securityGroups: [dbSecurityGroup], + }); + + // WHEN + const hostedRotation = secretsmanager.HostedRotation.mysqlSingleUser({ + vpc, + securityGroups: [ + new ec2.SecurityGroup(stack, 'SG1', { vpc }), + new ec2.SecurityGroup(stack, 'SG2', { vpc }), + ], + }); + secret.addRotationSchedule('RotationSchedule', { hostedRotation }); + dbConnections.allowDefaultPortFrom(hostedRotation); + + // THEN + expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + SecretId: { + Ref: 'SecretA720EF05', + }, + HostedRotationLambda: { + RotationType: 'MySQLSingleUser', + VpcSecurityGroupIds: { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'SG1BA065B6E', + 'GroupId', + ], + }, + ',', + { + 'Fn::GetAtt': [ + 'SG20CE3219C', + 'GroupId', + ], + }, + ], + ], + }, + VpcSubnetIds: { + 'Fn::Join': [ + '', + [ + { + Ref: 'VpcPrivateSubnet1Subnet536B997A', + }, + ',', + { + Ref: 'VpcPrivateSubnet2Subnet3788AAA1', + }, + ], + ], + }, + }, + RotationRules: { + AutomaticallyAfterDays: 30, + }, + }); + + expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + FromPort: 3306, + GroupId: { + 'Fn::GetAtt': [ + 'SecurityGroupDD263621', + 'GroupId', + ], + }, + SourceSecurityGroupId: { + 'Fn::GetAtt': [ + 'SG20CE3219C', + 'GroupId', + ], + }, + ToPort: 3306, + }); + }); + + test('throws with security groups and no vpc', () => { + // GIVEN + const secret = new secretsmanager.Secret(stack, 'Secret'); + + // THEN + expect(() => secret.addRotationSchedule('RotationSchedule', { + hostedRotation: secretsmanager.HostedRotation.oracleSingleUser({ + securityGroups: [ec2.SecurityGroup.fromSecurityGroupId(secret, 'SG', 'sg-12345678')], + }), + })).toThrow(/`vpc` must be specified when specifying `securityGroups`/); + }); + + test('throws when accessing the connections object when not in a vpc', () => { + // GIVEN + const secret = new secretsmanager.Secret(stack, 'Secret'); + + // WHEN + const hostedRotation = secretsmanager.HostedRotation.sqlServerSingleUser(); + secret.addRotationSchedule('RotationSchedule', { hostedRotation }); + + // THEN + expect(() => hostedRotation.connections.allowToAnyIpv4(ec2.Port.allTraffic())) + .toThrow(/Cannot use connections for a hosted rotation that is not deployed in a VPC/); + }); +}); diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 214c6340a1932..d567675f1ca2f 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -299,6 +299,12 @@ pipeline.addApplicationStage(new MyApplication(this, 'Production', { })); ``` +> Be aware that adding new stages via `addApplicationStage()` will +> automatically add them to the pipeline and deploy the new stacks, but +> *removing* them from the pipeline or deleting the pipeline stack will not +> automatically delete deployed application stacks. You must delete those +> stacks by hand using the AWS CloudFormation console or the AWS CLI. + ### More Control Every *Application Stage* added by `addApplicationStage()` will lead to the addition of