From 65063275b64f647c570a06f7a28d37c5d403113b Mon Sep 17 00:00:00 2001 From: Richard Davison Date: Wed, 22 Jul 2020 12:17:24 -0400 Subject: [PATCH 01/30] fix(stepfunctions-tasks): EvaluateExpression error when key specified multiple times (#8858) ### Commit Message fix(stepfunctions-tasks): replace all occurrences of variable in expression fixes #8856 ### End Commit Message ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/eval-nodejs10.x-handler/index.ts | 6 +++++- .../test/eval-nodejs10.x-handler.test.ts | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/eval-nodejs10.x-handler/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/eval-nodejs10.x-handler/index.ts index 4bd79ff06fa27..c40aeb340bfbb 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/eval-nodejs10.x-handler/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/eval-nodejs10.x-handler/index.ts @@ -1,12 +1,16 @@ /* eslint-disable no-console */ import { Event } from '../evaluate-expression'; +function escapeRegex(x: string) { + return x.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); +} + export async function handler(event: Event): Promise { console.log('Event: %j', event); const expression = Object.entries(event.expressionAttributeValues) .reduce( - (exp, [k, v]) => exp.replace(k, JSON.stringify(v)), + (exp: string, [k, v]) => exp.replace(new RegExp(escapeRegex(k), 'g'), JSON.stringify(v)), event.expression, ); console.log(`Expression: ${expression}`); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs10.x-handler.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs10.x-handler.test.ts index 3f0e254d5489c..d110f97f47002 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs10.x-handler.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs10.x-handler.test.ts @@ -44,3 +44,17 @@ test('with lists', async () => { const evaluated = await handler(event); expect(evaluated).toEqual([2, 4, 6]); }); + +test('with duplicated entries', async () => { + // GIVEN + const event: Event = { + expression: '$.a + $.a', + expressionAttributeValues: { + '$.a': 1, + }, + }; + + // THEN + const evaluated = await handler(event); + expect(evaluated).toBe(2); +}); From f99c3271ed2b4c68f3cd2970a1b38571f5ddc911 Mon Sep 17 00:00:00 2001 From: Taylor Date: Wed, 22 Jul 2020 09:58:30 -0700 Subject: [PATCH 02/30] fix(cloudfront): Set MinimumProtocolVersion and SslSupportMethod when specifying distribution certificate (#9200) Per the CloudFormation documentation, if you specify an ACM certificate ARN, you must also specify values for MinimumProtocolVersion and SslSupportMethod in AWS::CloudFront::Distribution ViewerCertificate. We are using the recommended SslSupportMethod of "sni-only" and MinimumProtocolVersion of "TLSv1.2_2018." Fixes: https://github.com/aws/aws-cdk/issues/9193 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-cloudfront/README.md | 3 +- .../aws-cloudfront/lib/distribution.ts | 43 ++++++++++++++++++- .../aws-cloudfront/lib/web_distribution.ts | 35 +-------------- .../aws-cloudfront/test/distribution.test.ts | 2 + 4 files changed, 47 insertions(+), 36 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront/README.md b/packages/@aws-cdk/aws-cloudfront/README.md index 3d9e91b7caac0..935b97ebef946 100644 --- a/packages/@aws-cdk/aws-cloudfront/README.md +++ b/packages/@aws-cdk/aws-cloudfront/README.md @@ -76,7 +76,8 @@ When you create a distribution, CloudFront assigns a domain name for the distrib be retrieved from `distribution.distributionDomainName`. CloudFront distributions use a default certificate (`*.cloudfront.net`) to support HTTPS by default. If you want to use your own domain name, such as `www.example.com`, you must associate a certificate with your distribution that contains your domain name. The certificate must be present in the AWS Certificate Manager (ACM) service in the US East (N. Virginia) region; the certificate -may either be created by ACM, or created elsewhere and imported into ACM. +may either be created by ACM, or created elsewhere and imported into ACM. When a certificate is used, the distribution will support HTTPS connections +from SNI only and a minimum protocol version of TLSv1.2_2018. ```ts const myCertificate = new acm.DnsValidatedCertificate(this, 'mySiteCert', { diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index 2290a8c366a2d..cb8b60a08a386 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -157,7 +157,7 @@ export class Distribution extends Resource implements IDistribution { origins: Lazy.anyValue({ produce: () => this.renderOrigins() }), defaultCacheBehavior: this.defaultBehavior._renderBehavior(), cacheBehaviors: Lazy.anyValue({ produce: () => this.renderCacheBehaviors() }), - viewerCertificate: this.certificate ? { acmCertificateArn: this.certificate.certificateArn } : undefined, + viewerCertificate: this.certificate ? this.renderViewerCertificate(this.certificate) : undefined, customErrorResponses: this.renderErrorResponses(), priceClass: props.priceClass ?? undefined, } }); @@ -221,6 +221,14 @@ export class Distribution extends Resource implements IDistribution { }); } + private renderViewerCertificate(certificate: acm.ICertificate): CfnDistribution.ViewerCertificateProperty { + return { + acmCertificateArn: certificate.certificateArn, + sslSupportMethod: SSLMethod.SNI, + minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2018, + }; + } + } /** @@ -260,6 +268,39 @@ export enum OriginProtocolPolicy { HTTPS_ONLY = 'https-only', } +/** + * The SSL method CloudFront will use for your distribution. + * + * Server Name Indication (SNI) - is an extension to the TLS computer networking protocol by which a client indicates + * which hostname it is attempting to connect to at the start of the handshaking process. This allows a server to present + * multiple certificates on the same IP address and TCP port number and hence allows multiple secure (HTTPS) websites + * (or any other service over TLS) to be served by the same IP address without requiring all those sites to use the same certificate. + * + * CloudFront can use SNI to host multiple distributions on the same IP - which a large majority of clients will support. + * + * If your clients cannot support SNI however - CloudFront can use dedicated IPs for your distribution - but there is a prorated monthly charge for + * using this feature. By default, we use SNI - but you can optionally enable dedicated IPs (VIP). + * + * See the CloudFront SSL for more details about pricing : https://aws.amazon.com/cloudfront/custom-ssl-domains/ + * + */ +export enum SSLMethod { + SNI = 'sni-only', + VIP = 'vip' +} + +/** + * The minimum version of the SSL protocol that you want CloudFront to use for HTTPS connections. + * CloudFront serves your objects only to browsers or devices that support at least the SSL version that you specify. + */ +export enum SecurityPolicyProtocol { + SSL_V3 = 'SSLv3', + TLS_V1 = 'TLSv1', + TLS_V1_2016 = 'TLSv1_2016', + TLS_V1_1_2016 = 'TLSv1.1_2016', + TLS_V1_2_2018 = 'TLSv1.2_2018' +} + /** * The HTTP methods that the Behavior will accept requests on. */ diff --git a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts index 2e6e56eb45f6b..450201170f98e 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts @@ -4,7 +4,7 @@ import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; import { CfnDistribution } from './cloudfront.generated'; -import { IDistribution, OriginProtocolPolicy, PriceClass, ViewerProtocolPolicy } from './distribution'; +import { IDistribution, OriginProtocolPolicy, PriceClass, ViewerProtocolPolicy, SSLMethod, SecurityPolicyProtocol } from './distribution'; import { IOriginAccessIdentity } from './origin_access_identity'; export enum HttpVersion { @@ -89,39 +89,6 @@ export interface AliasConfiguration { readonly securityPolicy?: SecurityPolicyProtocol; } -/** - * The SSL method CloudFront will use for your distribution. - * - * Server Name Indication (SNI) - is an extension to the TLS computer networking protocol by which a client indicates - * which hostname it is attempting to connect to at the start of the handshaking process. This allows a server to present - * multiple certificates on the same IP address and TCP port number and hence allows multiple secure (HTTPS) websites - * (or any other service over TLS) to be served by the same IP address without requiring all those sites to use the same certificate. - * - * CloudFront can use SNI to host multiple distributions on the same IP - which a large majority of clients will support. - * - * If your clients cannot support SNI however - CloudFront can use dedicated IPs for your distribution - but there is a prorated monthly charge for - * using this feature. By default, we use SNI - but you can optionally enable dedicated IPs (VIP). - * - * See the CloudFront SSL for more details about pricing : https://aws.amazon.com/cloudfront/custom-ssl-domains/ - * - */ -export enum SSLMethod { - SNI = 'sni-only', - VIP = 'vip' -} - -/** - * The minimum version of the SSL protocol that you want CloudFront to use for HTTPS connections. - * CloudFront serves your objects only to browsers or devices that support at least the SSL version that you specify. - */ -export enum SecurityPolicyProtocol { - SSL_V3 = 'SSLv3', - TLS_V1 = 'TLSv1', - TLS_V1_2016 = 'TLSv1_2016', - TLS_V1_1_2016 = 'TLSv1.1_2016', - TLS_V1_2_2018 = 'TLSv1.2_2018' -} - /** * Logging configuration for incoming requests */ diff --git a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts index 86c91c2254cf8..218f5a4c6ac10 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts @@ -217,6 +217,8 @@ describe('certificates', () => { DistributionConfig: { ViewerCertificate: { AcmCertificateArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012', + SslSupportMethod: 'sni-only', + MinimumProtocolVersion: 'TLSv1.2_2018', }, }, }); From c4818fe825a7350fdacf813f08feb7d0642bf0c8 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Wed, 22 Jul 2020 13:31:55 -0700 Subject: [PATCH 03/30] docs(route53-patterns): add detail to README and doc strings (#9194) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-route53-patterns/README.md | 35 +++++++++++++--- .../lib/website-redirect.ts | 42 ++++++++++++++----- .../aws-route53-patterns/package.json | 3 +- .../test/bucket-website-target.test.ts | 22 ++++++++++ 4 files changed, 84 insertions(+), 18 deletions(-) diff --git a/packages/@aws-cdk/aws-route53-patterns/README.md b/packages/@aws-cdk/aws-route53-patterns/README.md index 187c96389f4fd..f06f5fcd7e3a1 100644 --- a/packages/@aws-cdk/aws-route53-patterns/README.md +++ b/packages/@aws-cdk/aws-route53-patterns/README.md @@ -1,4 +1,4 @@ -# Route53 Patterns for the CDK Route53 Library +# CDK Construct library for higher-level Route 53 Constructs --- @@ -9,12 +9,37 @@ --- -This library contains commonly used patterns for Route53. - +This library provides higher-level Amazon Route 53 constructs which follow common +architectural patterns. ## HTTPS Redirect -This construct allows creating a simple domainA -> domainB redirect using CloudFront and S3. You can specify multiple domains to be redirected. +If you want to speed up delivery of your web content, you can use Amazon CloudFront, +the AWS content delivery network (CDN). CloudFront can deliver your entire website +—including dynamic, static, streaming, and interactive content—by using a global +network of edge locations. Requests for your content are automatically routed to the +edge location that gives your users the lowest latency. + +This construct allows creating a redirect from domainA to domainB using Amazon +CloudFront and Amazon S3. You can specify multiple domains to be redirected. +[Learn more](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-to-cloudfront-distribution.html) about routing traffic to a CloudFront web distribution. + +The `HttpsRedirect` constructs creates: + +* Amazon CloudFront distribution - makes website available from data centres + around the world +* Amazon S3 bucket - empty bucket used for website hosting redirect (`websiteRedirect`) capabilities. +* Amazon Route 53 Alias record - routes traffic to the CloudFront distribution +* AWS Certificate Manager certificate - SSL/TLS certificate used by + CloudFront for your domain + +⚠️ The stack/construct can be used in any region for configuring an HTTPS redirect. +The certificate created in Amazon Certificate Manager (ACM) will be in US East (N. Virginia) +region. If you use an existing certificate, the AWS region of the certificate +must be in US East (N. Virginia). + +The following example creates an HTTPS redirect from `foo.example.com` to `bar.example.com` +As an existing certificate is not provided, one will be created in `us-east-1` by the CDK. ```ts new HttpsRedirect(stack, 'Redirect', { @@ -26,5 +51,3 @@ This construct allows creating a simple domainA -> domainB redirect using CloudF }) }); ``` - -See the documentation of `@aws-cdk/aws-route53-patterns` for more information. diff --git a/packages/@aws-cdk/aws-route53-patterns/lib/website-redirect.ts b/packages/@aws-cdk/aws-route53-patterns/lib/website-redirect.ts index 7fd4b85f8d4f6..1dd1c97c11e66 100644 --- a/packages/@aws-cdk/aws-route53-patterns/lib/website-redirect.ts +++ b/packages/@aws-cdk/aws-route53-patterns/lib/website-redirect.ts @@ -1,33 +1,47 @@ +import * as crypto from 'crypto'; import { DnsValidatedCertificate, ICertificate } from '@aws-cdk/aws-certificatemanager'; import { CloudFrontWebDistribution, OriginProtocolPolicy, PriceClass, ViewerProtocolPolicy } from '@aws-cdk/aws-cloudfront'; import { ARecord, IHostedZone, RecordTarget } from '@aws-cdk/aws-route53'; import { CloudFrontTarget } from '@aws-cdk/aws-route53-targets'; import { Bucket, RedirectProtocol } from '@aws-cdk/aws-s3'; -import { Construct, RemovalPolicy } from '@aws-cdk/core'; -import * as crypto from 'crypto'; +import { Construct, RemovalPolicy, Stack, Token } from '@aws-cdk/core'; /** * Properties to configure an HTTPS Redirect */ export interface HttpsRedirectProps { /** - * HostedZone of the domain + * Hosted zone of the domain which will be used to create alias record(s) from + * domain names in the hosted zone to the target domain. The hosted zone must + * contain entries for the domain name(s) supplied through `recordNames` that + * will redirect to the target domain. + * + * Domain names in the hosted zone can include a specific domain (example.com) + * and its subdomains (acme.example.com, zenith.example.com). + * */ readonly zone: IHostedZone; + /** - * The redirect target domain + * The redirect target fully qualified domain name (FQDN). An alias record + * will be created that points to your CloudFront distribution. Root domain + * or sub-domain can be supplied. */ readonly targetDomain: string; + /** - * The domain names to create that will redirect to `targetDomain` + * The domain names that will redirect to `targetDomain` * - * @default - the domain name of the zone + * @default - the domain name of the hosted zone */ readonly recordNames?: string[]; + /** - * The ACM certificate; Has to be in us-east-1 + * The AWS Certificate Manager (ACM) certificate that will be associated with + * the CloudFront distribution that will be created. If provided, the certificate must be + * stored in us-east-1 (N. Virginia) * - * @default - create a new certificate in us-east-1 + * @default - A new certificate is created in us-east-1 (N. Virginia) */ readonly certificate?: ICertificate; } @@ -40,7 +54,14 @@ export class HttpsRedirect extends Construct { constructor(scope: Construct, id: string, props: HttpsRedirectProps) { super(scope, id); - const domainNames = props.recordNames || [props.zone.zoneName]; + const domainNames = props.recordNames ?? [props.zone.zoneName]; + + if (props.certificate) { + const certificateRegion = Stack.of(this).parseArn(props.certificate.certificateArn).region; + if (!Token.isUnresolved(certificateRegion) && certificateRegion !== 'us-east-1') { + throw new Error(`The certificate must be in the us-east-1 region and the certificate you provided is in ${certificateRegion}.`); + } + } const redirectCertArn = props.certificate ? props.certificate.certificateArn : new DnsValidatedCertificate(this, 'RedirectCertificate', { domainName: domainNames[0], @@ -82,6 +103,5 @@ export class HttpsRedirect extends Construct { target: RecordTarget.fromAlias(new CloudFrontTarget(redirectDist)), }); }); - } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-route53-patterns/package.json b/packages/@aws-cdk/aws-route53-patterns/package.json index 56855cc2c70b0..be7208bf11a6e 100644 --- a/packages/@aws-cdk/aws-route53-patterns/package.json +++ b/packages/@aws-cdk/aws-route53-patterns/package.json @@ -49,7 +49,8 @@ "aws", "cdk", "constructs", - "route53" + "route53", + "route53-patterns" ], "author": { "name": "Amazon Web Services", diff --git a/packages/@aws-cdk/aws-route53-patterns/test/bucket-website-target.test.ts b/packages/@aws-cdk/aws-route53-patterns/test/bucket-website-target.test.ts index 951bb8c603eec..ebbbf7dbba52c 100644 --- a/packages/@aws-cdk/aws-route53-patterns/test/bucket-website-target.test.ts +++ b/packages/@aws-cdk/aws-route53-patterns/test/bucket-website-target.test.ts @@ -107,3 +107,25 @@ test('create HTTPS redirect with existing cert', () => { }, }); }); + +test('throws when certificate in region other than us-east-1 is supplied', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'test', { env: { region: 'us-east-1' } }); + const certificate = Certificate.fromCertificateArn( + stack, 'Certificate', 'arn:aws:acm:us-east-2:123456789012:certificate/11-3336f1-44483d-adc7-9cd375c5169d', + ); + + // WHEN / THEN + expect(() => { + new HttpsRedirect(stack, 'Redirect', { + recordNames: ['foo.example.com'], + certificate, + targetDomain: 'bar.example.com', + zone: HostedZone.fromHostedZoneAttributes(stack, 'HostedZone', { + hostedZoneId: 'ID', + zoneName: 'example.com', + }), + }); + }).toThrow(/The certificate must be in the us-east-1 region and the certificate you provided is in us-east-2./); +}); From d1ca04f7a136be437a0538d7606803bdf0a73f98 Mon Sep 17 00:00:00 2001 From: AWS CDK Automation <43080478+aws-cdk-automation@users.noreply.github.com> Date: Wed, 22 Jul 2020 22:52:20 +0200 Subject: [PATCH 04/30] feat(cfnspec): cloudformation spec v16.1.0 (#9074) * feat: cloudformation spec v16.1.0 * chore: accept string maps as outputs when validating schema * add missing attributes to amplify domain * fix rds issue Co-authored-by: AWS CDK Team Co-authored-by: Elad Ben-Israel Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- packages/@aws-cdk/aws-amplify/lib/domain.ts | 26 +- packages/@aws-cdk/aws-rds/lib/proxy.ts | 7 +- packages/@aws-cdk/cfnspec/CHANGELOG.md | 112 +++ packages/@aws-cdk/cfnspec/cfn.version | 2 +- .../cfnspec/lib/schema/resource-type.ts | 17 +- ...0_CloudFormationResourceSpecification.json | 684 +++++++++++++++++- .../@aws-cdk/cfnspec/test/spec-validators.ts | 3 + 7 files changed, 825 insertions(+), 26 deletions(-) diff --git a/packages/@aws-cdk/aws-amplify/lib/domain.ts b/packages/@aws-cdk/aws-amplify/lib/domain.ts index ddb5b6b6f04e8..089eaa5117c78 100644 --- a/packages/@aws-cdk/aws-amplify/lib/domain.ts +++ b/packages/@aws-cdk/aws-amplify/lib/domain.ts @@ -1,4 +1,4 @@ -import { Construct, Lazy, Resource } from '@aws-cdk/core'; +import { Construct, Lazy, Resource, IResolvable } from '@aws-cdk/core'; import { CfnDomain } from './amplify.generated'; import { IApp } from './app'; import { IBranch } from './branch'; @@ -72,6 +72,27 @@ export class Domain extends Resource { */ public readonly statusReason: string; + /** + * Branch patterns for the automatically created subdomain. + * + * @attribute + */ + public readonly domainAutoSubDomainCreationPatterns: string[]; + + /** + * The IAM service role for the subdomain. + * + * @attribute + */ + public readonly domainAutoSubDomainIamRole: string; + + /** + * Specifies whether the automated creation of subdomains for branches is enabled. + * + * @attribute + */ + public readonly domainEnableAutoSubDomain: IResolvable; + private readonly subDomains: SubDomain[]; constructor(scope: Construct, id: string, props: DomainProps) { @@ -91,6 +112,9 @@ export class Domain extends Resource { this.domainName = domain.attrDomainName; this.domainStatus = domain.attrDomainStatus; this.statusReason = domain.attrStatusReason; + this.domainAutoSubDomainCreationPatterns = domain.attrAutoSubDomainCreationPatterns; + this.domainAutoSubDomainIamRole = domain.attrAutoSubDomainIamRole; + this.domainEnableAutoSubDomain = domain.attrEnableAutoSubDomain; } /** diff --git a/packages/@aws-cdk/aws-rds/lib/proxy.ts b/packages/@aws-cdk/aws-rds/lib/proxy.ts index e6e0ba5d66679..eff37a0b4307c 100644 --- a/packages/@aws-cdk/aws-rds/lib/proxy.ts +++ b/packages/@aws-cdk/aws-rds/lib/proxy.ts @@ -427,16 +427,13 @@ export class DatabaseProxy extends cdk.Resource throw new Error('Cannot specify both dbInstanceIdentifiers and dbClusterIdentifiers'); } - const proxyTargetGroup = new CfnDBProxyTargetGroup(this, 'ProxyTargetGroup', { + new CfnDBProxyTargetGroup(this, 'ProxyTargetGroup', { + targetGroupName: 'default', dbProxyName: this.dbProxyName, dbInstanceIdentifiers, dbClusterIdentifiers, connectionPoolConfigurationInfo: toConnectionPoolConfigurationInfo(props), }); - - // Currently(2020-07-04), this property must be set to default. - // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbproxytargetgroup.html#TargetGroupName-fn::getatt - proxyTargetGroup.addOverride('Properties.TargetGroupName', 'default'); } /** diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index 32390673011c9..5c05249b0514e 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,115 @@ +# CloudFormation Resource Specification v16.1.0 + +## New Resource Types + +* AWS::Athena::DataCatalog +* AWS::EC2::PrefixList +* AWS::QLDB::Stream + +## Attribute Changes + +* AWS::Amplify::Domain AutoSubDomainCreationPatterns (__added__) +* AWS::Amplify::Domain AutoSubDomainIAMRole (__added__) +* AWS::Amplify::Domain EnableAutoSubDomain (__added__) +* AWS::RDS::DBProxyTargetGroup TargetGroupName (__deleted__) +* AWS::ServiceCatalog::CloudFormationProvisionedProduct Outputs (__added__) +* AWS::ServiceCatalog::CloudFormationProvisionedProduct ProvisionedProductId (__added__) + +## Property Changes + +* AWS::Amplify::App EnableBranchAutoDeletion (__added__) +* AWS::Amplify::Domain AutoSubDomainCreationPatterns (__added__) +* AWS::Amplify::Domain AutoSubDomainIAMRole (__added__) +* AWS::Amplify::Domain EnableAutoSubDomain (__added__) +* AWS::AutoScaling::AutoScalingGroup NewInstancesProtectedFromScaleIn (__added__) +* AWS::CodeGuruProfiler::ProfilingGroup ComputePlatform (__added__) +* AWS::ElastiCache::ReplicationGroup MultiAZEnabled.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::ElasticLoadBalancingV2::Listener AlpnPolicy (__added__) +* AWS::Elasticsearch::Domain AdvancedSecurityOptions (__added__) +* AWS::Elasticsearch::Domain DomainEndpointOptions (__added__) +* AWS::FSx::FileSystem StorageCapacity.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::ImageBuilder::Component SupportedOsVersions (__added__) +* AWS::ImageBuilder::Image EnhancedImageMetadataEnabled (__added__) +* AWS::ImageBuilder::ImagePipeline EnhancedImageMetadataEnabled (__added__) +* AWS::ImageBuilder::ImageRecipe WorkingDirectory (__added__) +* AWS::Lambda::Function FileSystemConfigs (__added__) +* AWS::RDS::DBProxyTargetGroup TargetGroupName (__added__) +* AWS::RDS::DBProxyTargetGroup DBProxyName.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::Route53::HealthCheck HealthCheckConfig.UpdateType (__changed__) + * Old: Mutable + * New: Conditional +* AWS::SSM::Association ApplyOnlyAtCronInterval (__added__) +* AWS::SageMaker::EndpointConfig DataCaptureConfig (__added__) +* AWS::ServiceCatalog::CloudFormationProvisionedProduct NotificationArns.DuplicatesAllowed (__added__) +* AWS::ServiceDiscovery::HttpNamespace Tags.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::ServiceDiscovery::PrivateDnsNamespace Tags.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::ServiceDiscovery::PublicDnsNamespace Tags.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::ServiceDiscovery::Service Tags.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::Synthetics::Canary RunConfig.Required (__changed__) + * Old: true + * New: false + +## Property Type Changes + +* AWS::CodeBuild::Project.BuildStatusConfig (__added__) +* AWS::Elasticsearch::Domain.AdvancedSecurityOptionsInput (__added__) +* AWS::Elasticsearch::Domain.DomainEndpointOptions (__added__) +* AWS::Elasticsearch::Domain.MasterUserOptions (__added__) +* AWS::Lambda::Function.FileSystemConfig (__added__) +* AWS::SageMaker::EndpointConfig.CaptureContentTypeHeader (__added__) +* AWS::SageMaker::EndpointConfig.CaptureOption (__added__) +* AWS::SageMaker::EndpointConfig.DataCaptureConfig (__added__) +* AWS::WAFv2::RuleGroup.ForwardedIPConfiguration (__added__) +* AWS::WAFv2::RuleGroup.IPSetForwardedIPConfiguration (__added__) +* AWS::WAFv2::WebACL.ForwardedIPConfiguration (__added__) +* AWS::WAFv2::WebACL.IPSetForwardedIPConfiguration (__added__) +* AWS::CodeBuild::Project.Source BuildStatusConfig (__added__) +* AWS::FSx::FileSystem.LustreConfiguration AutomaticBackupRetentionDays (__added__) +* AWS::FSx::FileSystem.LustreConfiguration CopyTagsToBackups (__added__) +* AWS::FSx::FileSystem.LustreConfiguration DailyAutomaticBackupStartTime (__added__) +* AWS::FSx::FileSystem.WindowsConfiguration ThroughputCapacity.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::SageMaker::Model.ContainerDefinition ModelPackageName (__added__) +* AWS::SageMaker::Model.ContainerDefinition Image.Required (__changed__) + * Old: true + * New: false +* AWS::ServiceCatalog::CloudFormationProvisionedProduct.ProvisioningParameter Key.Required (__changed__) + * Old: false + * New: true +* AWS::ServiceCatalog::CloudFormationProvisionedProduct.ProvisioningParameter Value.Required (__changed__) + * Old: false + * New: true +* AWS::ServiceCatalog::CloudFormationProvisionedProduct.ProvisioningPreferences StackSetAccounts.DuplicatesAllowed (__added__) +* AWS::ServiceCatalog::CloudFormationProvisionedProduct.ProvisioningPreferences StackSetRegions.DuplicatesAllowed (__added__) +* AWS::Synthetics::Canary.RunConfig MemoryInMB (__added__) +* AWS::Synthetics::Canary.Schedule DurationInSeconds.Required (__changed__) + * Old: true + * New: false +* AWS::WAFv2::RuleGroup.GeoMatchStatement ForwardedIPConfig (__added__) +* AWS::WAFv2::RuleGroup.IPSetReferenceStatement IPSetForwardedIPConfig (__added__) +* AWS::WAFv2::RuleGroup.RateBasedStatementOne ForwardedIPConfig (__added__) +* AWS::WAFv2::RuleGroup.RateBasedStatementTwo ForwardedIPConfig (__added__) +* AWS::WAFv2::WebACL.GeoMatchStatement ForwardedIPConfig (__added__) +* AWS::WAFv2::WebACL.IPSetReferenceStatement IPSetForwardedIPConfig (__added__) +* AWS::WAFv2::WebACL.RateBasedStatementOne ForwardedIPConfig (__added__) +* AWS::WAFv2::WebACL.RateBasedStatementTwo ForwardedIPConfig (__added__) + + # CloudFormation Resource Specification v16.0.0 ## New Resource Types diff --git a/packages/@aws-cdk/cfnspec/cfn.version b/packages/@aws-cdk/cfnspec/cfn.version index 946789e6195a1..e0228cf182652 100644 --- a/packages/@aws-cdk/cfnspec/cfn.version +++ b/packages/@aws-cdk/cfnspec/cfn.version @@ -1 +1 @@ -16.0.0 +16.1.0 diff --git a/packages/@aws-cdk/cfnspec/lib/schema/resource-type.ts b/packages/@aws-cdk/cfnspec/lib/schema/resource-type.ts index aeccfaa04d86f..8a33571504545 100644 --- a/packages/@aws-cdk/cfnspec/lib/schema/resource-type.ts +++ b/packages/@aws-cdk/cfnspec/lib/schema/resource-type.ts @@ -38,7 +38,7 @@ export interface TaggableResource extends ResourceType { } } -export type Attribute = PrimitiveAttribute | ListAttribute; +export type Attribute = PrimitiveAttribute | ListAttribute | MapAttribute; export interface PrimitiveAttribute { PrimitiveType: PrimitiveType; @@ -56,6 +56,13 @@ export interface ComplexListAttribute { ItemType: string; } +export type MapAttribute = PrimitiveMapAttribute; + +export interface PrimitiveMapAttribute { + Type: 'Map'; + PrimitiveItemType: PrimitiveType; +} + /** * Determine if the resource supports tags * @@ -83,6 +90,10 @@ export function isListAttribute(spec: Attribute): spec is ListAttribute { return (spec as ListAttribute).Type === 'List'; } +export function isMapAttribute(spec: Attribute): spec is MapAttribute { + return (spec as MapAttribute).Type === 'Map'; +} + export function isPrimitiveListAttribute(spec: Attribute): spec is PrimitiveListAttribute { return isListAttribute(spec) && !!(spec as PrimitiveListAttribute).PrimitiveItemType; } @@ -91,6 +102,10 @@ export function isComplexListAttribute(spec: Attribute): spec is ComplexListAttr return isListAttribute(spec) && !!(spec as ComplexListAttribute).ItemType; } +export function isPrimitiveMapAttribute(spec: Attribute): spec is PrimitiveMapAttribute { + return isMapAttribute(spec) && !!(spec as PrimitiveMapAttribute).PrimitiveItemType; +} + /** * Type declaration for special values of the "Ref" attribute represents. * diff --git a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json index 2fda3710b204b..32e331c7f9fb0 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json +++ b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json @@ -3512,6 +3512,18 @@ } } }, + "AWS::Athena::DataCatalog.Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-athena-datacatalog-tags.html", + "Properties": { + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-athena-datacatalog-tags.html#cfn-athena-datacatalog-tags-tags", + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::Athena::WorkGroup.EncryptionConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-athena-workgroup-encryptionconfiguration.html", "Properties": { @@ -6424,6 +6436,23 @@ } } }, + "AWS::CodeBuild::Project.BuildStatusConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-project-buildstatusconfig.html", + "Properties": { + "Context": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-project-buildstatusconfig.html#cfn-codebuild-project-buildstatusconfig-context", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "TargetUrl": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-project-buildstatusconfig.html#cfn-codebuild-project-buildstatusconfig-targeturl", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::CodeBuild::Project.CloudWatchLogsConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-project-cloudwatchlogsconfig.html", "Properties": { @@ -6708,6 +6737,12 @@ "Required": false, "UpdateType": "Mutable" }, + "BuildStatusConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-project-source.html#cfn-codebuild-project-source-buildstatusconfig", + "Required": false, + "Type": "BuildStatusConfig", + "UpdateType": "Mutable" + }, "GitCloneDepth": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-project-source.html#cfn-codebuild-project-source-gitclonedepth", "PrimitiveType": "Integer", @@ -11175,6 +11210,23 @@ } } }, + "AWS::EC2::PrefixList.Entry": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-prefixlist-entry.html", + "Properties": { + "Cidr": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-prefixlist-entry.html#cfn-ec2-prefixlist-entry-cidr", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-prefixlist-entry.html#cfn-ec2-prefixlist-entry-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::EC2::SecurityGroup.Egress": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-security-group-rule.html", "Properties": { @@ -15850,6 +15902,29 @@ } } }, + "AWS::Elasticsearch::Domain.AdvancedSecurityOptionsInput": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticsearch-domain-advancedsecurityoptionsinput.html", + "Properties": { + "Enabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticsearch-domain-advancedsecurityoptionsinput.html#cfn-elasticsearch-domain-advancedsecurityoptionsinput-enabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, + "InternalUserDatabaseEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticsearch-domain-advancedsecurityoptionsinput.html#cfn-elasticsearch-domain-advancedsecurityoptionsinput-internaluserdatabaseenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "MasterUserOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticsearch-domain-advancedsecurityoptionsinput.html#cfn-elasticsearch-domain-advancedsecurityoptionsinput-masteruseroptions", + "Required": false, + "Type": "MasterUserOptions", + "UpdateType": "Mutable" + } + } + }, "AWS::Elasticsearch::Domain.CognitoOptions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticsearch-domain-cognitooptions.html", "Properties": { @@ -15879,6 +15954,23 @@ } } }, + "AWS::Elasticsearch::Domain.DomainEndpointOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticsearch-domain-domainendpointoptions.html", + "Properties": { + "EnforceHTTPS": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticsearch-domain-domainendpointoptions.html#cfn-elasticsearch-domain-domainendpointoptions-enforcehttps", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "TLSSecurityPolicy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticsearch-domain-domainendpointoptions.html#cfn-elasticsearch-domain-domainendpointoptions-tlssecuritypolicy", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::Elasticsearch::Domain.EBSOptions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticsearch-domain-ebsoptions.html", "Properties": { @@ -15989,6 +16081,29 @@ } } }, + "AWS::Elasticsearch::Domain.MasterUserOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticsearch-domain-masteruseroptions.html", + "Properties": { + "MasterUserARN": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticsearch-domain-masteruseroptions.html#cfn-elasticsearch-domain-masteruseroptions-masteruserarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "MasterUserName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticsearch-domain-masteruseroptions.html#cfn-elasticsearch-domain-masteruseroptions-masterusername", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "MasterUserPassword": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticsearch-domain-masteruseroptions.html#cfn-elasticsearch-domain-masteruseroptions-masteruserpassword", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::Elasticsearch::Domain.NodeToNodeEncryptionOptions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticsearch-domain-nodetonodeencryptionoptions.html", "Properties": { @@ -16482,6 +16597,24 @@ "AWS::FSx::FileSystem.LustreConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-lustreconfiguration.html", "Properties": { + "AutomaticBackupRetentionDays": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-lustreconfiguration.html#cfn-fsx-filesystem-lustreconfiguration-automaticbackupretentiondays", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "CopyTagsToBackups": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-lustreconfiguration.html#cfn-fsx-filesystem-lustreconfiguration-copytagstobackups", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, + "DailyAutomaticBackupStartTime": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-lustreconfiguration.html#cfn-fsx-filesystem-lustreconfiguration-dailyautomaticbackupstarttime", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "DeploymentType": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-lustreconfiguration.html#cfn-fsx-filesystem-lustreconfiguration-deploymenttype", "PrimitiveType": "String", @@ -16611,7 +16744,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-windowsconfiguration.html#cfn-fsx-filesystem-windowsconfiguration-throughputcapacity", "PrimitiveType": "Integer", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "WeeklyMaintenanceStartTime": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-windowsconfiguration.html#cfn-fsx-filesystem-windowsconfiguration-weeklymaintenancestarttime", @@ -24099,6 +24232,23 @@ } } }, + "AWS::Lambda::Function.FileSystemConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-filesystemconfig.html", + "Properties": { + "Arn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-filesystemconfig.html#cfn-lambda-function-filesystemconfig-arn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "LocalMountPath": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-filesystemconfig.html#cfn-lambda-function-filesystemconfig-localmountpath", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::Lambda::Function.TracingConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-tracingconfig.html", "Properties": { @@ -27076,6 +27226,23 @@ } } }, + "AWS::QLDB::Stream.KinesisConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-qldb-stream-kinesisconfiguration.html", + "Properties": { + "AggregationEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-qldb-stream-kinesisconfiguration.html#cfn-qldb-stream-kinesisconfiguration-aggregationenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, + "StreamArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-qldb-stream-kinesisconfiguration.html#cfn-qldb-stream-kinesisconfiguration-streamarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, "AWS::RDS::DBCluster.DBClusterRole": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-dbcluster-dbclusterrole.html", "Properties": { @@ -29982,6 +30149,78 @@ } } }, + "AWS::SageMaker::EndpointConfig.CaptureContentTypeHeader": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-datacaptureconfig-capturecontenttypeheader.html", + "Properties": { + "CsvContentTypes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-datacaptureconfig-capturecontenttypeheader.html#cfn-sagemaker-endpointconfig-datacaptureconfig-capturecontenttypeheader-csvcontenttypes", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "JsonContentTypes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-datacaptureconfig-capturecontenttypeheader.html#cfn-sagemaker-endpointconfig-datacaptureconfig-capturecontenttypeheader-jsoncontenttypes", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + } + } + }, + "AWS::SageMaker::EndpointConfig.CaptureOption": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-captureoption.html", + "Properties": { + "CaptureMode": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-captureoption.html#cfn-sagemaker-endpointconfig-captureoption-capturemode", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::SageMaker::EndpointConfig.DataCaptureConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-datacaptureconfig.html", + "Properties": { + "CaptureContentTypeHeader": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-datacaptureconfig.html#cfn-sagemaker-endpointconfig-datacaptureconfig-capturecontenttypeheader", + "Required": false, + "Type": "CaptureContentTypeHeader", + "UpdateType": "Immutable" + }, + "CaptureOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-datacaptureconfig.html#cfn-sagemaker-endpointconfig-datacaptureconfig-captureoptions", + "ItemType": "CaptureOption", + "Required": true, + "Type": "List", + "UpdateType": "Immutable" + }, + "DestinationS3Uri": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-datacaptureconfig.html#cfn-sagemaker-endpointconfig-datacaptureconfig-destinations3uri", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "EnableCapture": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-datacaptureconfig.html#cfn-sagemaker-endpointconfig-datacaptureconfig-enablecapture", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, + "InitialSamplingPercentage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-datacaptureconfig.html#cfn-sagemaker-endpointconfig-datacaptureconfig-initialsamplingpercentage", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Immutable" + }, + "KmsKeyId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-datacaptureconfig.html#cfn-sagemaker-endpointconfig-datacaptureconfig-kmskeyid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, "AWS::SageMaker::EndpointConfig.ProductionVariant": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-productionvariant.html", "Properties": { @@ -30041,7 +30280,7 @@ "Image": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-model-containerdefinition.html#cfn-sagemaker-model-containerdefinition-image", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Immutable" }, "Mode": { @@ -30055,6 +30294,12 @@ "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" + }, + "ModelPackageName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-model-containerdefinition.html#cfn-sagemaker-model-containerdefinition-modelpackagename", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" } } }, @@ -30244,13 +30489,13 @@ "Key": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicecatalog-cloudformationprovisionedproduct-provisioningparameter.html#cfn-servicecatalog-cloudformationprovisionedproduct-provisioningparameter-key", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Value": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicecatalog-cloudformationprovisionedproduct-provisioningparameter.html#cfn-servicecatalog-cloudformationprovisionedproduct-provisioningparameter-value", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" } } @@ -30260,6 +30505,7 @@ "Properties": { "StackSetAccounts": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicecatalog-cloudformationprovisionedproduct-provisioningpreferences.html#cfn-servicecatalog-cloudformationprovisionedproduct-provisioningpreferences-stacksetaccounts", + "DuplicatesAllowed": false, "PrimitiveItemType": "String", "Required": false, "Type": "List", @@ -30297,6 +30543,7 @@ }, "StackSetRegions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicecatalog-cloudformationprovisionedproduct-provisioningpreferences.html#cfn-servicecatalog-cloudformationprovisionedproduct-provisioningpreferences-stacksetregions", + "DuplicatesAllowed": false, "PrimitiveItemType": "String", "Required": false, "Type": "List", @@ -30523,6 +30770,12 @@ "AWS::Synthetics::Canary.RunConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-synthetics-canary-runconfig.html", "Properties": { + "MemoryInMB": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-synthetics-canary-runconfig.html#cfn-synthetics-canary-runconfig-memoryinmb", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, "TimeoutInSeconds": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-synthetics-canary-runconfig.html#cfn-synthetics-canary-runconfig-timeoutinseconds", "PrimitiveType": "Integer", @@ -30537,7 +30790,7 @@ "DurationInSeconds": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-synthetics-canary-schedule.html#cfn-synthetics-canary-schedule-durationinseconds", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Expression": { @@ -31271,6 +31524,23 @@ } } }, + "AWS::WAFv2::RuleGroup.ForwardedIPConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-forwardedipconfiguration.html", + "Properties": { + "FallbackBehavior": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-forwardedipconfiguration.html#cfn-wafv2-rulegroup-forwardedipconfiguration-fallbackbehavior", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "HeaderName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-forwardedipconfiguration.html#cfn-wafv2-rulegroup-forwardedipconfiguration-headername", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::WAFv2::RuleGroup.GeoMatchStatement": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-geomatchstatement.html", "Properties": { @@ -31280,6 +31550,35 @@ "Required": false, "Type": "List", "UpdateType": "Mutable" + }, + "ForwardedIPConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-geomatchstatement.html#cfn-wafv2-rulegroup-geomatchstatement-forwardedipconfig", + "Required": false, + "Type": "ForwardedIPConfiguration", + "UpdateType": "Mutable" + } + } + }, + "AWS::WAFv2::RuleGroup.IPSetForwardedIPConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-ipsetforwardedipconfiguration.html", + "Properties": { + "FallbackBehavior": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-ipsetforwardedipconfiguration.html#cfn-wafv2-rulegroup-ipsetforwardedipconfiguration-fallbackbehavior", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "HeaderName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-ipsetforwardedipconfiguration.html#cfn-wafv2-rulegroup-ipsetforwardedipconfiguration-headername", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Position": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-ipsetforwardedipconfiguration.html#cfn-wafv2-rulegroup-ipsetforwardedipconfiguration-position", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" } } }, @@ -31291,6 +31590,12 @@ "PrimitiveType": "String", "Required": true, "UpdateType": "Mutable" + }, + "IPSetForwardedIPConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-ipsetreferencestatement.html#cfn-wafv2-rulegroup-ipsetreferencestatement-ipsetforwardedipconfig", + "Required": false, + "Type": "IPSetForwardedIPConfiguration", + "UpdateType": "Mutable" } } }, @@ -31349,6 +31654,12 @@ "Required": true, "UpdateType": "Mutable" }, + "ForwardedIPConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-ratebasedstatementone.html#cfn-wafv2-rulegroup-ratebasedstatementone-forwardedipconfig", + "Required": false, + "Type": "ForwardedIPConfiguration", + "UpdateType": "Mutable" + }, "Limit": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-ratebasedstatementone.html#cfn-wafv2-rulegroup-ratebasedstatementone-limit", "PrimitiveType": "Integer", @@ -31372,6 +31683,12 @@ "Required": true, "UpdateType": "Mutable" }, + "ForwardedIPConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-ratebasedstatementtwo.html#cfn-wafv2-rulegroup-ratebasedstatementtwo-forwardedipconfig", + "Required": false, + "Type": "ForwardedIPConfiguration", + "UpdateType": "Mutable" + }, "Limit": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-ratebasedstatementtwo.html#cfn-wafv2-rulegroup-ratebasedstatementtwo-limit", "PrimitiveType": "Integer", @@ -31898,6 +32215,23 @@ } } }, + "AWS::WAFv2::WebACL.ForwardedIPConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-forwardedipconfiguration.html", + "Properties": { + "FallbackBehavior": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-forwardedipconfiguration.html#cfn-wafv2-webacl-forwardedipconfiguration-fallbackbehavior", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "HeaderName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-forwardedipconfiguration.html#cfn-wafv2-webacl-forwardedipconfiguration-headername", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::WAFv2::WebACL.GeoMatchStatement": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-geomatchstatement.html", "Properties": { @@ -31907,6 +32241,35 @@ "Required": false, "Type": "List", "UpdateType": "Mutable" + }, + "ForwardedIPConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-geomatchstatement.html#cfn-wafv2-webacl-geomatchstatement-forwardedipconfig", + "Required": false, + "Type": "ForwardedIPConfiguration", + "UpdateType": "Mutable" + } + } + }, + "AWS::WAFv2::WebACL.IPSetForwardedIPConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-ipsetforwardedipconfiguration.html", + "Properties": { + "FallbackBehavior": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-ipsetforwardedipconfiguration.html#cfn-wafv2-webacl-ipsetforwardedipconfiguration-fallbackbehavior", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "HeaderName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-ipsetforwardedipconfiguration.html#cfn-wafv2-webacl-ipsetforwardedipconfiguration-headername", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Position": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-ipsetforwardedipconfiguration.html#cfn-wafv2-webacl-ipsetforwardedipconfiguration-position", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" } } }, @@ -31918,6 +32281,12 @@ "PrimitiveType": "String", "Required": true, "UpdateType": "Mutable" + }, + "IPSetForwardedIPConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-ipsetreferencestatement.html#cfn-wafv2-webacl-ipsetreferencestatement-ipsetforwardedipconfig", + "Required": false, + "Type": "IPSetForwardedIPConfiguration", + "UpdateType": "Mutable" } } }, @@ -32017,6 +32386,12 @@ "Required": true, "UpdateType": "Mutable" }, + "ForwardedIPConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-ratebasedstatementone.html#cfn-wafv2-webacl-ratebasedstatementone-forwardedipconfig", + "Required": false, + "Type": "ForwardedIPConfiguration", + "UpdateType": "Mutable" + }, "Limit": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-ratebasedstatementone.html#cfn-wafv2-webacl-ratebasedstatementone-limit", "PrimitiveType": "Integer", @@ -32040,6 +32415,12 @@ "Required": true, "UpdateType": "Mutable" }, + "ForwardedIPConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-ratebasedstatementtwo.html#cfn-wafv2-webacl-ratebasedstatementtwo-forwardedipconfig", + "Required": false, + "Type": "ForwardedIPConfiguration", + "UpdateType": "Mutable" + }, "Limit": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-ratebasedstatementtwo.html#cfn-wafv2-webacl-ratebasedstatementtwo-limit", "PrimitiveType": "Integer", @@ -32613,7 +32994,7 @@ } } }, - "ResourceSpecificationVersion": "16.0.0", + "ResourceSpecificationVersion": "16.1.0", "ResourceTypes": { "AWS::ACMPCA::Certificate": { "Attributes": { @@ -33043,6 +33424,12 @@ "Required": false, "UpdateType": "Mutable" }, + "EnableBranchAutoDeletion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amplify-app.html#cfn-amplify-app-enablebranchautodeletion", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "EnvironmentVariables": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amplify-app.html#cfn-amplify-app-environmentvariables", "ItemType": "EnvironmentVariable", @@ -33169,6 +33556,13 @@ "Arn": { "PrimitiveType": "String" }, + "AutoSubDomainCreationPatterns": { + "PrimitiveItemType": "String", + "Type": "List" + }, + "AutoSubDomainIAMRole": { + "PrimitiveType": "String" + }, "CertificateRecord": { "PrimitiveType": "String" }, @@ -33178,6 +33572,9 @@ "DomainStatus": { "PrimitiveType": "String" }, + "EnableAutoSubDomain": { + "PrimitiveType": "Boolean" + }, "StatusReason": { "PrimitiveType": "String" } @@ -33190,12 +33587,31 @@ "Required": true, "UpdateType": "Immutable" }, + "AutoSubDomainCreationPatterns": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amplify-domain.html#cfn-amplify-domain-autosubdomaincreationpatterns", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "AutoSubDomainIAMRole": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amplify-domain.html#cfn-amplify-domain-autosubdomainiamrole", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "DomainName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amplify-domain.html#cfn-amplify-domain-domainname", "PrimitiveType": "String", "Required": true, "UpdateType": "Immutable" }, + "EnableAutoSubDomain": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amplify-domain.html#cfn-amplify-domain-enableautosubdomain", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "SubDomainSettings": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amplify-domain.html#cfn-amplify-domain-subdomainsettings", "ItemType": "SubDomainSetting", @@ -36044,6 +36460,42 @@ } } }, + "AWS::Athena::DataCatalog": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-athena-datacatalog.html", + "Properties": { + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-athena-datacatalog.html#cfn-athena-datacatalog-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-athena-datacatalog.html#cfn-athena-datacatalog-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Parameters": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-athena-datacatalog.html#cfn-athena-datacatalog-parameters", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-athena-datacatalog.html#cfn-athena-datacatalog-tags", + "Required": false, + "Type": "Tags", + "UpdateType": "Mutable" + }, + "Type": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-athena-datacatalog.html#cfn-athena-datacatalog-type", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::Athena::NamedQuery": { "Attributes": { "NamedQueryId": { @@ -36237,6 +36689,12 @@ "Type": "MixedInstancesPolicy", "UpdateType": "Mutable" }, + "NewInstancesProtectedFromScaleIn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-group.html#cfn-as-group-newinstancesprotectedfromscalein", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "NotificationConfigurations": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-group.html#cfn-as-group-notificationconfigurations", "DuplicatesAllowed": true, @@ -38056,6 +38514,12 @@ "Required": false, "UpdateType": "Mutable" }, + "ComputePlatform": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codeguruprofiler-profilinggroup.html#cfn-codeguruprofiler-profilinggroup-computeplatform", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "ProfilingGroupName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codeguruprofiler-profilinggroup.html#cfn-codeguruprofiler-profilinggroup-profilinggroupname", "PrimitiveType": "String", @@ -41725,6 +42189,57 @@ } } }, + "AWS::EC2::PrefixList": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "OwnerId": { + "PrimitiveType": "String" + }, + "PrefixListId": { + "PrimitiveType": "String" + }, + "Version": { + "PrimitiveType": "Integer" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-prefixlist.html", + "Properties": { + "AddressFamily": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-prefixlist.html#cfn-ec2-prefixlist-addressfamily", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Entries": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-prefixlist.html#cfn-ec2-prefixlist-entries", + "ItemType": "Entry", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "MaxEntries": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-prefixlist.html#cfn-ec2-prefixlist-maxentries", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + }, + "PrefixListName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-prefixlist.html#cfn-ec2-prefixlist-prefixlistname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-prefixlist.html#cfn-ec2-prefixlist-tags", + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::EC2::Route": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-route.html", "Properties": { @@ -44302,7 +44817,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticache-replicationgroup.html#cfn-elasticache-replicationgroup-multiazenabled", "PrimitiveType": "Boolean", "Required": false, - "UpdateType": "Mutable" + "UpdateType": "Immutable" }, "NodeGroupConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticache-replicationgroup.html#cfn-elasticache-replicationgroup-nodegroupconfiguration", @@ -44803,6 +45318,14 @@ "AWS::ElasticLoadBalancingV2::Listener": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-listener.html", "Properties": { + "AlpnPolicy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-listener.html#cfn-elasticloadbalancingv2-listener-alpnpolicy", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, "Certificates": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-listener.html#cfn-elasticloadbalancingv2-listener-certificates", "DuplicatesAllowed": false, @@ -45137,12 +45660,24 @@ "Type": "Map", "UpdateType": "Mutable" }, + "AdvancedSecurityOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticsearch-domain.html#cfn-elasticsearch-domain-advancedsecurityoptions", + "Required": false, + "Type": "AdvancedSecurityOptionsInput", + "UpdateType": "Immutable" + }, "CognitoOptions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticsearch-domain.html#cfn-elasticsearch-domain-cognitooptions", "Required": false, "Type": "CognitoOptions", "UpdateType": "Mutable" }, + "DomainEndpointOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticsearch-domain.html#cfn-elasticsearch-domain-domainendpointoptions", + "Required": false, + "Type": "DomainEndpointOptions", + "UpdateType": "Mutable" + }, "DomainName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticsearch-domain.html#cfn-elasticsearch-domain-domainname", "PrimitiveType": "String", @@ -45619,7 +46154,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-fsx-filesystem.html#cfn-fsx-filesystem-storagecapacity", "PrimitiveType": "Integer", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "StorageType": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-fsx-filesystem.html#cfn-fsx-filesystem-storagetype", @@ -47880,6 +48415,13 @@ "Required": true, "UpdateType": "Immutable" }, + "SupportedOsVersions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html#cfn-imagebuilder-component-supportedosversions", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html#cfn-imagebuilder-component-tags", "PrimitiveItemType": "String", @@ -47954,6 +48496,12 @@ "Required": false, "UpdateType": "Immutable" }, + "EnhancedImageMetadataEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html#cfn-imagebuilder-image-enhancedimagemetadataenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, "ImageRecipeArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html#cfn-imagebuilder-image-imagerecipearn", "PrimitiveType": "String", @@ -48001,6 +48549,12 @@ "Required": false, "UpdateType": "Mutable" }, + "EnhancedImageMetadataEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-enhancedimagemetadataenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "ImageRecipeArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-imagerecipearn", "PrimitiveType": "String", @@ -48098,6 +48652,12 @@ "PrimitiveType": "String", "Required": true, "UpdateType": "Immutable" + }, + "WorkingDirectory": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagerecipe.html#cfn-imagebuilder-imagerecipe-workingdirectory", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" } } }, @@ -49365,6 +49925,14 @@ "Type": "Environment", "UpdateType": "Mutable" }, + "FileSystemConfigs": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html#cfn-lambda-function-filesystemconfigs", + "DuplicatesAllowed": false, + "ItemType": "FileSystemConfig", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, "FunctionName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html#cfn-lambda-function-functionname", "PrimitiveType": "String", @@ -52670,6 +53238,63 @@ } } }, + "AWS::QLDB::Stream": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "Id": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-qldb-stream.html", + "Properties": { + "ExclusiveEndTime": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-qldb-stream.html#cfn-qldb-stream-exclusiveendtime", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "InclusiveStartTime": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-qldb-stream.html#cfn-qldb-stream-inclusivestarttime", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "KinesisConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-qldb-stream.html#cfn-qldb-stream-kinesisconfiguration", + "Required": true, + "Type": "KinesisConfiguration", + "UpdateType": "Immutable" + }, + "LedgerName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-qldb-stream.html#cfn-qldb-stream-ledgername", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "RoleArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-qldb-stream.html#cfn-qldb-stream-rolearn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "StreamName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-qldb-stream.html#cfn-qldb-stream-streamname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-qldb-stream.html#cfn-qldb-stream-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::RAM::ResourceShare": { "Attributes": { "Arn": { @@ -53407,9 +54032,6 @@ "Attributes": { "TargetGroupArn": { "PrimitiveType": "String" - }, - "TargetGroupName": { - "PrimitiveType": "String" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbproxytargetgroup.html", @@ -53438,7 +54060,13 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbproxytargetgroup.html#cfn-rds-dbproxytargetgroup-dbproxyname", "PrimitiveType": "String", "Required": true, - "UpdateType": "Mutable" + "UpdateType": "Immutable" + }, + "TargetGroupName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbproxytargetgroup.html#cfn-rds-dbproxytargetgroup-targetgroupname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" } } }, @@ -54157,7 +54785,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-route53-healthcheck.html#cfn-route53-healthcheck-healthcheckconfig", "Required": true, "Type": "HealthCheckConfig", - "UpdateType": "Mutable" + "UpdateType": "Conditional" }, "HealthCheckTags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-route53-healthcheck.html#cfn-route53-healthcheck-healthchecktags", @@ -55045,6 +55673,12 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html", "Properties": { + "ApplyOnlyAtCronInterval": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-applyonlyatcroninterval", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "AssociationName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-associationname", "PrimitiveType": "String", @@ -55648,6 +56282,12 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-endpointconfig.html", "Properties": { + "DataCaptureConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-endpointconfig.html#cfn-sagemaker-endpointconfig-datacaptureconfig", + "Required": false, + "Type": "DataCaptureConfig", + "UpdateType": "Immutable" + }, "EndpointConfigName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-endpointconfig.html#cfn-sagemaker-endpointconfig-endpointconfigname", "PrimitiveType": "String", @@ -56116,6 +56756,13 @@ "CloudformationStackArn": { "PrimitiveType": "String" }, + "Outputs": { + "PrimitiveItemType": "String", + "Type": "Map" + }, + "ProvisionedProductId": { + "PrimitiveType": "String" + }, "RecordId": { "PrimitiveType": "String" } @@ -56130,6 +56777,7 @@ }, "NotificationArns": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicecatalog-cloudformationprovisionedproduct.html#cfn-servicecatalog-cloudformationprovisionedproduct-notificationarns", + "DuplicatesAllowed": false, "PrimitiveItemType": "String", "Required": false, "Type": "List", @@ -56591,7 +57239,7 @@ "ItemType": "Tag", "Required": false, "Type": "List", - "UpdateType": "Mutable" + "UpdateType": "Immutable" } } }, @@ -56646,7 +57294,7 @@ "ItemType": "Tag", "Required": false, "Type": "List", - "UpdateType": "Mutable" + "UpdateType": "Immutable" }, "Vpc": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-privatednsnamespace.html#cfn-servicediscovery-privatednsnamespace-vpc", @@ -56684,7 +57332,7 @@ "ItemType": "Tag", "Required": false, "Type": "List", - "UpdateType": "Mutable" + "UpdateType": "Immutable" } } }, @@ -56743,7 +57391,7 @@ "ItemType": "Tag", "Required": false, "Type": "List", - "UpdateType": "Mutable" + "UpdateType": "Immutable" } } }, @@ -56872,7 +57520,7 @@ }, "RunConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-synthetics-canary.html#cfn-synthetics-canary-runconfig", - "Required": true, + "Required": false, "Type": "RunConfig", "UpdateType": "Mutable" }, diff --git a/packages/@aws-cdk/cfnspec/test/spec-validators.ts b/packages/@aws-cdk/cfnspec/test/spec-validators.ts index 8fe789970d0bb..521a61be55682 100644 --- a/packages/@aws-cdk/cfnspec/test/spec-validators.ts +++ b/packages/@aws-cdk/cfnspec/test/spec-validators.ts @@ -138,6 +138,9 @@ function validateAttributes( const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; test.ok(resolvedType, `${typeName}.Attributes.${name} ItemType (${fqn}) resolves`); test.ok(!('PrimitiveItemType' in attribute), `${typeName}.Attributes.${name} has no PrimitiveItemType`); + } else if (schema.isPrimitiveMapAttribute(attribute)) { + test.ok(schema.isPrimitiveType(attribute.PrimitiveItemType), `${typeName}.Attributes.${name} has a valid PrimitiveItemType`); + test.ok(!('ItemType' in attribute), `${typeName}.Attributes.${name} has no ItemType`); } else { test.ok(false, `${typeName}.Attributes.${name} has a valid type`); } From 9b3de1dfbb3113c0bfee482775bf68de2a89653c Mon Sep 17 00:00:00 2001 From: Somaya Date: Wed, 22 Jul 2020 14:14:48 -0700 Subject: [PATCH 05/30] chore: update README (#9125) chore: update README OSDS is updating all of the SDKs&Tools READMEs so that we can have a uniform look, which will make for a better experience for our users. - Reorganize some sections - Rename some section titles - Reword small sections ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- README.md | 64 +++++++++++++++++++++++++++---------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 9b51cbaba4118..d12001783d663 100644 --- a/README.md +++ b/README.md @@ -26,16 +26,18 @@ The CDK is available in the following languages: * Java ([Java ≥ 8](https://www.oracle.com/technetwork/java/javase/downloads/index.html) and [Maven ≥ 3.5.4](https://maven.apache.org/download.cgi)) * .NET ([.NET Core ≥ 3.1](https://dotnet.microsoft.com/download)) -------- - +\ +Jump To: [Developer Guide](https://docs.aws.amazon.com/cdk/latest/guide) | -[CDK Workshop](https://cdkworkshop.com/) | -[Getting Started](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) | [API Reference](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-construct-library.html) | -[Examples](https://github.com/aws-samples/aws-cdk-examples) | +[Getting Started](#getting-started) | [Getting Help](#getting-help) | +[Contributing](#contributing) | [RFCs](https://github.com/aws/aws-cdk-rfcs) | -[Roadmap](https://github.com/aws/aws-cdk/blob/master/ROADMAP.md) +[Roadmap](https://github.com/aws/aws-cdk/blob/master/ROADMAP.md) | +[More Resources](#more-resources) + +------- Developers use the [CDK framework] in one of the supported programming languages to define reusable cloud components called [constructs], which @@ -51,16 +53,20 @@ how to use AWS. The AWS Construct Library aims to reduce the complexity and glue-logic required when integrating various AWS services to achieve your goals on AWS. -[cdk framework]: https://docs.aws.amazon.com/cdk/api/latest/ +[CDK framework]: https://docs.aws.amazon.com/cdk/latest/guide/home.html [constructs]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html -[stacks]: https://docs.aws.amazon.com/cdk/latest/guide/apps_and_stacks.html#stacks -[apps]: https://docs.aws.amazon.com/cdk/latest/guide/apps_and_stacks.html#apps +[stacks]: https://docs.aws.amazon.com/cdk/latest/guide/stacks.html +[apps]: https://docs.aws.amazon.com/cdk/latest/guide/apps.html [Developer Guide]: https://docs.aws.amazon.com/cdk/latest/guide [AWS CDK CLI]: https://docs.aws.amazon.com/cdk/latest/guide/tools.html [AWS Construct Library]: https://docs.aws.amazon.com/cdk/api/latest/docs/aws-construct-library.html -## At a glance +## Getting Started + +For a detailed walkthrough, see the [tutorial](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html#hello_world_tutorial) in the AWS CDK [Developer Guide](https://docs.aws.amazon.com/cdk/latest/guide/home.html). + +### At a glance Install or update the [AWS CDK CLI] from npm (requires [Node.js ≥ 10.13.0](https://nodejs.org/download/release/latest-v10.x/)). We recommend using a version in [Active LTS](https://nodejs.org/en/about/releases/) ⚠️ versions `13.0.0` to `13.6.0` are not supported due to compatibility issues with our dependencies. @@ -108,38 +114,32 @@ Use the `cdk` command-line toolkit to interact with your project: * `cdk synth`: synthesizes an AWS CloudFormation template for your app * `cdk diff`: compares your app with the deployed stack -For a detailed walkthrough, see the [tutorial] in the AWS CDK [Developer Guide]. - ## Getting Help -Please use these community resources for getting help. We use the GitHub issues -for tracking bugs and feature requests. +The best way to interact with our team is through GitHub. You can open an [issue](https://github.com/aws/aws-cdk/issues/new/choose) and choose from one of our templates for bug reports, feature requests, documentation issues, or guidance. + +If you have a support plan with AWS Support, you can also create a new [support case](https://console.aws.amazon.com/support/home#/). +You may also find help on these community resources: +* Look through the [API Reference](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-construct-library.html) or [Developer Guide](https://docs.aws.amazon.com/cdk/latest/guide) * Ask a question on [Stack Overflow](https://stackoverflow.com/questions/tagged/aws-cdk) and tag it with `aws-cdk` * Come join the AWS CDK community on [Gitter](https://gitter.im/awslabs/aws-cdk) * Talk in the CDK channel of the [AWS Developers Slack workspace](https://awsdevelopers.slack.com) (invite required) -* Open a support ticket with [AWS Support](https://console.aws.amazon.com/support/home#/) -* If it turns out that you may have found a bug, - please open an [issue](https://github.com/aws/aws-cdk/issues/new) + +### Roadmap + +The [AWS CDK Roadmap project board](https://github.com/orgs/aws/projects/7) lets developers know about our upcoming features and priorities to help them plan how to best leverage the CDK and identify opportunities to contribute to the project. See [ROADMAP.md](https://github.com/aws/aws-cdk/blob/master/ROADMAP.md) for more information and FAQs. ## Contributing We welcome community contributions and pull requests. See -[CONTRIBUTING](./CONTRIBUTING.md) for information on how to set up a development +[CONTRIBUTING.md](./CONTRIBUTING.md) for information on how to set up a development environment and submit code. -## Roadmap - -The [AWS CDK Roadmap project board] lets developers know about our upcoming features and priorities to help them plan how to best leverage the CDK and identify opportunities to contribute to the project. See [ROADMAP] for more information and FAQs. - -[AWS CDK Roadmap project board]: https://github.com/orgs/aws/projects/7 -[Roadmap]: (https://github.com/aws/aws-cdk/ROADMAP.md) - -## License - -The AWS CDK is distributed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). - -See [LICENSE](./LICENSE) and [NOTICE](./NOTICE) for more information. - -[Tutorial]: https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html#hello_world_tutorial +## More Resources +* [CDK Workshop](https://cdkworkshop.com/) +* [Examples](https://github.com/aws-samples/aws-cdk-examples) +* [Changelog](./CHANGELOG.md) +* [NOTICE](./NOTICE) +* [License](./LICENSE) From d4b68d3040a96451f2b708c512af5afa8fa33bb8 Mon Sep 17 00:00:00 2001 From: AWS CDK Automation <43080478+aws-cdk-automation@users.noreply.github.com> Date: Wed, 22 Jul 2020 23:43:23 +0200 Subject: [PATCH 06/30] feat(cfnspec): cloudformation spec v16.1.0 (#9216) Co-authored-by: AWS CDK Team Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- packages/@aws-cdk/cfnspec/CHANGELOG.md | 23 +++++++ .../cfnspec/spec-source/000_sam.spec.json | 64 +++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index 5c05249b0514e..176c426cca4d5 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,26 @@ + +# Serverless Application Model (SAM) Resource Specification v2016-10-31 + +## New Resource Types + + +## Attribute Changes + + +## Property Changes + + +## Property Type Changes + +* AWS::Serverless::Function.DestinationConfig (__added__) +* AWS::Serverless::Function.OnFailure (__added__) +* AWS::Serverless::Function.DynamoDBEvent BisectBatchOnFunctionError (__added__) +* AWS::Serverless::Function.DynamoDBEvent DestinationConfig (__added__) +* AWS::Serverless::Function.DynamoDBEvent MaximumBatchingWindowInSeconds (__added__) +* AWS::Serverless::Function.DynamoDBEvent MaximumRecordAgeInSeconds (__added__) +* AWS::Serverless::Function.DynamoDBEvent MaximumRetryAttempts (__added__) +* AWS::Serverless::Function.DynamoDBEvent ParallelizationFactor (__added__) + # CloudFormation Resource Specification v16.1.0 ## New Resource Types diff --git a/packages/@aws-cdk/cfnspec/spec-source/000_sam.spec.json b/packages/@aws-cdk/cfnspec/spec-source/000_sam.spec.json index b6eac7d714e02..40aeb5f55049b 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/000_sam.spec.json +++ b/packages/@aws-cdk/cfnspec/spec-source/000_sam.spec.json @@ -260,6 +260,17 @@ } } }, + "AWS::Serverless::Function.DestinationConfig": { + "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#destination-config-object", + "Properties": { + "OnFailure": { + "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#destination-config-object", + "Required": true, + "Type": "OnFailure", + "UpdateType": "Immutable" + } + } + }, "AWS::Serverless::Function.DomainSAMPT": { "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/docs/policy_templates.rst", "Properties": { @@ -280,12 +291,48 @@ "Required": false, "UpdateType": "Immutable" }, + "BisectBatchOnFunctionError": { + "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#dynamodb", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, + "DestinationConfig": { + "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#dynamodb", + "Required": false, + "Type": "DestinationConfig", + "UpdateType": "Immutable" + }, "Enabled": { "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#dynamodb", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Immutable" }, + "MaximumBatchingWindowInSeconds": { + "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#dynamodb", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + }, + "MaximumRecordAgeInSeconds": { + "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#dynamodb", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + }, + "MaximumRetryAttempts": { + "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#dynamodb", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + }, + "ParallelizationFactor": { + "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#dynamodb", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + }, "StartingPosition": { "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#dynamodb", "PrimitiveType": "String", @@ -476,6 +523,23 @@ } } }, + "AWS::Serverless::Function.OnFailure": { + "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#destination-config-object", + "Properties": { + "Destination": { + "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#destination-config-object", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Type": { + "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#destination-config-object", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, "AWS::Serverless::Function.QueueSAMPT": { "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/docs/policy_templates.rst", "Properties": { From 4cc2834e0ef2683b99c4a6258cf104f8a714479f Mon Sep 17 00:00:00 2001 From: Adam Elmore Date: Wed, 22 Jul 2020 17:07:16 -0500 Subject: [PATCH 07/30] feat(lambda-python): introducing LambdaPython (#9182) Higher level API to work with Lambda functions written in Python. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-lambda-python/.eslintrc.js | 3 + .../@aws-cdk/aws-lambda-python/.gitignore | 20 ++ .../@aws-cdk/aws-lambda-python/.npmignore | 26 +++ packages/@aws-cdk/aws-lambda-python/LICENSE | 201 ++++++++++++++++++ packages/@aws-cdk/aws-lambda-python/NOTICE | 2 + packages/@aws-cdk/aws-lambda-python/README.md | 39 ++++ .../@aws-cdk/aws-lambda-python/jest.config.js | 10 + .../aws-lambda-python/lib/bundling.ts | 49 +++++ .../aws-lambda-python/lib/function.ts | 74 +++++++ .../@aws-cdk/aws-lambda-python/lib/index.ts | 1 + .../@aws-cdk/aws-lambda-python/package.json | 88 ++++++++ .../aws-lambda-python/test/bundling.test.ts | 79 +++++++ .../aws-lambda-python/test/function.test.ts | 72 +++++++ .../test/integ.function.expected.json | 113 ++++++++++ .../aws-lambda-python/test/integ.function.ts | 28 +++ .../lambda-handler-sub/inner/custom_index.py | 0 .../test/lambda-handler/index.py | 11 + .../test/lambda-handler/requirements.txt | 2 + packages/@aws-cdk/aws-lambda/README.md | 5 +- packages/decdk/package.json | 1 + packages/monocdk-experiment/package.json | 1 + 21 files changed, 823 insertions(+), 2 deletions(-) create mode 100644 packages/@aws-cdk/aws-lambda-python/.eslintrc.js create mode 100644 packages/@aws-cdk/aws-lambda-python/.gitignore create mode 100644 packages/@aws-cdk/aws-lambda-python/.npmignore create mode 100644 packages/@aws-cdk/aws-lambda-python/LICENSE create mode 100644 packages/@aws-cdk/aws-lambda-python/NOTICE create mode 100644 packages/@aws-cdk/aws-lambda-python/README.md create mode 100644 packages/@aws-cdk/aws-lambda-python/jest.config.js create mode 100644 packages/@aws-cdk/aws-lambda-python/lib/bundling.ts create mode 100644 packages/@aws-cdk/aws-lambda-python/lib/function.ts create mode 100644 packages/@aws-cdk/aws-lambda-python/lib/index.ts create mode 100644 packages/@aws-cdk/aws-lambda-python/package.json create mode 100644 packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts create mode 100644 packages/@aws-cdk/aws-lambda-python/test/function.test.ts create mode 100644 packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json create mode 100644 packages/@aws-cdk/aws-lambda-python/test/integ.function.ts create mode 100644 packages/@aws-cdk/aws-lambda-python/test/lambda-handler-sub/inner/custom_index.py create mode 100644 packages/@aws-cdk/aws-lambda-python/test/lambda-handler/index.py create mode 100644 packages/@aws-cdk/aws-lambda-python/test/lambda-handler/requirements.txt diff --git a/packages/@aws-cdk/aws-lambda-python/.eslintrc.js b/packages/@aws-cdk/aws-lambda-python/.eslintrc.js new file mode 100644 index 0000000000000..61dd8dd001f63 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-lambda-python/.gitignore b/packages/@aws-cdk/aws-lambda-python/.gitignore new file mode 100644 index 0000000000000..5a6d23ad32d07 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/.gitignore @@ -0,0 +1,20 @@ +*.js +tsconfig.json +*.js.map +*.d.ts +*.generated.ts +dist +lib/generated/resources.ts +.jsii + +.LAST_BUILD +.nyc_output +coverage +nyc.config.js +.LAST_PACKAGE +*.snk + +!.eslintrc.js +!jest.config.js + +junit.xml diff --git a/packages/@aws-cdk/aws-lambda-python/.npmignore b/packages/@aws-cdk/aws-lambda-python/.npmignore new file mode 100644 index 0000000000000..b70bd617fba2d --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/.npmignore @@ -0,0 +1,26 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml diff --git a/packages/@aws-cdk/aws-lambda-python/LICENSE b/packages/@aws-cdk/aws-lambda-python/LICENSE new file mode 100644 index 0000000000000..b71ec1688783a --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-lambda-python/NOTICE b/packages/@aws-cdk/aws-lambda-python/NOTICE new file mode 100644 index 0000000000000..bfccac9a7f69c --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-lambda-python/README.md b/packages/@aws-cdk/aws-lambda-python/README.md new file mode 100644 index 0000000000000..1e89a206f869a --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/README.md @@ -0,0 +1,39 @@ +## Amazon Lambda Python Library + +--- + +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. They are subject to non-backward compatible changes or removal in any future version. These are not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be announced in the release notes. This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +This library provides constructs for Python Lambda functions. + +To use this module, you will need to have Docker installed. + +### Python Function +Define a `PythonFunction`: + +```ts +new lambda.PythonFunction(this, 'MyFunction', { + entry: '/path/to/my/function', // required + index: 'my_index.py', // optional, defaults to 'index.py' + handler: 'my_exported_func', // optional, defaults to 'handler' + runtime: lambda.Runtime.PYTHON_3_6 // optional, defaults to lambda.Runtime.PYTHON_3_7 +}); +``` + +All other properties of `lambda.Function` are supported, see also the [AWS Lambda construct library](https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/aws-lambda). + +### Module Dependencies + +If `requirements.txt` exists at the entry path, the construct will handle installing +all required modules in a [Lambda compatible Docker container](https://github.com/lambci/docker-lambda) +according to the `runtime`. +``` +. +├── lambda_function.py # exports a function named 'handler' +├── requirements.txt # has to be present at the entry path +``` diff --git a/packages/@aws-cdk/aws-lambda-python/jest.config.js b/packages/@aws-cdk/aws-lambda-python/jest.config.js new file mode 100644 index 0000000000000..fc310b5014407 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + ...baseConfig.coverageThreshold.global, + branches: 60, + }, + }, +}; diff --git a/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts b/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts new file mode 100644 index 0000000000000..22cec7c841bde --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts @@ -0,0 +1,49 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk from '@aws-cdk/core'; + +/** + * Options for bundling + */ +export interface BundlingOptions { + /** + * Entry path + */ + readonly entry: string; + + /** + * The runtime of the lambda function + */ + readonly runtime: lambda.Runtime; +} + +/** + * Produce bundled Lambda asset code + */ +export function bundle(options: BundlingOptions): lambda.AssetCode { + let installer = options.runtime === lambda.Runtime.PYTHON_2_7 ? Installer.PIP : Installer.PIP3; + + let hasRequirements = fs.existsSync(path.join(options.entry, 'requirements.txt')); + + let depsCommand = chain([ + hasRequirements ? `${installer} install -r requirements.txt -t ${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}` : '', + `rsync -r . ${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}`, + ]); + + return lambda.Code.fromAsset(options.entry, { + bundling: { + image: options.runtime.bundlingDockerImage, + command: ['bash', '-c', depsCommand], + }, + }); +} + +enum Installer { + PIP = 'pip', + PIP3 = 'pip3', +} + +function chain(commands: string[]): string { + return commands.filter(c => !!c).join(' && '); +} diff --git a/packages/@aws-cdk/aws-lambda-python/lib/function.ts b/packages/@aws-cdk/aws-lambda-python/lib/function.ts new file mode 100644 index 0000000000000..7b3fbb1d71fe8 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/lib/function.ts @@ -0,0 +1,74 @@ +import * as fs from 'fs'; +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 a PythonFunction + */ +export interface PythonFunctionProps extends lambda.FunctionOptions { + /** + * The path to the root directory of the function. + */ + readonly entry: string; + + /** + * The path (relative to entry) to the index file containing the exported handler. + * + * @default index.py + */ + readonly index?: string; + + /** + * The name of the exported handler in the index file. + * + * @default handler + */ + readonly handler?: string; + + /** + * The runtime environment. Only runtimes of the Python family are + * supported. + * + * @default lambda.Runtime.PYTHON_3_7 + */ + readonly runtime?: lambda.Runtime; +} + +/** + * A Python Lambda function + */ +export class PythonFunction extends lambda.Function { + constructor(scope: cdk.Construct, id: string, props: PythonFunctionProps) { + if (props.runtime && props.runtime.family !== lambda.RuntimeFamily.PYTHON) { + throw new Error('Only `PYTHON` runtimes are supported.'); + } + if (props.index && !/\.py$/.test(props.index)) { + throw new Error('Only Python (.py) index files are supported.'); + } + + // Entry and defaults + const entry = path.resolve(props.entry); + const index = props.index ?? 'index.py'; + + const resolvedIndex = path.resolve(entry, index); + if (!fs.existsSync(resolvedIndex)) { + throw new Error(`Cannot find index file at ${resolvedIndex}`); + } + + const handler = props.handler ?? 'handler'; + const runtime = props.runtime ?? lambda.Runtime.PYTHON_3_7; + + super(scope, id, { + ...props, + runtime, + code: bundle({ + ...props, + entry, + runtime, + }), + 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 new file mode 100644 index 0000000000000..2653adb2a89e8 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/lib/index.ts @@ -0,0 +1 @@ +export * from './function'; diff --git a/packages/@aws-cdk/aws-lambda-python/package.json b/packages/@aws-cdk/aws-lambda-python/package.json new file mode 100644 index 0000000000000..ca24866ea79b1 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/package.json @@ -0,0 +1,88 @@ +{ + "name": "@aws-cdk/aws-lambda-python", + "version": "0.0.0", + "description": "CDK Constructs for AWS Lambda in Python", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awscdk.services.lambda.python", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "lambda-python" + } + }, + "dotnet": { + "namespace": "Amazon.CDK.AWS.Lambda.Python", + "packageId": "Amazon.CDK.AWS.Lambda.Python", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-cdk.aws-lambda-python", + "module": "aws_cdk.aws_lambda_python" + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-lambda-python" + }, + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "build+test+package": "npm run build+test && npm run package", + "build+test": "npm run build && npm test", + "compat": "cdk-compat" + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "lambda" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", + "pkglint": "0.0.0" + }, + "dependencies": { + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.0.2" + }, + "homepage": "https://github.com/aws/aws-cdk", + "peerDependencies": { + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.0.2" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "experimental", + "awscdkio": { + "announce": false + }, + "cdk-build": { + "jest": true + } +} diff --git a/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts b/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts new file mode 100644 index 0000000000000..492f7b3dbd890 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts @@ -0,0 +1,79 @@ +import * as fs from 'fs'; +import { Code, Runtime } from '@aws-cdk/aws-lambda'; +import { bundle } from '../lib/bundling'; + +jest.mock('@aws-cdk/aws-lambda'); +const existsSyncOriginal = fs.existsSync; +const existsSyncMock = jest.spyOn(fs, 'existsSync'); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +test('Bundling', () => { + bundle({ + entry: '/project/folder', + runtime: Runtime.PYTHON_3_7, + }); + + // Correctly bundles + expect(Code.fromAsset).toHaveBeenCalledWith('/project/folder', { + bundling: expect.objectContaining({ + command: [ + 'bash', '-c', + 'rsync -r . /asset-output', + ], + }), + }); + + // Searches for requirements.txt in entry + expect(existsSyncMock).toHaveBeenCalledWith('/project/folder/requirements.txt'); +}); + +test('Bundling with requirements.txt installed', () => { + existsSyncMock.mockImplementation((p: fs.PathLike) => { + if (/requirements.txt/.test(p.toString())) { + return true; + } + return existsSyncOriginal(p); + }); + + bundle({ + entry: '/project/folder', + runtime: Runtime.PYTHON_3_7, + }); + + // Correctly bundles with requirements.txt pip installed + expect(Code.fromAsset).toHaveBeenCalledWith('/project/folder', { + bundling: expect.objectContaining({ + command: [ + 'bash', '-c', + 'pip3 install -r requirements.txt -t /asset-output && rsync -r . /asset-output', + ], + }), + }); +}); + +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); + }); + + bundle({ + entry: '/project/folder', + runtime: Runtime.PYTHON_2_7, + }); + + // Correctly bundles with requirements.txt pip installed + expect(Code.fromAsset).toHaveBeenCalledWith('/project/folder', { + bundling: expect.objectContaining({ + command: [ + 'bash', '-c', + 'pip install -r requirements.txt -t /asset-output && rsync -r . /asset-output', + ], + }), + }); +}); diff --git a/packages/@aws-cdk/aws-lambda-python/test/function.test.ts b/packages/@aws-cdk/aws-lambda-python/test/function.test.ts new file mode 100644 index 0000000000000..9bc69a7757946 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/function.test.ts @@ -0,0 +1,72 @@ +import '@aws-cdk/assert/jest'; +import { Runtime } from '@aws-cdk/aws-lambda'; +import { Stack } from '@aws-cdk/core'; +import { PythonFunction } from '../lib'; +import { bundle } from '../lib/bundling'; + +jest.mock('../lib/bundling', () => { + return { + bundle: jest.fn().mockReturnValue({ + bind: () => { + return { inlineCode: 'code' }; + }, + bindToResource: () => { return; }, + }), + }; +}); + +let stack: Stack; +beforeEach(() => { + stack = new Stack(); + jest.clearAllMocks(); +}); + +test('PythonFunction with defaults', () => { + new PythonFunction(stack, 'handler', { + entry: 'test/lambda-handler', + }); + + expect(bundle).toHaveBeenCalledWith(expect.objectContaining({ + entry: expect.stringMatching(/@aws-cdk\/aws-lambda-python\/test\/lambda-handler$/), + })); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Handler: 'index.handler', + }); +}); + +test('PythonFunction with index in a subdirectory', () => { + new PythonFunction(stack, 'handler', { + entry: 'test/lambda-handler-sub', + index: 'inner/custom_index.py', + handler: 'custom_handler', + }); + + expect(bundle).toHaveBeenCalledWith(expect.objectContaining({ + entry: expect.stringMatching(/@aws-cdk\/aws-lambda-python\/test\/lambda-handler-sub$/), + })); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Handler: 'inner/custom_index.custom_handler', + }); +}); + +test('throws when index is not py', () => { + expect(() => new PythonFunction(stack, 'Fn', { + entry: 'test/lambda-handler', + index: 'index.js', + })).toThrow(/Only Python \(\.py\) index files are supported/); +}); + +test('throws when entry does not exist', () => { + expect(() => new PythonFunction(stack, 'Fn', { + entry: 'notfound', + })).toThrow(/Cannot find index file at/); +}); + +test('throws with the wrong runtime family', () => { + expect(() => new PythonFunction(stack, 'handler1', { + entry: 'test/lambda-handler', + runtime: Runtime.NODEJS_12_X, + })).toThrow(/Only `PYTHON` runtimes are supported/); +}); 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 new file mode 100644 index 0000000000000..7466ec4d88601 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json @@ -0,0 +1,113 @@ +{ + "Resources": { + "myhandlerServiceRole77891068": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "myhandlerD202FA8E": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeS3Bucket0552B5BB" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeS3VersionKey9522AC10" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeS3VersionKey9522AC10" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "myhandlerServiceRole77891068", + "Arn" + ] + }, + "Runtime": "python3.6" + }, + "DependsOn": [ + "myhandlerServiceRole77891068" + ] + } + }, + "Parameters": { + "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeS3Bucket0552B5BB": { + "Type": "String", + "Description": "S3 bucket for asset \"822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1ade\"" + }, + "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeS3VersionKey9522AC10": { + "Type": "String", + "Description": "S3 key for asset version \"822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1ade\"" + }, + "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeArtifactHash3CE06D09": { + "Type": "String", + "Description": "Artifact hash for asset \"822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1ade\"" + } + }, + "Outputs": { + "FunctionArn": { + "Value": { + "Fn::GetAtt": [ + "myhandlerD202FA8E", + "Arn" + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.ts b/packages/@aws-cdk/aws-lambda-python/test/integ.function.ts new file mode 100644 index 0000000000000..8da5f2a82e12d --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.ts @@ -0,0 +1,28 @@ +import * as path from 'path'; +import { Runtime } from '@aws-cdk/aws-lambda'; +import { App, CfnOutput, Construct, Stack, StackProps } from '@aws-cdk/core'; +import * as lambda from '../lib'; + +/* + * Stack verification steps: + * * aws lambda invoke --function-name --invocation-type Event --payload '"OK"' response.json + */ + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const fn = new lambda.PythonFunction(this, 'my_handler', { + entry: path.join(__dirname, 'lambda-handler'), + runtime: Runtime.PYTHON_3_6, + }); + + new CfnOutput(this, 'FunctionArn', { + value: fn.functionArn, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-lambda-python'); +app.synth(); diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-sub/inner/custom_index.py b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-sub/inner/custom_index.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/index.py b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/index.py new file mode 100644 index 0000000000000..25d1fc8520d7f --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/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).raw + img = Image.open(response) + + print(response.status_code) + print(img.size) + + return response.status_code diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/requirements.txt b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/requirements.txt new file mode 100644 index 0000000000000..288e2971fb79a --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/requirements.txt @@ -0,0 +1,2 @@ +requests==2.23.0 +pillow==7.2.0 diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index 7797c913ec40f..d9bd0466c3ed9 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -282,7 +282,7 @@ The `logRetention` property can be used to set a different expiration period. It is possible to obtain the function's log group as a `logs.ILogGroup` by calling the `logGroup` property of the `Function` construct. -By default, CDK uses the AWS SDK retry options when creating a log group. The `logRetentionRetryOptions` property +By default, CDK uses the AWS SDK retry options when creating a log group. The `logRetentionRetryOptions` property allows you to customize the maximum number of retries and base backoff duration. *Note* that, if either `logRetention` is set or `logGroup` property is called, a [CloudFormation custom @@ -297,7 +297,7 @@ the log retention to never expire even if it was configured with a different val You can configure a function to mount an Amazon Elastic File System (Amazon EFS) to a directory in your runtime environment with the `filesystem` property. To access Amazon EFS -from lambda function, the Amazon EFS access point will be required. +from lambda function, the Amazon EFS access point will be required. The following sample allows the lambda function to mount the Amazon EFS access point to `/mnt/msg` in the runtime environment and access the filesystem with the POSIX identity defined in `posixUser`. @@ -398,3 +398,4 @@ new lambda.Function(this, 'Function', { Language-specific higher level constructs are provided in separate modules: * Node.js: [`@aws-cdk/aws-lambda-nodejs`](https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/aws-lambda-nodejs) +* Python: [`@aws-cdk/aws-lambda-python`](https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/aws-lambda-python) diff --git a/packages/decdk/package.json b/packages/decdk/package.json index ad17de632e09a..4542512a7c10d 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -122,6 +122,7 @@ "@aws-cdk/aws-lambda-destinations": "0.0.0", "@aws-cdk/aws-lambda-event-sources": "0.0.0", "@aws-cdk/aws-lambda-nodejs": "0.0.0", + "@aws-cdk/aws-lambda-python": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-logs-destinations": "0.0.0", "@aws-cdk/aws-macie": "0.0.0", diff --git a/packages/monocdk-experiment/package.json b/packages/monocdk-experiment/package.json index 73a4bd12d2a82..70ba917eb6c21 100644 --- a/packages/monocdk-experiment/package.json +++ b/packages/monocdk-experiment/package.json @@ -190,6 +190,7 @@ "@aws-cdk/aws-lambda-destinations": "0.0.0", "@aws-cdk/aws-lambda-event-sources": "0.0.0", "@aws-cdk/aws-lambda-nodejs": "0.0.0", + "@aws-cdk/aws-lambda-python": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-logs-destinations": "0.0.0", "@aws-cdk/aws-macie": "0.0.0", From 8e73608365137fa2b418804489c74abad6c7e852 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 23 Jul 2020 00:28:33 +0200 Subject: [PATCH 08/30] chore(core): do not use console.error in bundling (#9213) Use `process.stderr.write` instead to avoid alerting test runners. Closes #9210 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/core/lib/asset-staging.ts | 3 +-- packages/@aws-cdk/core/test/test.staging.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/core/lib/asset-staging.ts b/packages/@aws-cdk/core/lib/asset-staging.ts index 92a2b6b9bdf97..d042ec90b1f4b 100644 --- a/packages/@aws-cdk/core/lib/asset-staging.ts +++ b/packages/@aws-cdk/core/lib/asset-staging.ts @@ -171,8 +171,7 @@ export class AssetStaging extends Construct { ]; try { - // eslint-disable-next-line no-console - console.error(`Bundling asset ${this.node.path}...`); + process.stderr.write(`Bundling asset ${this.node.path}...\n`); options.image._run({ command: options.command, user, diff --git a/packages/@aws-cdk/core/test/test.staging.ts b/packages/@aws-cdk/core/test/test.staging.ts index bff677342bf7f..6fb2a6f80ab4b 100644 --- a/packages/@aws-cdk/core/test/test.staging.ts +++ b/packages/@aws-cdk/core/test/test.staging.ts @@ -108,7 +108,7 @@ export = { const ensureDirSyncSpy = sinon.spy(fs, 'ensureDirSync'); const mkdtempSyncSpy = sinon.spy(fs, 'mkdtempSync'); const chmodSyncSpy = sinon.spy(fs, 'chmodSync'); - const consoleErrorSpy = sinon.spy(console, 'error'); + const processStdErrWriteSpy = sinon.spy(process.stderr, 'write'); // WHEN new AssetStaging(stack, 'Asset', { @@ -140,7 +140,7 @@ export = { test.ok(chmodSyncSpy.calledWith(sinon.match(path.join(stagingTmp, 'asset-bundle-')), 0o777)); // shows a message before bundling - test.ok(consoleErrorSpy.calledWith('Bundling asset stack/Asset...')); + test.ok(processStdErrWriteSpy.calledWith('Bundling asset stack/Asset...\n')); test.done(); }, From d28752513175c94fb2bc4da43374d7f2e66d6550 Mon Sep 17 00:00:00 2001 From: comcalvi <66279577+comcalvi@users.noreply.github.com> Date: Wed, 22 Jul 2020 21:02:19 -0400 Subject: [PATCH 09/30] feat(cfn-include): handle resources not in the CloudFormation schema (#9199) Closes #9197 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/cloudformation-include/README.md | 4 ++ .../cloudformation-include/lib/cfn-include.ts | 68 +++++++----------- .../test/nested-stacks.test.ts | 17 ++++- .../custom-resource-with-attributes.json | 44 ++++++++++++ .../custom-resource-with-bad-condition.json | 8 +++ .../nested/custom-resource.json | 7 ++ .../non-existent-resource-type.json | 7 -- .../test/valid-templates.test.ts | 16 ++++- packages/@aws-cdk/core/lib/cfn-parse.ts | 39 +++++++++- packages/@aws-cdk/core/test/test.cfn-parse.ts | 72 ------------------- tools/cfn2ts/lib/codegen.ts | 31 +------- 11 files changed, 154 insertions(+), 159 deletions(-) create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/custom-resource-with-attributes.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/custom-resource-with-bad-condition.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/nested/custom-resource.json delete mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/non-existent-resource-type.json delete mode 100644 packages/@aws-cdk/core/test/test.cfn-parse.ts diff --git a/packages/@aws-cdk/cloudformation-include/README.md b/packages/@aws-cdk/cloudformation-include/README.md index e8ea292934958..4a591bc74f150 100644 --- a/packages/@aws-cdk/cloudformation-include/README.md +++ b/packages/@aws-cdk/cloudformation-include/README.md @@ -106,6 +106,10 @@ const bucket = s3.Bucket.fromBucketName(this, 'L2Bucket', cfnBucket.ref); // bucket is of type s3.IBucket ``` +Note that [Custom Resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cfn-customresource.html) +will be of type CfnResource, and hence won't need to be casted. +This holds for any resource that isn't in the CloudFormation schema. + ## Conditions If your template uses [CloudFormation Conditions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html), diff --git a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts index d8c6abd702448..a5f1c23d0a857 100644 --- a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts +++ b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts @@ -311,12 +311,7 @@ export class CfnInclude extends core.CfnElement { } const resourceAttributes: any = this.template.Resources[logicalId]; - const l1ClassFqn = cfn_type_to_l1_mapping.lookup(resourceAttributes.Type); - if (!l1ClassFqn) { - // currently, we only handle types we know the L1 for - - // in the future, we might construct an instance of CfnResource instead - throw new Error(`Unrecognized CloudFormation resource type: '${resourceAttributes.Type}'`); - } + // fail early for resource attributes we don't support yet const knownAttributes = [ 'Type', 'Properties', 'Condition', 'DependsOn', 'Metadata', @@ -329,9 +324,6 @@ export class CfnInclude extends core.CfnElement { } } - const [moduleName, ...className] = l1ClassFqn.split('.'); - const module = require(moduleName); // eslint-disable-line @typescript-eslint/no-require-imports - const jsClassFromModule = module[className.join('.')]; const self = this; const finder: core.ICfnFinder = { findCondition(conditionName: string): core.CfnCondition | undefined { @@ -353,13 +345,31 @@ export class CfnInclude extends core.CfnElement { return this.findResource(elementName); }, }; - const options: core.FromCloudFormationOptions = { + const cfnParser = new cfn_parse.CfnParser({ finder, - }; + }); - const l1Instance = this.nestedStacksToInclude[logicalId] - ? this.createNestedStack(logicalId, finder) - : jsClassFromModule.fromCloudFormation(this, logicalId, resourceAttributes, options); + let l1Instance: core.CfnResource; + if (this.nestedStacksToInclude[logicalId]) { + l1Instance = this.createNestedStack(logicalId, cfnParser); + } else { + const l1ClassFqn = cfn_type_to_l1_mapping.lookup(resourceAttributes.Type); + if (l1ClassFqn) { + const options: core.FromCloudFormationOptions = { + finder, + }; + const [moduleName, ...className] = l1ClassFqn.split('.'); + const module = require(moduleName); // eslint-disable-line @typescript-eslint/no-require-imports + const jsClassFromModule = module[className.join('.')]; + l1Instance = jsClassFromModule.fromCloudFormation(this, logicalId, resourceAttributes, options); + } else { + l1Instance = new core.CfnResource(this, logicalId, { + type: resourceAttributes.Type, + properties: cfnParser.parseValue(resourceAttributes.Properties), + }); + cfnParser.handleAttributes(l1Instance, resourceAttributes, logicalId); + } + } if (this.preserveLogicalIds) { // override the logical ID to match the original template @@ -370,7 +380,7 @@ export class CfnInclude extends core.CfnElement { return l1Instance; } - private createNestedStack(nestedStackId: string, finder: core.ICfnFinder): core.CfnResource { + private createNestedStack(nestedStackId: string, cfnParser: cfn_parse.CfnParser): core.CfnResource { const templateResources = this.template.Resources || {}; const nestedStackAttributes = templateResources[nestedStackId] || {}; @@ -384,9 +394,6 @@ export class CfnInclude extends core.CfnElement { throw new Error('UpdatePolicy is not supported by the AWS::CloudFormation::Stack resource'); } - const cfnParser = new cfn_parse.CfnParser({ - finder, - }); const nestedStackProps = cfnParser.parseValue(nestedStackAttributes.Properties); const nestedStack = new core.NestedStack(this, nestedStackId, { parameters: nestedStackProps.Parameters, @@ -396,30 +403,7 @@ export class CfnInclude extends core.CfnElement { // we know this is never undefined for nested stacks const nestedStackResource: core.CfnResource = nestedStack.nestedStackResource!; - // handle resource attributes - const cfnOptions = nestedStackResource.cfnOptions; - cfnOptions.metadata = cfnParser.parseValue(nestedStackAttributes.Metadata); - cfnOptions.deletionPolicy = cfnParser.parseDeletionPolicy(nestedStackAttributes.DeletionPolicy); - cfnOptions.updateReplacePolicy = cfnParser.parseDeletionPolicy(nestedStackAttributes.UpdateReplacePolicy); - // handle DependsOn - nestedStackAttributes.DependsOn = nestedStackAttributes.DependsOn ?? []; - const dependencies: string[] = Array.isArray(nestedStackAttributes.DependsOn) ? - nestedStackAttributes.DependsOn : [nestedStackAttributes.DependsOn]; - for (const dep of dependencies) { - const depResource = finder.findResource(dep); - if (!depResource) { - throw new Error(`nested stack '${nestedStackId}' depends on '${dep}' that doesn't exist`); - } - nestedStackResource.node.addDependency(depResource); - } - // handle Condition - if (nestedStackAttributes.Condition) { - const condition = finder.findCondition(nestedStackAttributes.Condition); - if (!condition) { - throw new Error(`nested stack '${nestedStackId}' uses Condition '${nestedStackAttributes.Condition}' that doesn't exist`); - } - cfnOptions.condition = condition; - } + cfnParser.handleAttributes(nestedStackResource, nestedStackAttributes, nestedStackId); const propStack = this.nestedStacksToInclude[nestedStackId]; const template = new CfnInclude(nestedStack, nestedStackId, { diff --git a/packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts b/packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts index 0d16cdfa057cc..b682df6e752f4 100644 --- a/packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts @@ -144,7 +144,7 @@ describe('CDK Include', () => { }, }, }); - }).toThrow(/nested stack 'ChildStack' uses Condition 'FakeCondition' that doesn't exist/); + }).toThrow(/Resource 'ChildStack' uses Condition 'FakeCondition' that doesn't exist/); }); test('throws an exception when a nested stacks depends on a resource that does not exist in the template', () => { @@ -157,7 +157,20 @@ describe('CDK Include', () => { }, }, }); - }).toThrow(/nested stack 'ChildStack' depends on 'AFakeResource' that doesn't exist/); + }).toThrow(/Resource 'ChildStack' depends on 'AFakeResource' that doesn't exist/); + }); + + test('throws an exception when an ID was passed in nestedStacks that is a resource type not in the CloudFormation schema', () => { + expect(() => { + new inc.CfnInclude(stack, 'Template', { + templateFile: testTemplateFilePath('custom-resource.json'), + nestedStacks: { + 'CustomResource': { + templateFile: testTemplateFilePath('whatever.json'), + }, + }, + }); + }).toThrow(/Nested Stack with logical ID 'CustomResource' is not an AWS::CloudFormation::Stack resource/); }); test('can modify resources in nested stacks', () => { diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/custom-resource-with-attributes.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/custom-resource-with-attributes.json new file mode 100644 index 0000000000000..b99e4cc2c0eea --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/custom-resource-with-attributes.json @@ -0,0 +1,44 @@ +{ + "Conditions": { + "AlwaysFalseCond": { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "completely-made-up-region" + ] + } + }, + "Resources": { + "CustomBucket": { + "Type": "AWS::MyService::Custom", + "Condition": "AlwaysFalseCond", + "Metadata": { + "Object1": "Value1", + "Object2": "Value2" + }, + "CreationPolicy": { + "AutoScalingCreationPolicy": { + "MinSuccessfulInstancesPercent" : 90 + } + }, + "DeletionPolicy": "Retain", + "DependsOn": [ "CustomResource" ] + }, + "CustomResource": { + "Type": "AWS::MyService::AnotherCustom", + "Properties": { + "CustomProp": "CustomValue", + "CustomFuncProp": { + "Ref": "AWS::NoValue" + } + }, + "UpdatePolicy": { + "AutoScalingReplacingUpdate": { + "WillReplace" : "false" + } + }, + "UpdateReplacePolicy": "Retain" + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/custom-resource-with-bad-condition.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/custom-resource-with-bad-condition.json new file mode 100644 index 0000000000000..43365aa151e9c --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/custom-resource-with-bad-condition.json @@ -0,0 +1,8 @@ +{ + "Resources": { + "CustomResource": { + "Type": "AWS::MyService::Custom", + "Condition": "AlwaysFalseCond" + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/custom-resource.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/custom-resource.json new file mode 100644 index 0000000000000..23c13fd6946bc --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/nested/custom-resource.json @@ -0,0 +1,7 @@ +{ + "Resources": { + "CustomResource": { + "Type": "AWS::CustomResource::Type" + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/non-existent-resource-type.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/non-existent-resource-type.json deleted file mode 100644 index 9307ef8976f2d..0000000000000 --- a/packages/@aws-cdk/cloudformation-include/test/test-templates/non-existent-resource-type.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "Resources": { - "Bucket": { - "Type": "AWS::FakeService::DoesNotExist" - } - } -} diff --git a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts index 751708edadb16..912525b7c369f 100644 --- a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts @@ -444,10 +444,20 @@ describe('CDK Include', () => { }); }); - test("throws an exception when encountering a Resource type it doesn't recognize", () => { + test('can include a template with a custom resource that uses attributes', () => { + const cfnTemplate = includeTestTemplate(stack, 'custom-resource-with-attributes.json'); + expect(stack).toMatchTemplate( + loadTestFileToJsObject('custom-resource-with-attributes.json'), + ); + + const alwaysFalseCondition = cfnTemplate.getCondition('AlwaysFalseCond'); + expect(cfnTemplate.getResource('CustomBucket').cfnOptions.condition).toBe(alwaysFalseCondition); + }); + + test("throws an exception when a custom resource uses a Condition attribute that doesn't exist in the template", () => { expect(() => { - includeTestTemplate(stack, 'non-existent-resource-type.json'); - }).toThrow(/Unrecognized CloudFormation resource type: 'AWS::FakeService::DoesNotExist'/); + includeTestTemplate(stack, 'custom-resource-with-bad-condition.json'); + }).toThrow(/Resource 'CustomResource' uses Condition 'AlwaysFalseCond' that doesn't exist/); }); test('can ingest a template that contains outputs and modify them', () => { diff --git a/packages/@aws-cdk/core/lib/cfn-parse.ts b/packages/@aws-cdk/core/lib/cfn-parse.ts index 756aae374b42a..6c880b80cd420 100644 --- a/packages/@aws-cdk/core/lib/cfn-parse.ts +++ b/packages/@aws-cdk/core/lib/cfn-parse.ts @@ -1,5 +1,6 @@ import { Fn } from './cfn-fn'; import { Aws } from './cfn-pseudo'; +import { CfnResource } from './cfn-resource'; import { CfnAutoScalingReplacingUpdate, CfnAutoScalingRollingUpdate, CfnAutoScalingScheduledAction, CfnCodeDeployLambdaAliasUpdate, CfnCreationPolicy, CfnDeletionPolicy, CfnResourceAutoScalingCreationPolicy, CfnResourceSignal, CfnUpdatePolicy, @@ -169,7 +170,39 @@ export class CfnParser { this.options = options; } - public parseCreationPolicy(policy: any): CfnCreationPolicy | undefined { + public handleAttributes(resource: CfnResource, resourceAttributes: any, logicalId: string): void { + const finder = this.options.finder; + const cfnOptions = resource.cfnOptions; + + cfnOptions.creationPolicy = this.parseCreationPolicy(resourceAttributes.CreationPolicy); + cfnOptions.updatePolicy = this.parseUpdatePolicy(resourceAttributes.UpdatePolicy); + cfnOptions.deletionPolicy = this.parseDeletionPolicy(resourceAttributes.DeletionPolicy); + cfnOptions.updateReplacePolicy = this.parseDeletionPolicy(resourceAttributes.UpdateReplacePolicy); + cfnOptions.metadata = this.parseValue(resourceAttributes.Metadata); + + // handle Condition + if (resourceAttributes.Condition) { + const condition = finder.findCondition(resourceAttributes.Condition); + if (!condition) { + throw new Error(`Resource '${logicalId}' uses Condition '${resourceAttributes.Condition}' that doesn't exist`); + } + cfnOptions.condition = condition; + } + + // handle DependsOn + resourceAttributes.DependsOn = resourceAttributes.DependsOn ?? []; + const dependencies: string[] = Array.isArray(resourceAttributes.DependsOn) ? + resourceAttributes.DependsOn : [resourceAttributes.DependsOn]; + for (const dep of dependencies) { + const depResource = finder.findResource(dep); + if (!depResource) { + throw new Error(`Resource '${logicalId}' depends on '${dep}' that doesn't exist`); + } + resource.node.addDependency(depResource); + } + } + + private parseCreationPolicy(policy: any): CfnCreationPolicy | undefined { if (typeof policy !== 'object') { return undefined; } // change simple JS values to their CDK equivalents @@ -198,7 +231,7 @@ export class CfnParser { } } - public parseUpdatePolicy(policy: any): CfnUpdatePolicy | undefined { + private parseUpdatePolicy(policy: any): CfnUpdatePolicy | undefined { if (typeof policy !== 'object') { return undefined; } // change simple JS values to their CDK equivalents @@ -254,7 +287,7 @@ export class CfnParser { } } - public parseDeletionPolicy(policy: any): CfnDeletionPolicy | undefined { + private parseDeletionPolicy(policy: any): CfnDeletionPolicy | undefined { switch (policy) { case null: return undefined; case undefined: return undefined; diff --git a/packages/@aws-cdk/core/test/test.cfn-parse.ts b/packages/@aws-cdk/core/test/test.cfn-parse.ts deleted file mode 100644 index 7000af65e231d..0000000000000 --- a/packages/@aws-cdk/core/test/test.cfn-parse.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Test } from 'nodeunit'; -import { CfnParser } from '../lib/cfn-parse'; - -export = { - 'FromCloudFormation class': { - '#parseCreationPolicy': { - 'returns undefined when given a non-object as the argument'(test: Test) { - test.equal(parseCreationPolicy('blah'), undefined); - - test.done(); - }, - - 'returns undefined when given an empty object as the argument'(test: Test) { - test.equal(parseCreationPolicy({}), undefined); - - test.done(); - }, - - 'returns undefined when given empty sub-objects as the argument'(test: Test) { - test.equal(parseCreationPolicy({ - AutoScalingCreationPolicy: null, - ResourceSignal: { - Count: undefined, - }, - }), undefined); - - test.done(); - }, - }, - - '#parseUpdatePolicy': { - 'returns undefined when given a non-object as the argument'(test: Test) { - test.equal(parseUpdatePolicy('blah'), undefined); - - test.done(); - }, - - 'returns undefined when given an empty object as the argument'(test: Test) { - test.equal(parseUpdatePolicy({}), undefined); - - test.done(); - }, - - 'returns undefined when given empty sub-objects as the argument'(test: Test) { - test.equal(parseUpdatePolicy({ - AutoScalingReplacingUpdate: null, - AutoScalingRollingUpdate: { - PauseTime: undefined, - }, - }), undefined); - - test.done(); - }, - }, - }, -}; - -function parseCreationPolicy(policy: any) { - return testCfnParser.parseCreationPolicy(policy); -} - -function parseUpdatePolicy(policy: any) { - return testCfnParser.parseUpdatePolicy(policy); -} - -const testCfnParser = new CfnParser({ - finder: { - findCondition() { return undefined; }, - findRefTarget() { return undefined; }, - findResource() { return undefined; }, - }, -}); diff --git a/tools/cfn2ts/lib/codegen.ts b/tools/cfn2ts/lib/codegen.ts index a00fee69cbb8e..9d555dc0fdde0 100644 --- a/tools/cfn2ts/lib/codegen.ts +++ b/tools/cfn2ts/lib/codegen.ts @@ -252,36 +252,7 @@ export default class CodeGenerator { } // handle all non-property attributes // (retention policies, conditions, metadata, etc.) - this.code.line('const cfnOptions = ret.cfnOptions;'); - this.code.line('cfnOptions.creationPolicy = cfnParser.parseCreationPolicy(resourceAttributes.CreationPolicy);'); - this.code.line('cfnOptions.updatePolicy = cfnParser.parseUpdatePolicy(resourceAttributes.UpdatePolicy);'); - this.code.line('cfnOptions.deletionPolicy = cfnParser.parseDeletionPolicy(resourceAttributes.DeletionPolicy);'); - this.code.line('cfnOptions.updateReplacePolicy = cfnParser.parseDeletionPolicy(resourceAttributes.UpdateReplacePolicy);'); - this.code.line('cfnOptions.metadata = cfnParser.parseValue(resourceAttributes.Metadata);'); - - // handle DependsOn - this.code.line('// handle DependsOn'); - // DependsOn can be either a single string, or an array of strings - this.code.line('resourceAttributes.DependsOn = resourceAttributes.DependsOn ?? [];'); - // eslint-disable-next-line max-len - this.code.line('const dependencies: string[] = Array.isArray(resourceAttributes.DependsOn) ? resourceAttributes.DependsOn : [resourceAttributes.DependsOn];'); - this.code.openBlock('for (const dep of dependencies)'); - this.code.line('const depResource = options.finder.findResource(dep);'); - this.code.openBlock('if (!depResource)'); - this.code.line("throw new Error(`Resource '${id}' depends on '${dep}' that doesn't exist`);"); - this.code.closeBlock(); - this.code.line('ret.node.addDependency(depResource);'); - this.code.closeBlock(); - - // handle Condition - this.code.line('// handle Condition'); - this.code.openBlock('if (resourceAttributes.Condition)'); - this.code.line('const condition = options.finder.findCondition(resourceAttributes.Condition);'); - this.code.openBlock('if (!condition)'); - this.code.line("throw new Error(`Resource '${id}' uses Condition '${resourceAttributes.Condition}' that doesn't exist`);"); - this.code.closeBlock(); - this.code.line('cfnOptions.condition = condition;'); - this.code.closeBlock(); + this.code.line('cfnParser.handleAttributes(ret, resourceAttributes, id);'); this.code.line('return ret;'); this.code.closeBlock(); From 27ee332cba66ec9bd2ac369c657c4f94464f1f4c Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Thu, 23 Jul 2020 11:58:35 +0100 Subject: [PATCH 10/30] feat(cloudfront): new aws-cloudfront-origins module, support for ALB/NLB origins (#9209) This change creates a new, dedicated aws-cloudfront-origins module to hold convenience methods for constructing origins based on other constructs. The initial set of supported origin types are S3 buckets, app and network load balancers, and fallback support for any other HTTP server. Future work in this space might include using properties of the constructs to automatically configure the origins in optimal ways. For example, gaining access to the load balancers' listeners to determine which port(s) CloudFront should connect to. Notes: - It's worth someone challenging whether this is necessary. Most of the origin implementations are reasonably straightforward and could be included directly in the aws-cloudfront module. - I wanted -- for non-S3 origins -- to expose the full set of configurable options, but didn't want to force users to specify the domain name, so split the *Origin Props into Options/Props, with the latter requiring the domain name. Open to other suggestions on how to achieve this (or arguments that the `S3Origin` should have this support as well). fixes #9207 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-cloudfront-origins/.eslintrc.js | 3 + .../aws-cloudfront-origins/.gitignore | 19 ++ .../aws-cloudfront-origins/.npmignore | 26 +++ .../@aws-cdk/aws-cloudfront-origins/LICENSE | 201 ++++++++++++++++++ .../@aws-cdk/aws-cloudfront-origins/NOTICE | 2 + .../@aws-cdk/aws-cloudfront-origins/README.md | 78 +++++++ .../aws-cloudfront-origins/jest.config.js | 2 + .../aws-cloudfront-origins/lib/http-origin.ts | 21 ++ .../aws-cloudfront-origins/lib/index.ts | 3 + .../lib/load-balancer-origin.ts | 22 ++ .../aws-cloudfront-origins/lib/s3-origin.ts | 64 ++++++ .../aws-cloudfront-origins/package.json | 94 ++++++++ .../test/http-origin.test.ts | 49 +++++ .../test/integ.s3-origin.expected.json | 107 ++++++++++ .../test/integ.s3-origin.ts | 15 ++ .../test/load-balancer-origin.test.ts | 57 +++++ .../test/s3-origin.test.ts | 64 ++++++ packages/@aws-cdk/aws-cloudfront/README.md | 39 +++- .../aws-cloudfront/lib/distribution.ts | 4 +- .../@aws-cdk/aws-cloudfront/lib/origin.ts | 61 ++---- .../aws-cloudfront/test/distribution.test.ts | 35 +-- .../aws-cloudfront/test/origin.test.ts | 63 ++---- .../test/private/cache-behavior.test.ts | 11 +- packages/decdk/package.json | 1 + packages/monocdk-experiment/package.json | 1 + 25 files changed, 919 insertions(+), 123 deletions(-) create mode 100644 packages/@aws-cdk/aws-cloudfront-origins/.eslintrc.js create mode 100644 packages/@aws-cdk/aws-cloudfront-origins/.gitignore create mode 100644 packages/@aws-cdk/aws-cloudfront-origins/.npmignore create mode 100644 packages/@aws-cdk/aws-cloudfront-origins/LICENSE create mode 100644 packages/@aws-cdk/aws-cloudfront-origins/NOTICE create mode 100644 packages/@aws-cdk/aws-cloudfront-origins/README.md create mode 100644 packages/@aws-cdk/aws-cloudfront-origins/jest.config.js create mode 100644 packages/@aws-cdk/aws-cloudfront-origins/lib/http-origin.ts create mode 100644 packages/@aws-cdk/aws-cloudfront-origins/lib/index.ts create mode 100644 packages/@aws-cdk/aws-cloudfront-origins/lib/load-balancer-origin.ts create mode 100644 packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts create mode 100644 packages/@aws-cdk/aws-cloudfront-origins/package.json create mode 100644 packages/@aws-cdk/aws-cloudfront-origins/test/http-origin.test.ts create mode 100644 packages/@aws-cdk/aws-cloudfront-origins/test/integ.s3-origin.expected.json create mode 100644 packages/@aws-cdk/aws-cloudfront-origins/test/integ.s3-origin.ts create mode 100644 packages/@aws-cdk/aws-cloudfront-origins/test/load-balancer-origin.test.ts create mode 100644 packages/@aws-cdk/aws-cloudfront-origins/test/s3-origin.test.ts diff --git a/packages/@aws-cdk/aws-cloudfront-origins/.eslintrc.js b/packages/@aws-cdk/aws-cloudfront-origins/.eslintrc.js new file mode 100644 index 0000000000000..61dd8dd001f63 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-cloudfront-origins/.gitignore b/packages/@aws-cdk/aws-cloudfront-origins/.gitignore new file mode 100644 index 0000000000000..d8a8561d50885 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +nyc.config.js +.LAST_PACKAGE +*.snk +!.eslintrc.js +!jest.config.js + +junit.xml \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront-origins/.npmignore b/packages/@aws-cdk/aws-cloudfront-origins/.npmignore new file mode 100644 index 0000000000000..bca0ae2513f1e --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/.npmignore @@ -0,0 +1,26 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront-origins/LICENSE b/packages/@aws-cdk/aws-cloudfront-origins/LICENSE new file mode 100644 index 0000000000000..b71ec1688783a --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-cloudfront-origins/NOTICE b/packages/@aws-cdk/aws-cloudfront-origins/NOTICE new file mode 100644 index 0000000000000..bfccac9a7f69c --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-cloudfront-origins/README.md b/packages/@aws-cdk/aws-cloudfront-origins/README.md new file mode 100644 index 0000000000000..b1dd1de905709 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/README.md @@ -0,0 +1,78 @@ +# CloudFront Origins for the CDK CloudFront Library + + +--- + +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. They are subject to non-backward compatible changes or removal in any future version. These are not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be announced in the release notes. This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +This library contains convenience methods for defining origins for a CloudFront distribution. You can use this library to create origins from +S3 buckets, Elastic Load Balancing v2 load balancers, or any other domain name. + +## S3 Bucket + +An S3 bucket can be added as an origin. If the bucket is configured as a website endpoint, the distribution can use S3 redirects and S3 custom error +documents. + +```ts +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as origins from '@aws-cdk/aws-cloudfront-origins'; + +const myBucket = new s3.Bucket(this, 'myBucket'); +new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { origin: new origins.S3Origin(myBucket) }, +}); +``` + +The above will treat the bucket differently based on if `IBucket.isWebsite` is set or not. If the bucket is configured as a website, the bucket is +treated as an HTTP origin, and the built-in S3 redirects and error pages can be used. Otherwise, the bucket is handled as a bucket origin and +CloudFront's redirect and error handling will be used. In the latter case, the Origin wil create an origin access identity and grant it access to the +underlying bucket. This can be used in conjunction with a bucket that is not public to require that your users access your content using CloudFront +URLs and not S3 URLs directly. + +## ELBv2 Load Balancer + +An Elastic Load Balancing (ELB) v2 load balancer may be used as an origin. In order for a load balancer to serve as an origin, it must be publicly +accessible (`internetFacing` is true). Both Application and Network load balancers are supported. + +```ts +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; + +const vpc = new ec2.Vpc(...); +// Create an application load balancer in a VPC. 'internetFacing' must be 'true' +// for CloudFront to access the load balancer and use it as an origin. +const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { + vpc, + internetFacing: true +}); +new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { origin: new origins.LoadBalancerV2Origin(lb) }, +}); +``` + +The origin can also be customized to respond on different ports, have different connection properties, etc. + +```ts +const origin = new origins.LoadBalancerV2Origin(loadBalancer, { + connectionAttempts: 3, + connectionTimeout: Duration.seconds(5), + protocolPolicy: cloudfront.OriginProtocolPolicy.MATCH_VIEWER, +}); +``` + +## From an HTTP endpoint + +Origins can also be created from any other HTTP endpoint, given the domain name, and optionally, other origin properties. + +```ts +new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { origin: new origins.HttpOrigin('www.example.com') }, +}); +``` + +See the documentation of `@aws-cdk/aws-cloudfront` for more information. diff --git a/packages/@aws-cdk/aws-cloudfront-origins/jest.config.js b/packages/@aws-cdk/aws-cloudfront-origins/jest.config.js new file mode 100644 index 0000000000000..54e28beb9798b --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-cloudfront-origins/lib/http-origin.ts b/packages/@aws-cdk/aws-cloudfront-origins/lib/http-origin.ts new file mode 100644 index 0000000000000..19bfb9cb15e95 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/lib/http-origin.ts @@ -0,0 +1,21 @@ +import * as cloudfront from '@aws-cdk/aws-cloudfront'; + +/** + * Properties for an Origin backed by any HTTP server. + * + * @experimental + */ +export interface HttpOriginProps extends cloudfront.HttpOriginProps { } + +/** + * An Origin for an HTTP server. + * + * @experimental + */ +export class HttpOrigin extends cloudfront.HttpOrigin { + + constructor(domainName: string, props: HttpOriginProps = {}) { + super(domainName, { ...props }); + } + +} diff --git a/packages/@aws-cdk/aws-cloudfront-origins/lib/index.ts b/packages/@aws-cdk/aws-cloudfront-origins/lib/index.ts new file mode 100644 index 0000000000000..5f41b97f3dfde --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/lib/index.ts @@ -0,0 +1,3 @@ +export * from './http-origin'; +export * from './load-balancer-origin'; +export * from './s3-origin'; diff --git a/packages/@aws-cdk/aws-cloudfront-origins/lib/load-balancer-origin.ts b/packages/@aws-cdk/aws-cloudfront-origins/lib/load-balancer-origin.ts new file mode 100644 index 0000000000000..de3f2d17593cd --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/lib/load-balancer-origin.ts @@ -0,0 +1,22 @@ +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; + +/** + * Properties for an Origin backed by a v2 load balancer. + * + * @experimental + */ +export interface LoadBalancerV2OriginProps extends cloudfront.HttpOriginProps { } + +/** + * An Origin for a v2 load balancer. + * + * @experimental + */ +export class LoadBalancerV2Origin extends cloudfront.HttpOrigin { + + constructor(loadBalancer: elbv2.ILoadBalancerV2, props: LoadBalancerV2OriginProps = {}) { + super(loadBalancer.loadBalancerDnsName, { ...props }); + } + +} diff --git a/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts b/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts new file mode 100644 index 0000000000000..b976e482b2276 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts @@ -0,0 +1,64 @@ +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; + +/** + * Properties to use to customize an S3 Origin. + * + * @experimental + */ +export interface S3OriginProps { + /** + * An optional path that CloudFront appends to the origin domain name when CloudFront requests content from the origin. + * Must begin, but not end, with '/' (e.g., '/production/images'). + * + * @default '/' + */ + readonly originPath?: string; +} + +/** + * An Origin that is backed by an S3 bucket. + * + * If the bucket is configured for website hosting, this origin will be configured to use the bucket as an + * HTTP server origin and will use the bucket's configured website redirects and error handling. Otherwise, + * the origin is created as a bucket origin and will use CloudFront's redirect and error handling. + * + * @experimental + */ +export class S3Origin extends cloudfront.Origin { + + private readonly origin: cloudfront.Origin; + + constructor(bucket: s3.IBucket, props: S3OriginProps = {}) { + let proxyOrigin; + if (bucket.isWebsite) { + proxyOrigin = new cloudfront.HttpOrigin(bucket.bucketWebsiteDomainName, { + protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY, // S3 only supports HTTP for website buckets + ...props, + }); + } else { + proxyOrigin = new cloudfront.S3Origin({ + bucket, + ...props, + }); + } + + super(proxyOrigin.domainName); + + this.origin = proxyOrigin; + } + + public get id() { + return this.origin.id; + } + + public bind(scope: cdk.Construct, options: cloudfront.OriginBindOptions) { + this.origin.bind(scope, options); + } + + public renderOrigin() { + return this.origin.renderOrigin(); + } + +} diff --git a/packages/@aws-cdk/aws-cloudfront-origins/package.json b/packages/@aws-cdk/aws-cloudfront-origins/package.json new file mode 100644 index 0000000000000..df97c4987294f --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/package.json @@ -0,0 +1,94 @@ +{ + "name": "@aws-cdk/aws-cloudfront-origins", + "version": "0.0.0", + "description": "CDK Constructs for AWS CloudFront Origins", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awscdk.services.cloudfront.origins", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "cloudfront-origins" + } + }, + "dotnet": { + "namespace": "Amazon.CDK.AWS.CloudFront.Origins", + "packageId": "Amazon.CDK.AWS.CloudFront.Origins", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-cdk.aws-cloudfront-origins", + "module": "aws_cdk.aws_cloudfront_origins" + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-cloudfront-origins" + }, + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "build+test+package": "npm run build+test && npm run package", + "build+test": "npm run build && npm test", + "compat": "cdk-compat" + }, + "cdk-build": { + "jest": true + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "cloudfront", + "cloudfront-origins" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "aws-sdk": "^2.715.0", + "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", + "pkglint": "0.0.0" + }, + "dependencies": { + "@aws-cdk/aws-cloudfront": "0.0.0", + "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.0.2" + }, + "homepage": "https://github.com/aws/aws-cdk", + "peerDependencies": { + "@aws-cdk/core": "0.0.0", + "constructs": "^3.0.2", + "@aws-cdk/aws-cloudfront": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "experimental", + "awscdkio": { + "announce": false + } +} diff --git a/packages/@aws-cdk/aws-cloudfront-origins/test/http-origin.test.ts b/packages/@aws-cdk/aws-cloudfront-origins/test/http-origin.test.ts new file mode 100644 index 0000000000000..a5475dc4c760b --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/test/http-origin.test.ts @@ -0,0 +1,49 @@ +import '@aws-cdk/assert/jest'; +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import { App, Duration, Stack } from '@aws-cdk/core'; +import { HttpOrigin } from '../lib'; + +let app: App; +let stack: Stack; + +beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + env: { account: '1234', region: 'testregion' }, + }); +}); + +test('Renders minimal example with just a domain name', () => { + const origin = new HttpOrigin('www.example.com'); + origin.bind(stack, { originIndex: 0 }); + + expect(origin.renderOrigin()).toEqual({ + id: 'StackOrigin029E19582', + domainName: 'www.example.com', + customOriginConfig: { + originProtocolPolicy: 'https-only', + }, + }); +}); + +test('Can customize properties of the origin', () => { + const origin = new HttpOrigin('www.example.com', { + customHeaders: { AUTH: 'NONE' }, + readTimeout: Duration.seconds(10), + protocolPolicy: cloudfront.OriginProtocolPolicy.MATCH_VIEWER, + }); + origin.bind(stack, { originIndex: 0 }); + + expect(origin.renderOrigin()).toEqual({ + id: 'StackOrigin029E19582', + domainName: 'www.example.com', + originCustomHeaders: [{ + headerName: 'AUTH', + headerValue: 'NONE', + }], + customOriginConfig: { + originProtocolPolicy: 'match-viewer', + originReadTimeout: 10, + }, + }); +}); diff --git a/packages/@aws-cdk/aws-cloudfront-origins/test/integ.s3-origin.expected.json b/packages/@aws-cdk/aws-cloudfront-origins/test/integ.s3-origin.expected.json new file mode 100644 index 0000000000000..64455613ffcbc --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/test/integ.s3-origin.expected.json @@ -0,0 +1,107 @@ +{ + "Resources": { + "Bucket83908E77": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "BucketPolicyE9A3008A": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "Bucket83908E77" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Principal": { + "CanonicalUser": { + "Fn::GetAtt": [ + "DistributionS3Origin115FD918D", + "S3CanonicalUserId" + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "DistributionS3Origin115FD918D": { + "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", + "Properties": { + "CloudFrontOriginAccessIdentityConfig": { + "Comment": "Allows CloudFront to reach the bucket" + } + } + }, + "DistributionCFDistribution882A7313": { + "Type": "AWS::CloudFront::Distribution", + "Properties": { + "DistributionConfig": { + "DefaultCacheBehavior": { + "ForwardedValues": { + "QueryString": false + }, + "TargetOriginId": "cloudfronts3originDistributionOrigin1741C4E95", + "ViewerProtocolPolicy": "allow-all" + }, + "Enabled": true, + "Origins": [ + { + "DomainName": { + "Fn::GetAtt": [ + "Bucket83908E77", + "RegionalDomainName" + ] + }, + "Id": "cloudfronts3originDistributionOrigin1741C4E95", + "S3OriginConfig": { + "OriginAccessIdentity": { + "Fn::Join": [ + "", + [ + "origin-access-identity/cloudfront/", + { + "Ref": "DistributionS3Origin115FD918D" + } + ] + ] + } + } + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront-origins/test/integ.s3-origin.ts b/packages/@aws-cdk/aws-cloudfront-origins/test/integ.s3-origin.ts new file mode 100644 index 0000000000000..53289c306a256 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/test/integ.s3-origin.ts @@ -0,0 +1,15 @@ +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import * as origins from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'cloudfront-s3-origin'); + +const bucket = new s3.Bucket(stack, 'Bucket'); +new cloudfront.Distribution(stack, 'Distribution', { + defaultBehavior: { origin: new origins.S3Origin(bucket) }, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-cloudfront-origins/test/load-balancer-origin.test.ts b/packages/@aws-cdk/aws-cloudfront-origins/test/load-balancer-origin.test.ts new file mode 100644 index 0000000000000..c06c9cc7a7b84 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/test/load-balancer-origin.test.ts @@ -0,0 +1,57 @@ +import '@aws-cdk/assert/jest'; +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; +import { App, Duration, Stack } from '@aws-cdk/core'; +import { LoadBalancerV2Origin } from '../lib'; + +let app: App; +let stack: Stack; + +beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + env: { account: '1234', region: 'testregion' }, + }); +}); + +test('Renders minimal example with just a load balancer', () => { + const loadBalancer = elbv2.NetworkLoadBalancer.fromNetworkLoadBalancerAttributes(stack, 'elbv2', { + loadBalancerArn: 'arn:aws:elasticloadbalancing:us-west-2:111111111111:loadbalancer/net/mylb/5d1b75f4f1cee11e', + loadBalancerDnsName: 'mylb-5d1b75f4f1cee11e.elb.us-west-2.amazonaws.com', + }); + + const origin = new LoadBalancerV2Origin(loadBalancer); + origin.bind(stack, { originIndex: 0 }); + + expect(origin.renderOrigin()).toEqual({ + id: 'StackOrigin029E19582', + domainName: loadBalancer.loadBalancerDnsName, + customOriginConfig: { + originProtocolPolicy: 'https-only', + }, + }); +}); + +test('Can customize properties of the origin', () => { + const loadBalancer = elbv2.NetworkLoadBalancer.fromNetworkLoadBalancerAttributes(stack, 'elbv2', { + loadBalancerArn: 'arn:aws:elasticloadbalancing:us-west-2:111111111111:loadbalancer/net/mylb/5d1b75f4f1cee11e', + loadBalancerDnsName: 'mylb-5d1b75f4f1cee11e.elb.us-west-2.amazonaws.com', + }); + + const origin = new LoadBalancerV2Origin(loadBalancer, { + connectionAttempts: 3, + connectionTimeout: Duration.seconds(5), + protocolPolicy: cloudfront.OriginProtocolPolicy.MATCH_VIEWER, + }); + origin.bind(stack, { originIndex: 0 }); + + expect(origin.renderOrigin()).toEqual({ + id: 'StackOrigin029E19582', + domainName: loadBalancer.loadBalancerDnsName, + connectionAttempts: 3, + connectionTimeout: 5, + customOriginConfig: { + originProtocolPolicy: 'match-viewer', + }, + }); +}); diff --git a/packages/@aws-cdk/aws-cloudfront-origins/test/s3-origin.test.ts b/packages/@aws-cdk/aws-cloudfront-origins/test/s3-origin.test.ts new file mode 100644 index 0000000000000..c851f53b122a0 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront-origins/test/s3-origin.test.ts @@ -0,0 +1,64 @@ +import '@aws-cdk/assert/jest'; +import * as s3 from '@aws-cdk/aws-s3'; +import { App, Stack } from '@aws-cdk/core'; +import { S3Origin } from '../lib'; + +let app: App; +let stack: Stack; + +beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + env: { account: '1234', region: 'testregion' }, + }); +}); + +test('With non-website bucket, renders all required properties, including S3Origin config', () => { + const bucket = new s3.Bucket(stack, 'Bucket'); + + const origin = new S3Origin(bucket); + origin.bind(stack, { originIndex: 0 }); + + expect(origin.renderOrigin()).toEqual({ + id: 'StackOrigin029E19582', + domainName: bucket.bucketRegionalDomainName, + s3OriginConfig: { + originAccessIdentity: 'origin-access-identity/cloudfront/${Token[TOKEN.69]}', + }, + }); +}); + +test('With website bucket, renders all required properties, including custom origin config', () => { + const bucket = new s3.Bucket(stack, 'Bucket', { + websiteIndexDocument: 'index.html', + }); + + const origin = new S3Origin(bucket); + origin.bind(stack, { originIndex: 0 }); + + expect(origin.renderOrigin()).toEqual({ + id: 'StackOrigin029E19582', + domainName: bucket.bucketWebsiteDomainName, + customOriginConfig: { + originProtocolPolicy: 'http-only', + }, + }); +}); + +test('Respects props passed down to underlying origin', () => { + const bucket = new s3.Bucket(stack, 'Bucket', { + websiteIndexDocument: 'index.html', + }); + + const origin = new S3Origin(bucket, { originPath: '/website' }); + origin.bind(stack, { originIndex: 0 }); + + expect(origin.renderOrigin()).toEqual({ + id: 'StackOrigin029E19582', + domainName: bucket.bucketWebsiteDomainName, + originPath: '/website', + customOriginConfig: { + originProtocolPolicy: 'http-only', + }, + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/README.md b/packages/@aws-cdk/aws-cloudfront/README.md index 935b97ebef946..69bf6f7cf9250 100644 --- a/packages/@aws-cdk/aws-cloudfront/README.md +++ b/packages/@aws-cdk/aws-cloudfront/README.md @@ -45,11 +45,12 @@ documents. ```ts import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as origins from '@aws-cdk/aws-cloudfront-origins'; // Creates a distribution for a S3 bucket. const myBucket = new s3.Bucket(this, 'myBucket'); new cloudfront.Distribution(this, 'myDist', { - defaultBehavior: { origin: cloudfront.Origin.fromBucket(myBucket) }, + defaultBehavior: { origin: new origins.S3Origin(myBucket) }, }); ``` @@ -59,14 +60,34 @@ CloudFront's redirect and error handling will be used. In the latter case, the O underlying bucket. This can be used in conjunction with a bucket that is not public to require that your users access your content using CloudFront URLs and not S3 URLs directly. -#### From an HTTP endpoint +#### ELBv2 Load Balancer -Origins can also be created from other resources (e.g., load balancers, API gateways), or from any accessible HTTP server, given the domain name. +An Elastic Load Balancing (ELB) v2 load balancer may be used as an origin. In order for a load balancer to serve as an origin, it must be publicly +accessible (`internetFacing` is true). Both Application and Network load balancers are supported. + +```ts +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; + +const vpc = new ec2.Vpc(...); +// Create an application load balancer in a VPC. 'internetFacing' must be 'true' +// for CloudFront to access the load balancer and use it as an origin. +const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { + vpc, + internetFacing: true +}); +new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { origin: new origins.LoadBalancerV2Origin(lb) }, +}); +``` + +## From an HTTP endpoint + +Origins can also be created from any other HTTP endpoint, given the domain name, and optionally, other origin properties. ```ts -// Creates a distribution for an HTTP server. new cloudfront.Distribution(this, 'myDist', { - defaultBehavior: { origin: cloudfront.Origin.fromHttpServer({ domainName: 'www.example.com' }) }, + defaultBehavior: { origin: new origins.HttpOrigin('www.example.com') }, }); ``` @@ -85,7 +106,7 @@ const myCertificate = new acm.DnsValidatedCertificate(this, 'mySiteCert', { hostedZone, }); new cloudfront.Distribution(this, 'myDist', { - defaultBehavior: { origin: cloudfront.Origin.fromBucket(myBucket) }, + defaultBehavior: { origin: new origins.S3Origin(myBucket) }, certificate: myCertificate, }); ``` @@ -102,7 +123,7 @@ methods and viewer protocol policy of the cache. ```ts const myWebDistribution = new cloudfront.Distribution(this, 'myDist', { defaultBehavior: { - origin: cloudfront.Origin.fromBucket(myBucket), + origin: new origins.S3Origin(myBucket), allowedMethods: AllowedMethods.ALLOW_ALL, viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, } @@ -114,7 +135,7 @@ and enable customization for a specific set of resources based on a URL path pat override the default time-to-live (TTL) for all of the images. ```ts -myWebDistribution.addBehavior('/images/*.jpg', cloudfront.Origin.fromBucket(myOtherBucket), { +myWebDistribution.addBehavior('/images/*.jpg', new origins.S3Origin(myBucket), { viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, defaultTtl: cdk.Duration.days(7), }); @@ -123,7 +144,7 @@ myWebDistribution.addBehavior('/images/*.jpg', cloudfront.Origin.fromBucket(myOt These behaviors can also be specified at distribution creation time. ```ts -const bucketOrigin = cloudfront.Origin.fromBucket(myBucket); +const bucketOrigin = new origins.S3Origin(myBucket); new cloudfront.Distribution(this, 'myDist', { defaultBehavior: { origin: bucketOrigin, diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index cb8b60a08a386..2ec2501e28bd9 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -184,13 +184,13 @@ export class Distribution extends Resource implements IDistribution { private addOrigin(origin: Origin) { if (!this.origins.has(origin)) { this.origins.add(origin); - origin._bind(this, { originIndex: this.origins.size }); + origin.bind(this, { originIndex: this.origins.size }); } } private renderOrigins(): CfnDistribution.OriginProperty[] { const renderedOrigins: CfnDistribution.OriginProperty[] = []; - this.origins.forEach(origin => renderedOrigins.push(origin._renderOrigin())); + this.origins.forEach(origin => renderedOrigins.push(origin.renderOrigin())); return renderedOrigins; } diff --git a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts index 1842b5999ed3a..ee15b4106f0e1 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts @@ -5,17 +5,11 @@ import { OriginProtocolPolicy } from './distribution'; import { OriginAccessIdentity } from './origin_access_identity'; /** - * Properties to be used to create an Origin. Prefer to use one of the Origin.from* factory methods rather than - * instantiating an Origin directly from these properties. + * Properties to define an Origin. * * @experimental */ export interface OriginProps { - /** - * The domain name of the Amazon S3 bucket or HTTP server origin. - */ - readonly domainName: string; - /** * An optional path that CloudFront appends to the origin domain name when CloudFront requests content from the origin. * Must begin, but not end, with '/' (e.g., '/production/images'). @@ -49,8 +43,10 @@ export interface OriginProps { /** * Options passed to Origin.bind(). + * + * @experimental */ -interface OriginBindOptions { +export interface OriginBindOptions { /** * The positional index of this origin within the distribution. Used for ensuring unique IDs. */ @@ -65,32 +61,6 @@ interface OriginBindOptions { */ export abstract class Origin { - /** - * Creates a pre-configured origin for a S3 bucket. - * If this bucket has been configured for static website hosting, then `fromWebsiteBucket` should be used instead. - * - * An Origin Access Identity will be created and granted read access to the bucket. - * - * @param bucket the bucket to act as an origin. - */ - public static fromBucket(bucket: IBucket): Origin { - if (bucket.isWebsite) { - return new HttpOrigin({ - domainName: bucket.bucketWebsiteDomainName, - protocolPolicy: OriginProtocolPolicy.HTTP_ONLY, // S3 only supports HTTP for website buckets - }); - } else { - return new S3Origin({ domainName: bucket.bucketRegionalDomainName, bucket }); - } - } - - /** - * Creates an origin from an HTTP server. - */ - public static fromHttpServer(props: HttpOriginProps): Origin { - return new HttpOrigin(props); - } - /** * The domain name of the origin. */ @@ -103,11 +73,11 @@ export abstract class Origin { private originId?: string; - protected constructor(props: OriginProps) { + protected constructor(domainName: string, props: OriginProps = {}) { validateIntInRangeOrUndefined('connectionTimeout', 1, 10, props.connectionTimeout?.toSeconds()); validateIntInRangeOrUndefined('connectionAttempts', 1, 3, props.connectionAttempts, false); - this.domainName = props.domainName; + this.domainName = domainName; this.originPath = this.validateOriginPath(props.originPath); this.connectionTimeout = props.connectionTimeout; this.connectionAttempts = props.connectionAttempts; @@ -126,19 +96,15 @@ export abstract class Origin { /** * Binds the origin to the associated Distribution. Can be used to grant permissions, create dependent resources, etc. - * - * @internal */ - public _bind(scope: Construct, options: OriginBindOptions): void { + public bind(scope: Construct, options: OriginBindOptions): void { this.originId = new Construct(scope, `Origin${options.originIndex}`).node.uniqueId; } /** * Creates and returns the CloudFormation representation of this origin. - * - * @internal */ - public _renderOrigin(): CfnDistribution.OriginProperty { + public renderOrigin(): CfnDistribution.OriginProperty { const s3OriginConfig = this.renderS3OriginConfig(); const customOriginConfig = this.renderCustomOriginConfig(); @@ -214,13 +180,12 @@ export class S3Origin extends Origin { private originAccessIdentity!: OriginAccessIdentity; constructor(props: S3OriginProps) { - super(props); + super(props.bucket.bucketRegionalDomainName, props); this.bucket = props.bucket; } - /** @internal */ - public _bind(scope: Construct, options: OriginBindOptions) { - super._bind(scope, options); + public bind(scope: Construct, options: OriginBindOptions) { + super.bind(scope, options); if (!this.originAccessIdentity) { this.originAccessIdentity = new OriginAccessIdentity(scope, `S3Origin${options.originIndex}`); this.bucket.grantRead(this.originAccessIdentity); @@ -283,8 +248,8 @@ export interface HttpOriginProps extends OriginProps { */ export class HttpOrigin extends Origin { - constructor(private readonly props: HttpOriginProps) { - super(props); + constructor(domainName: string, private readonly props: HttpOriginProps = {}) { + super(domainName, props); validateIntInRangeOrUndefined('readTimeout', 1, 60, props.readTimeout?.toSeconds()); validateIntInRangeOrUndefined('keepaliveTimeout', 1, 60, props.keepaliveTimeout?.toSeconds()); diff --git a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts index 218f5a4c6ac10..edaca21e1b2e3 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts @@ -2,7 +2,7 @@ import '@aws-cdk/assert/jest'; import * as acm from '@aws-cdk/aws-certificatemanager'; import * as s3 from '@aws-cdk/aws-s3'; import { App, Duration, Stack } from '@aws-cdk/core'; -import { Distribution, Origin, PriceClass } from '../lib'; +import { Distribution, Origin, PriceClass, S3Origin } from '../lib'; let app: App; let stack: Stack; @@ -15,7 +15,7 @@ beforeEach(() => { }); test('minimal example renders correctly', () => { - const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); + const origin = defaultS3Origin(); new Distribution(stack, 'MyDist', { defaultBehavior: { origin } }); expect(stack).toHaveResource('AWS::CloudFront::Distribution', { @@ -42,7 +42,7 @@ test('minimal example renders correctly', () => { describe('multiple behaviors', () => { test('a second behavior can\'t be specified with the catch-all path pattern', () => { - const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); + const origin = defaultS3Origin(); expect(() => { new Distribution(stack, 'MyDist', { @@ -55,7 +55,7 @@ describe('multiple behaviors', () => { }); test('a second behavior can be added to the original origin', () => { - const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); + const origin = defaultS3Origin(); new Distribution(stack, 'MyDist', { defaultBehavior: { origin }, additionalBehaviors: { @@ -91,8 +91,9 @@ describe('multiple behaviors', () => { }); test('a second behavior can be added to a secondary origin', () => { - const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); - const origin2 = Origin.fromBucket(new s3.Bucket(stack, 'Bucket2')); + const origin = defaultS3Origin(); + const bucket2 = new s3.Bucket(stack, 'Bucket2'); + const origin2 = new S3Origin({ bucket: bucket2 }); new Distribution(stack, 'MyDist', { defaultBehavior: { origin }, additionalBehaviors: { @@ -137,8 +138,9 @@ describe('multiple behaviors', () => { }); test('behavior creation order is preserved', () => { - const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); - const origin2 = Origin.fromBucket(new s3.Bucket(stack, 'Bucket2')); + const origin = defaultS3Origin(); + const bucket2 = new s3.Bucket(stack, 'Bucket2'); + const origin2 = new S3Origin({ bucket: bucket2 }); const dist = new Distribution(stack, 'MyDist', { defaultBehavior: { origin }, additionalBehaviors: { @@ -194,7 +196,7 @@ describe('multiple behaviors', () => { describe('certificates', () => { test('should fail if using an imported certificate from outside of us-east-1', () => { - const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); + const origin = defaultS3Origin(); const certificate = acm.Certificate.fromCertificateArn(stack, 'Cert', 'arn:aws:acm:eu-west-1:123456789012:certificate/12345678-1234-1234-1234-123456789012'); expect(() => { @@ -209,7 +211,7 @@ describe('certificates', () => { const certificate = acm.Certificate.fromCertificateArn(stack, 'Cert', 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012'); new Distribution(stack, 'Dist', { - defaultBehavior: { origin: Origin.fromBucket(new s3.Bucket(stack, 'Bucket')) }, + defaultBehavior: { origin: defaultS3Origin() }, certificate, }); @@ -228,7 +230,7 @@ describe('certificates', () => { describe('custom error responses', () => { test('should fail if responsePagePath is defined but responseCode is not', () => { - const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); + const origin = defaultS3Origin(); expect(() => { new Distribution(stack, 'Dist', { @@ -242,7 +244,7 @@ describe('custom error responses', () => { }); test('should fail if only the error code is provided', () => { - const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); + const origin = defaultS3Origin(); expect(() => { new Distribution(stack, 'Dist', { @@ -253,7 +255,7 @@ describe('custom error responses', () => { }); test('should render the array of error configs if provided', () => { - const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); + const origin = defaultS3Origin(); new Distribution(stack, 'Dist', { defaultBehavior: { origin }, errorResponses: [{ @@ -287,7 +289,7 @@ describe('custom error responses', () => { }); test('price class is included if provided', () => { - const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); + const origin = defaultS3Origin(); new Distribution(stack, 'Dist', { defaultBehavior: { origin }, priceClass: PriceClass.PRICE_CLASS_200, @@ -298,5 +300,8 @@ test('price class is included if provided', () => { PriceClass: 'PriceClass_200', }, }); +}); -}); \ No newline at end of file +function defaultS3Origin(): Origin { + return new S3Origin({ bucket: new s3.Bucket(stack, 'Bucket') }); +} diff --git a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts index a3e57515293fc..716af7e3225f5 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts @@ -1,7 +1,7 @@ import '@aws-cdk/assert/jest'; import * as s3 from '@aws-cdk/aws-s3'; import { App, Stack, Duration } from '@aws-cdk/core'; -import { CfnDistribution, Distribution, Origin, OriginProps, HttpOrigin, OriginProtocolPolicy } from '../lib'; +import { CfnDistribution, Distribution, Origin, OriginProps, HttpOrigin, OriginProtocolPolicy, S3Origin } from '../lib'; let app: App; let stack: Stack; @@ -13,14 +13,14 @@ beforeEach(() => { }); }); -describe('fromBucket', () => { +describe('S3Origin', () => { test('as bucket, renders all required properties, including S3Origin config', () => { const bucket = new s3.Bucket(stack, 'Bucket'); - const origin = Origin.fromBucket(bucket); - origin._bind(stack, { originIndex: 0 }); + const origin = new S3Origin({ bucket }); + origin.bind(stack, { originIndex: 0 }); - expect(origin._renderOrigin()).toEqual({ + expect(origin.renderOrigin()).toEqual({ id: 'StackOrigin029E19582', domainName: bucket.bucketRegionalDomainName, s3OriginConfig: { @@ -32,7 +32,7 @@ describe('fromBucket', () => { test('as bucket, creates an OriginAccessIdentity and grants read permissions on the bucket', () => { const bucket = new s3.Bucket(stack, 'Bucket'); - const origin = Origin.fromBucket(bucket); + const origin = new S3Origin({ bucket }); new Distribution(stack, 'Dist', { defaultBehavior: { origin } }); expect(stack).toHaveResourceLike('AWS::CloudFront::CloudFrontOriginAccessIdentity', { @@ -50,31 +50,14 @@ describe('fromBucket', () => { }, }); }); - - test('as website bucket, renders all required properties, including custom origin config', () => { - const bucket = new s3.Bucket(stack, 'Bucket', { - websiteIndexDocument: 'index.html', - }); - - const origin = Origin.fromBucket(bucket); - origin._bind(stack, { originIndex: 0 }); - - expect(origin._renderOrigin()).toEqual({ - id: 'StackOrigin029E19582', - domainName: bucket.bucketWebsiteDomainName, - customOriginConfig: { - originProtocolPolicy: 'http-only', - }, - }); - }); }); describe('HttpOrigin', () => { test('renders a minimal example with required props', () => { - const origin = new HttpOrigin({ domainName: 'www.example.com' }); - origin._bind(stack, { originIndex: 0 }); + const origin = new HttpOrigin('www.example.com'); + origin.bind(stack, { originIndex: 0 }); - expect(origin._renderOrigin()).toEqual({ + expect(origin.renderOrigin()).toEqual({ id: 'StackOrigin029E19582', domainName: 'www.example.com', customOriginConfig: { @@ -84,8 +67,7 @@ describe('HttpOrigin', () => { }); test('renders an example with all available props', () => { - const origin = new HttpOrigin({ - domainName: 'www.example.com', + const origin = new HttpOrigin('www.example.com', { originPath: '/app', connectionTimeout: Duration.seconds(5), connectionAttempts: 2, @@ -96,9 +78,9 @@ describe('HttpOrigin', () => { readTimeout: Duration.seconds(45), keepaliveTimeout: Duration.seconds(3), }); - origin._bind(stack, { originIndex: 0 }); + origin.bind(stack, { originIndex: 0 }); - expect(origin._renderOrigin()).toEqual({ + expect(origin.renderOrigin()).toEqual({ id: 'StackOrigin029E19582', domainName: 'www.example.com', originPath: '/app', @@ -126,8 +108,7 @@ describe('HttpOrigin', () => { Duration.minutes(5), ])('validates readTimeout is an integer between 1 and 60 seconds', (readTimeout) => { expect(() => { - new HttpOrigin({ - domainName: 'www.example.com', + new HttpOrigin('www.example.com', { readTimeout, }); }).toThrow(`readTimeout: Must be an int between 1 and 60 seconds (inclusive); received ${readTimeout.toSeconds()}.`); @@ -141,8 +122,7 @@ describe('HttpOrigin', () => { Duration.minutes(5), ])('validates keepaliveTimeout is an integer between 1 and 60 seconds', (keepaliveTimeout) => { expect(() => { - new HttpOrigin({ - domainName: 'www.example.com', + new HttpOrigin('www.example.com', { keepaliveTimeout, }); }).toThrow(`keepaliveTimeout: Must be an int between 1 and 60 seconds (inclusive); received ${keepaliveTimeout.toSeconds()}.`); @@ -158,8 +138,7 @@ describe('Origin', () => { Duration.minutes(5), ])('validates connectionTimeout is an int between 1 and 10 seconds', (connectionTimeout) => { expect(() => { - new TestOrigin({ - domainName: 'www.example.com', + new TestOrigin('www.example.com', { connectionTimeout, }); }).toThrow(`connectionTimeout: Must be an int between 1 and 10 seconds (inclusive); received ${connectionTimeout.toSeconds()}.`); @@ -168,8 +147,7 @@ describe('Origin', () => { test.each([-0.5, 0.5, 1.5, 4]) ('validates connectionAttempts is an int between 1 and 3', (connectionAttempts) => { expect(() => { - new TestOrigin({ - domainName: 'www.example.com', + new TestOrigin('www.example.com', { connectionAttempts, }); }).toThrow(`connectionAttempts: Must be an int between 1 and 3 (inclusive); received ${connectionAttempts}.`); @@ -177,19 +155,18 @@ describe('Origin', () => { test.each(['api', '/api', '/api/', 'api/']) ('enforces that originPath starts but does not end, with a /', (originPath) => { - const origin = new TestOrigin({ - domainName: 'www.example.com', + const origin = new TestOrigin('www.example.com', { originPath, }); - origin._bind(stack, { originIndex: 0 }); + origin.bind(stack, { originIndex: 0 }); - expect(origin._renderOrigin().originPath).toEqual('/api'); + expect(origin.renderOrigin().originPath).toEqual('/api'); }); }); /** Used for testing common Origin functionality */ class TestOrigin extends Origin { - constructor(props: OriginProps) { super(props); } + constructor(domainName: string, props: OriginProps = {}) { super(domainName, props); } protected renderS3OriginConfig(): CfnDistribution.S3OriginConfigProperty | undefined { return { originAccessIdentity: 'origin-access-identity/cloudfront/MyOAIName' }; } diff --git a/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts b/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts index b70d3ef02c590..81dd84d716dd9 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts @@ -1,7 +1,6 @@ import '@aws-cdk/assert/jest'; -import * as s3 from '@aws-cdk/aws-s3'; import { App, Stack } from '@aws-cdk/core'; -import { AllowedMethods, Origin } from '../../lib'; +import { AllowedMethods, HttpOrigin } from '../../lib'; import { CacheBehavior } from '../../lib/private/cache-behavior'; let app: App; @@ -15,12 +14,12 @@ beforeEach(() => { }); test('renders the minimum template with an origin and path specified', () => { - const origin = Origin.fromBucket(new s3.Bucket(stack, 'MyBucket')); + const origin = new HttpOrigin('www.example.com'); const behavior = new CacheBehavior({ origin, pathPattern: '*', }); - origin._bind(stack, { originIndex: 0 }); + origin.bind(stack, { originIndex: 0 }); expect(behavior._renderBehavior()).toEqual({ targetOriginId: behavior.origin.id, @@ -31,7 +30,7 @@ test('renders the minimum template with an origin and path specified', () => { }); test('renders with all properties specified', () => { - const origin = Origin.fromBucket(new s3.Bucket(stack, 'MyBucket')); + const origin = new HttpOrigin('www.example.com'); const behavior = new CacheBehavior({ origin, pathPattern: '*', @@ -39,7 +38,7 @@ test('renders with all properties specified', () => { forwardQueryString: true, forwardQueryStringCacheKeys: ['user_id', 'auth'], }); - origin._bind(stack, { originIndex: 0 }); + origin.bind(stack, { originIndex: 0 }); expect(behavior._renderBehavior()).toEqual({ targetOriginId: behavior.origin.id, diff --git a/packages/decdk/package.json b/packages/decdk/package.json index 4542512a7c10d..62f438973b2c9 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -57,6 +57,7 @@ "@aws-cdk/aws-cloud9": "0.0.0", "@aws-cdk/aws-cloudformation": "0.0.0", "@aws-cdk/aws-cloudfront": "0.0.0", + "@aws-cdk/aws-cloudfront-origins": "0.0.0", "@aws-cdk/aws-cloudtrail": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-cloudwatch-actions": "0.0.0", diff --git a/packages/monocdk-experiment/package.json b/packages/monocdk-experiment/package.json index 70ba917eb6c21..3b385eaa08184 100644 --- a/packages/monocdk-experiment/package.json +++ b/packages/monocdk-experiment/package.json @@ -125,6 +125,7 @@ "@aws-cdk/aws-cloud9": "0.0.0", "@aws-cdk/aws-cloudformation": "0.0.0", "@aws-cdk/aws-cloudfront": "0.0.0", + "@aws-cdk/aws-cloudfront-origins": "0.0.0", "@aws-cdk/aws-cloudtrail": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-cloudwatch-actions": "0.0.0", From a30ab57c02efc0d4f7527e0b91b8bf46fa5a5e57 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Thu, 23 Jul 2020 10:03:48 -0700 Subject: [PATCH 11/30] chore(cloudfront): fix error message that was not substituting in the region name --- packages/@aws-cdk/aws-cloudfront/lib/distribution.ts | 2 +- packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index 2ec2501e28bd9..81d84515a4736 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -137,7 +137,7 @@ export class Distribution extends Resource implements IDistribution { if (props.certificate) { const certificateRegion = Stack.of(this).parseArn(props.certificate.certificateArn).region; if (!Token.isUnresolved(certificateRegion) && certificateRegion !== 'us-east-1') { - throw new Error('Distribution certificates must be in the us-east-1 region and the certificate you provided is in $Region.'); + throw new Error(`Distribution certificates must be in the us-east-1 region and the certificate you provided is in ${certificateRegion}.`); } } diff --git a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts index edaca21e1b2e3..4a8ea27d3fbec 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts @@ -204,7 +204,7 @@ describe('certificates', () => { defaultBehavior: { origin }, certificate, }); - }).toThrow(/Distribution certificates must be in the us-east-1 region/); + }).toThrow(/Distribution certificates must be in the us-east-1 region and the certificate you provided is in eu-west-1./); }); test('adding a certificate renders the correct ViewerCertificate property', () => { From 45c352ff444c936ae7cf41f4422566338aa108ab Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Thu, 23 Jul 2020 10:04:30 -0700 Subject: [PATCH 12/30] Revert "chore(cloudfront): fix error message that was not substituting in the region name" This reverts commit a30ab57c02efc0d4f7527e0b91b8bf46fa5a5e57. --- packages/@aws-cdk/aws-cloudfront/lib/distribution.ts | 2 +- packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index 81d84515a4736..2ec2501e28bd9 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -137,7 +137,7 @@ export class Distribution extends Resource implements IDistribution { if (props.certificate) { const certificateRegion = Stack.of(this).parseArn(props.certificate.certificateArn).region; if (!Token.isUnresolved(certificateRegion) && certificateRegion !== 'us-east-1') { - throw new Error(`Distribution certificates must be in the us-east-1 region and the certificate you provided is in ${certificateRegion}.`); + throw new Error('Distribution certificates must be in the us-east-1 region and the certificate you provided is in $Region.'); } } diff --git a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts index 4a8ea27d3fbec..edaca21e1b2e3 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts @@ -204,7 +204,7 @@ describe('certificates', () => { defaultBehavior: { origin }, certificate, }); - }).toThrow(/Distribution certificates must be in the us-east-1 region and the certificate you provided is in eu-west-1./); + }).toThrow(/Distribution certificates must be in the us-east-1 region/); }); test('adding a certificate renders the correct ViewerCertificate property', () => { From 679e16aee95d44b157f64b606e7f23bc7136dfc3 Mon Sep 17 00:00:00 2001 From: Somaya Date: Thu, 23 Jul 2020 10:31:56 -0700 Subject: [PATCH 13/30] chore: update synthetics area owner in auto assign action (#9229) Updated the Synthetics area owner in the auto label/assign github action workflow file. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .github/workflows/issue-label-assign.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue-label-assign.yml b/.github/workflows/issue-label-assign.yml index 54dc60522b6d9..864b2e872a45e 100644 --- a/.github/workflows/issue-label-assign.yml +++ b/.github/workflows/issue-label-assign.yml @@ -149,7 +149,7 @@ jobs: {"keywords":["[@aws-cdk/aws-ssm]","[aws-ssm]","[ssm]"],"labels":["@aws-cdk/aws-ssm"],"assignees":["MrArnoldPalmer"]}, {"keywords":["[@aws-cdk/aws-stepfunctions]","[aws-stepfunctions]","[stepfunctions]","[step functions]","[step-functions]"],"labels":["@aws-cdk/aws-stepfunctions"],"assignees":["shivlaks"]}, {"keywords":["[@aws-cdk/aws-stepfunctions-tasks]","[aws-stepfunctions-tasks]","[stepfunctions-tasks]","[stepfunctions tasks]"],"labels":["@aws-cdk/aws-stepfunctions-tasks"],"assignees":["shivlaks"]}, - {"keywords":["[@aws-cdk/aws-synthetics]","[aws-synthetics]","[synthetics]"],"labels":["@aws-cdk/aws-synthetics"],"assignees":["ericzbeard"]}, + {"keywords":["[@aws-cdk/aws-synthetics]","[aws-synthetics]","[synthetics]"],"labels":["@aws-cdk/aws-synthetics"],"assignees":["NetaNir"]}, {"keywords":["[@aws-cdk/aws-transfer]","[aws-transfer]","[transfer]"],"labels":["@aws-cdk/aws-transfer"],"assignees":["iliapolo"]}, {"keywords":["[@aws-cdk/aws-waf]","[aws-waf]","[waf]"],"labels":["@aws-cdk/aws-waf"],"assignees":["ericzbeard"]}, {"keywords":["[@aws-cdk/aws-wafregional]","[aws-wafregional]","[wafregional]","[waf regional]","[waf-regional]"],"labels":["@aws-cdk/aws-wafregional"],"assignees":["ericzbeard"]}, From 34a20757dec648f602e15ca0eba7ac9188eab8a1 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Thu, 23 Jul 2020 10:52:24 -0700 Subject: [PATCH 14/30] chore(cloudfront): fix error message that was not substituting in the region name (#9231) we were not performing string interpolation so the `$Region` was being plugged into error messages as is. Updated the error message string and corresponding test ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-cloudfront/lib/distribution.ts | 2 +- packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index 2ec2501e28bd9..81d84515a4736 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -137,7 +137,7 @@ export class Distribution extends Resource implements IDistribution { if (props.certificate) { const certificateRegion = Stack.of(this).parseArn(props.certificate.certificateArn).region; if (!Token.isUnresolved(certificateRegion) && certificateRegion !== 'us-east-1') { - throw new Error('Distribution certificates must be in the us-east-1 region and the certificate you provided is in $Region.'); + throw new Error(`Distribution certificates must be in the us-east-1 region and the certificate you provided is in ${certificateRegion}.`); } } diff --git a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts index edaca21e1b2e3..4a8ea27d3fbec 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts @@ -204,7 +204,7 @@ describe('certificates', () => { defaultBehavior: { origin }, certificate, }); - }).toThrow(/Distribution certificates must be in the us-east-1 region/); + }).toThrow(/Distribution certificates must be in the us-east-1 region and the certificate you provided is in eu-west-1./); }); test('adding a certificate renders the correct ViewerCertificate property', () => { From 950e51f1edab335a3fd323b6d51f7444738bb9dc Mon Sep 17 00:00:00 2001 From: Josh Pavel <65420015+paveljos@users.noreply.github.com> Date: Thu, 23 Jul 2020 14:27:33 -0400 Subject: [PATCH 15/30] feat(aws-codepipeline): experimental support for ServiceCatalog deploy action (#9214) feat(aws-codepipeline-actions): support for service catalog ---- Initial support for Service Catalog as a component of codepipeline-actions. It will allow a user to integrate cloudformation templates against an existing service catalog product as a part of a codepipeline. *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-codepipeline-actions/README.md | 23 ++++ .../aws-codepipeline-actions/lib/index.ts | 1 + .../lib/servicecatalog/deploy-action.ts | 104 ++++++++++++++ .../aws-codepipeline-actions/package.json | 2 + .../test.servicecatalog-action.ts | 130 ++++++++++++++++++ 5 files changed, 260 insertions(+) create mode 100644 packages/@aws-cdk/aws-codepipeline-actions/lib/servicecatalog/deploy-action.ts create mode 100644 packages/@aws-cdk/aws-codepipeline-actions/test/servicecatalog/test.servicecatalog-action.ts diff --git a/packages/@aws-cdk/aws-codepipeline-actions/README.md b/packages/@aws-cdk/aws-codepipeline-actions/README.md index 825f15b8c2662..7edd0244bcbcb 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/README.md +++ b/packages/@aws-cdk/aws-codepipeline-actions/README.md @@ -694,6 +694,29 @@ new codepipeline_actions.AlexaSkillDeployAction({ }); ``` +### AWS Service Catalog + +You can deploy a CloudFormation template to an existing Service Catalog product with the following action: + +```ts +new codepipeline.Pipeline(this, 'Pipeline', { + stages: [ + { + stageName: 'ServiceCatalogDeploy', + actions: [ + new codepipeline_actions.ServiceCatalogDeployAction({ + actionName: 'ServiceCatalogDeploy', + templatePath: cdkBuildOutput.atPath("Sample.template.json"), + productVersionName: "Version - " + Date.now.toString, + productType: "CLOUD_FORMATION_TEMPLATE", + productVersionDescription: "This is a version from the pipeline with a new description.", + productId: "prod-XXXXXXXX", + }), + }, + ], +}); +``` + ## Approve & invoke ### Manual approval Action diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/index.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/index.ts index 74d27362c5a3b..3cc0534bf1e11 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/index.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/index.ts @@ -15,4 +15,5 @@ export * from './manual-approval-action'; export * from './s3/deploy-action'; export * from './s3/source-action'; export * from './stepfunctions/invoke-action'; +export * from './servicecatalog/deploy-action'; export * from './action'; // for some reason, JSII fails building the module without exporting this class diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/servicecatalog/deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/servicecatalog/deploy-action.ts new file mode 100644 index 0000000000000..a73299761bbae --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/servicecatalog/deploy-action.ts @@ -0,0 +1,104 @@ +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as iam from '@aws-cdk/aws-iam'; +import { Construct } from '@aws-cdk/core'; +import { Action } from '../action'; + +/** TODO: + * 1.) Support cross account deployments + * 2.) Fix least privilege + * 3.) Handle CREATION of a new product + * 4.) Handle MAINTENANCE of a provisioned product + * 5.) Test/support product types beyond CLOUD_FORMATION_TEMPLATE + * 6.) Valid test cases! + */ + +/** + * Construction properties of the {@link ServiceCatalogDeployAction ServiceCatalog deploy CodePipeline Action}. + * + * **Note**: this API is still experimental, and may have breaking changes in the future! + * + * @experimental + */ +export interface ServiceCatalogDeployActionProps extends codepipeline.CommonAwsActionProps { + /** + * The path to the cloudformation artifact. + */ + readonly templatePath: codepipeline.ArtifactPath; + + /** + * The name of the version of the Service Catalog product to be deployed. + */ + readonly productVersionName: string; + + /** + * The optional description of this version of the Service Catalog product. + * @default '' + */ + readonly productVersionDescription?: string; + + /** + * The identifier of the product in the Service Catalog. This product must already exist. + */ + readonly productId: string; +} + +/** + * CodePipeline action to connect to an existing ServiceCatalog product. + * + * **Note**: this class is still experimental, and may have breaking changes in the future! + * + * @experimental + */ +export class ServiceCatalogDeployAction extends Action { + private readonly templatePath: string; + private readonly productVersionName: string; + private readonly productVersionDescription?: string; + private readonly productId: string; + private readonly productType: string; + + constructor(props: ServiceCatalogDeployActionProps) { + super({ + ...props, + provider: 'ServiceCatalog', + category: codepipeline.ActionCategory.DEPLOY, + artifactBounds: { + minInputs: 1, + maxInputs: 1, + minOutputs: 0, + maxOutputs: 0, + }, + inputs: [props.templatePath.artifact], + }); + this.templatePath = props.templatePath.location; + this.productVersionName = props.productVersionName; + this.productVersionDescription = props.productVersionDescription; + this.productId = props.productId; + this.productType = 'CLOUD_FORMATION_TEMPLATE'; + } + + protected bound(_scope: Construct, _stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { + + options.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AWSServiceCatalogAdminFullAccess')); + + // Attempt at least privilege; using this alone fails with "invalid template". + // Should construct ARN: 'arn:aws:catalog:::product/' + this.scProductId + // options.role.addToPolicy(new PolicyStatement({ + // resources: ['*'], + // actions: ['servicecatalog:UpdateProduct', 'servicecatalog:ListProvisioningArtifacts', 'servicecatalog:CreateProvisioningArtifact'], + // })); + + // the Action's Role needs to read from the Bucket to get artifacts + options.bucket.grantRead(options.role); + + return { + configuration: { + TemplateFilePath: this.templatePath, + ProductVersionName: this.productVersionName, + ProductVersionDescription: this.productVersionDescription, + ProductType: this.productType, + ProductId: this.productId, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-codepipeline-actions/package.json b/packages/@aws-cdk/aws-codepipeline-actions/package.json index de7642c8649db..5eb4fc886561b 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package.json @@ -86,6 +86,7 @@ "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-servicecatalog": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sns-subscriptions": "0.0.0", "@aws-cdk/aws-stepfunctions": "0.0.0", @@ -108,6 +109,7 @@ "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-servicecatalog": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sns-subscriptions": "0.0.0", "@aws-cdk/aws-stepfunctions": "0.0.0", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/servicecatalog/test.servicecatalog-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/servicecatalog/test.servicecatalog-action.ts new file mode 100644 index 0000000000000..06c46cff3adae --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/servicecatalog/test.servicecatalog-action.ts @@ -0,0 +1,130 @@ +import { expect, haveResourceLike } from '@aws-cdk/assert'; +import * as codecommit from '@aws-cdk/aws-codecommit'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import { Stack } from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as cpactions from '../../lib'; + +/* eslint-disable quote-props */ + +export = { + 'addAction succesfully leads to creation of codepipeline service catalog action'(test: Test) { + // GIVEN + const stack = new TestFixture(); + // WHEN + stack.deployStage.addAction(new cpactions.ServiceCatalogDeployAction({ + actionName: 'ServiceCatalogTest', + templatePath: stack.sourceOutput.atPath('template.yaml'), + productVersionDescription: 'This is a description of the version.', + productVersionName: 'VersionName', + productId: 'prod-xxxxxxxxx', + })); + // THEN + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + 'Stages': [ + { 'Name': 'Source' /* don't care about the rest */ }, + { + 'Name': 'Deploy', + 'Actions': [ + { + 'ActionTypeId': { + 'Category': 'Deploy', + 'Owner': 'AWS', + 'Provider': 'ServiceCatalog', + 'Version': '1', + }, + 'Configuration': { + 'TemplateFilePath': 'SourceArtifact::template.yaml', + 'ProductVersionDescription': 'This is a description of the version.', + 'ProductVersionName': 'VersionName', + 'ProductType': 'CLOUD_FORMATION_TEMPLATE', + 'ProductId': 'prod-xxxxxxxxx', + }, + 'InputArtifacts': [ + { + 'Name': 'SourceArtifact', + }, + ], + 'Name': 'ServiceCatalogTest', + }, + ], + }, + ], + })); + + test.done(); + }, + 'deployment without a description works successfully'(test: Test) { + // GIVEN + const stack = new TestFixture(); + // WHEN + stack.deployStage.addAction(new cpactions.ServiceCatalogDeployAction({ + actionName: 'ServiceCatalogTest', + templatePath: stack.sourceOutput.atPath('template.yaml'), + productVersionName: 'VersionName', + productId: 'prod-xxxxxxxxx', + })); + // THEN + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + 'Stages': [ + { 'Name': 'Source' /* don't care about the rest */ }, + { + 'Name': 'Deploy', + 'Actions': [ + { + 'ActionTypeId': { + 'Category': 'Deploy', + 'Owner': 'AWS', + 'Provider': 'ServiceCatalog', + 'Version': '1', + }, + 'Configuration': { + 'TemplateFilePath': 'SourceArtifact::template.yaml', + 'ProductVersionName': 'VersionName', + 'ProductType': 'CLOUD_FORMATION_TEMPLATE', + 'ProductId': 'prod-xxxxxxxxx', + }, + 'InputArtifacts': [ + { + 'Name': 'SourceArtifact', + }, + ], + 'Name': 'ServiceCatalogTest', + }, + ], + }, + ], + })); + + test.done(); + }, +}; + +/** + * A test stack with a half-prepared pipeline ready to add CloudFormation actions to + */ +class TestFixture extends Stack { + public readonly pipeline: codepipeline.Pipeline; + public readonly sourceStage: codepipeline.IStage; + public readonly deployStage: codepipeline.IStage; + public readonly repo: codecommit.Repository; + public readonly sourceOutput: codepipeline.Artifact; + + constructor() { + super(); + + this.pipeline = new codepipeline.Pipeline(this, 'Pipeline'); + this.sourceStage = this.pipeline.addStage({ stageName: 'Source' }); + this.deployStage = this.pipeline.addStage({ stageName: 'Deploy' }); + this.repo = new codecommit.Repository(this, 'MyVeryImportantRepo', { + repositoryName: 'my-very-important-repo', + }); + this.sourceOutput = new codepipeline.Artifact('SourceArtifact'); + const source = new cpactions.CodeCommitSourceAction({ + actionName: 'Source', + output: this.sourceOutput, + repository: this.repo, + }); + this.sourceStage.addAction(source); + } +} \ No newline at end of file From add23bf3331f73830c918953566e1d772da34cc0 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Thu, 23 Jul 2020 12:47:45 -0700 Subject: [PATCH 16/30] feat(route53-patterns): the route53-patterns module is now stable (#9232) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-route53-patterns/README.md | 4 +--- packages/@aws-cdk/aws-route53-patterns/package.json | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-route53-patterns/README.md b/packages/@aws-cdk/aws-route53-patterns/README.md index f06f5fcd7e3a1..a652b8ee153f6 100644 --- a/packages/@aws-cdk/aws-route53-patterns/README.md +++ b/packages/@aws-cdk/aws-route53-patterns/README.md @@ -2,9 +2,7 @@ --- -![cdk-constructs: Developer Preview](https://img.shields.io/badge/cdk--constructs-developer--preview-informational.svg?style=for-the-badge) - -> The APIs of higher level constructs in this module are in **developer preview** before they become stable. We will only make breaking changes to address unforeseen API issues. Therefore, these APIs are not subject to [Semantic Versioning](https://semver.org/), and breaking changes will be announced in release notes. This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. +![cdk-constructs: Stable](https://img.shields.io/badge/cdk--constructs-stable-success.svg?style=for-the-badge) --- diff --git a/packages/@aws-cdk/aws-route53-patterns/package.json b/packages/@aws-cdk/aws-route53-patterns/package.json index be7208bf11a6e..040ec243a21a8 100644 --- a/packages/@aws-cdk/aws-route53-patterns/package.json +++ b/packages/@aws-cdk/aws-route53-patterns/package.json @@ -92,8 +92,8 @@ "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, - "stability": "experimental", - "maturity": "developer-preview", + "stability": "stable", + "maturity": "stable", "awscdkio": { "announce": false }, From e6dca529098e54c91f0706019b9ee06522ddb025 Mon Sep 17 00:00:00 2001 From: Bryan Pan Date: Thu, 23 Jul 2020 15:43:38 -0700 Subject: [PATCH 17/30] feat(appsync): grant APIs for managing permissions (#8993) **[ISSUE]** AppSync allows for `authorizationType` of `AWS_IAM`. However, no easy function or attribute to retrieve the `IAM Role` for later use. **[APPROACH]** Build out a basic granting interface that allows users to pass an `IAM Role` to grant permissions access. ``` const api = new appsync.GraphQLApi(...); const role = new iam.Role(..); api.grant(role, ... ); ``` **[NOTE]** Current implementation let's you use `IAM` authorization, but doesn't come with `grant`. Fixes #6772 #7871 #7313 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-appsync/.gitignore | 3 +- packages/@aws-cdk/aws-appsync/README.md | 295 ++++----- .../@aws-cdk/aws-appsync/lib/graphqlapi.ts | 133 +++- .../aws-appsync/test/appsync-apikey.test.ts | 8 +- .../aws-appsync/test/appsync-grant.test.ts | 558 +++++++++++++++++ .../aws-appsync/test/appsync.test.graphql | 11 + .../@aws-cdk/aws-appsync/test/appsync.test.ts | 4 +- .../test/integ.graphql-iam.expected.json | 583 ++++++++++++++++++ .../test/integ.graphql-iam.graphql | 19 + .../aws-appsync/test/integ.graphql-iam.ts | 110 ++++ .../{schema.graphql => integ.graphql.graphql} | 0 .../aws-appsync/test/integ.graphql.ts | 4 +- .../test/verify.integ.graphql-iam.sh | 18 + .../aws-appsync/test/verify.integ.graphql.sh | 4 +- .../aws-appsync/test/verify/iam-query.js | 40 ++ 15 files changed, 1597 insertions(+), 193 deletions(-) create mode 100644 packages/@aws-cdk/aws-appsync/test/appsync-grant.test.ts create mode 100644 packages/@aws-cdk/aws-appsync/test/appsync.test.graphql create mode 100644 packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.expected.json create mode 100644 packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.graphql create mode 100644 packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.ts rename packages/@aws-cdk/aws-appsync/test/{schema.graphql => integ.graphql.graphql} (100%) create mode 100644 packages/@aws-cdk/aws-appsync/test/verify.integ.graphql-iam.sh create mode 100644 packages/@aws-cdk/aws-appsync/test/verify/iam-query.js diff --git a/packages/@aws-cdk/aws-appsync/.gitignore b/packages/@aws-cdk/aws-appsync/.gitignore index d8a8561d50885..e0a8bac32ac3f 100644 --- a/packages/@aws-cdk/aws-appsync/.gitignore +++ b/packages/@aws-cdk/aws-appsync/.gitignore @@ -16,4 +16,5 @@ nyc.config.js !.eslintrc.js !jest.config.js -junit.xml \ No newline at end of file +!test/verify/*.js +junit.xml diff --git a/packages/@aws-cdk/aws-appsync/README.md b/packages/@aws-cdk/aws-appsync/README.md index e815444352288..f9811ddd9c1be 100644 --- a/packages/@aws-cdk/aws-appsync/README.md +++ b/packages/@aws-cdk/aws-appsync/README.md @@ -13,186 +13,149 @@ --- -This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. +The `@aws-cdk/aws-appsync` package contains constructs for building flexible +APIs that use GraphQL. -## Usage Example +### Example -Given the following GraphQL schema file `schema.graphql`: +Example of a GraphQL API with `AWS_IAM` authorization resolving into a DynamoDb +backend data source. -```graphql -type ServiceVersion { - version: String! -} +GraphQL schema file `schema.graphql`: -type Customer { - id: String! - name: String! +```gql +type demo { + id: String! + version: String! } - -input SaveCustomerInput { - name: String! +type Query { + getDemos: [ test! ] } - -type Order { - customer: String! - order: String! +input DemoInput { + version: String! } - -type Query { - getServiceVersion: ServiceVersion - getCustomers: [Customer] - getCustomer(id: String): Customer +type Mutation { + addDemo(input: DemoInput!): demo } +``` -input FirstOrderInput { - product: String! - quantity: Int! -} +CDK stack file `app-stack.ts`: +```ts +import * as appsync from '@aws-cdk/aws-appsync'; +import * as db from '@aws-cdk/aws-dynamodb'; + +const api = new appsync.GraphQLApi(stack, 'Api', { + name: 'demo', + schemaDefinitionFile: join(__dirname, 'schema.graphql'), + authorizationConfig: { + defaultAuthorization: { + authorizationType: appsync.AuthorizationType.IAM + }, + }, +}); + +const demoTable = new db.Table(stack, 'DemoTable', { + partitionKey: { + name: 'id', + type: AttributeType.STRING, + }, +}); + +const demoDS = api.addDynamoDbDataSource('demoDataSource', 'Table for Demos"', demoTable); + +// Resolver for the Query "getDemos" that scans the DyanmoDb table and returns the entire list. +demoDS.createResolver({ + typeName: 'Query', + fieldName: 'getDemos', + requestMappingTemplate: MappingTemplate.dynamoDbScanTable(), + responseMappingTemplate: MappingTemplate.dynamoDbResultList(), +}); + +// Resolver for the Mutation "addDemo" that puts the item into the DynamoDb table. +demoDS.createResolver({ + typeName: 'Mutation', + fieldName: 'addDemo', + requestMappingTemplate: MappingTemplate.dynamoDbPutItem(PrimaryKey.partition('id').auto(), Values.projecting('demo')), + responseMappingTemplate: MappingTemplate.dynamoDbResultItem(), +}); +``` + +## Permissions + +When using `AWS_IAM` as the authorization type for GraphQL API, an IAM Role +with correct permissions must be used for access to API. + +When configuring permissions, you can specify specific resources to only be +accessible by `IAM` authorization. For example, if you want to only allow mutability +for `IAM` authorized access you would configure the following. + +In `schema.graphql`: +```ts type Mutation { - addCustomer(customer: SaveCustomerInput!): Customer - saveCustomer(id: String!, customer: SaveCustomerInput!): Customer - removeCustomer(id: String!): Customer - saveCustomerWithFirstOrder(customer: SaveCustomerInput!, order: FirstOrderInput!, referral: String): Order - doPostOnAws: String! + updateExample(...): ... + @aws_iam } ``` -the following CDK app snippet will create a complete CRUD AppSync API: +In `IAM`: +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "appsync:GraphQL" + ], + "Resource": [ + "arn:aws:appsync:REGION:ACCOUNT_ID:apis/GRAPHQL_ID/types/Mutation/fields/updateExample" + ] + } + ] +} +``` + +See [documentation](https://docs.aws.amazon.com/appsync/latest/devguide/security.html#aws-iam-authorization) for more details. + +To make this easier, CDK provides `grant` API. + +Use the `grant` function for more granular authorization. ```ts -export class ApiStack extends Stack { - constructor(scope: Construct, id: string) { - super(scope, id); - - const userPool = new UserPool(this, 'UserPool', { - userPoolName: 'myPool', - }); - - const api = new GraphQLApi(this, 'Api', { - name: `demoapi`, - logConfig: { - fieldLogLevel: FieldLogLevel.ALL, - }, - authorizationConfig: { - defaultAuthorization: { - authorizationType: AuthorizationType.USER_POOL, - userPoolConfig: { - userPool, - defaultAction: UserPoolDefaultAction.ALLOW - }, - }, - additionalAuthorizationModes: [ - { - authorizationType: AuthorizationType.API_KEY, - } - ], - }, - schemaDefinitionFile: './schema.graphql', - }); - - const noneDS = api.addNoneDataSource('None', 'Dummy data source'); - - noneDS.createResolver({ - typeName: 'Query', - fieldName: 'getServiceVersion', - requestMappingTemplate: MappingTemplate.fromString(JSON.stringify({ - version: '2017-02-28', - })), - responseMappingTemplate: MappingTemplate.fromString(JSON.stringify({ - version: 'v1', - })), - }); - - const customerTable = new Table(this, 'CustomerTable', { - billingMode: BillingMode.PAY_PER_REQUEST, - partitionKey: { - name: 'id', - type: AttributeType.STRING, - }, - }); - // If your table is already created you can also use use import table and use it as data source. - const customerDS = api.addDynamoDbDataSource('Customer', 'The customer data source', customerTable); - customerDS.createResolver({ - typeName: 'Query', - fieldName: 'getCustomers', - requestMappingTemplate: MappingTemplate.dynamoDbScanTable(), - responseMappingTemplate: MappingTemplate.dynamoDbResultList(), - }); - customerDS.createResolver({ - typeName: 'Query', - fieldName: 'getCustomer', - requestMappingTemplate: MappingTemplate.dynamoDbGetItem('id', 'id'), - responseMappingTemplate: MappingTemplate.dynamoDbResultItem(), - }); - customerDS.createResolver({ - typeName: 'Mutation', - fieldName: 'addCustomer', - requestMappingTemplate: MappingTemplate.dynamoDbPutItem( - PrimaryKey.partition('id').auto(), - Values.projecting('customer')), - responseMappingTemplate: MappingTemplate.dynamoDbResultItem(), - }); - customerDS.createResolver({ - typeName: 'Mutation', - fieldName: 'saveCustomer', - requestMappingTemplate: MappingTemplate.dynamoDbPutItem( - PrimaryKey.partition('id').is('id'), - Values.projecting('customer')), - responseMappingTemplate: MappingTemplate.dynamoDbResultItem(), - }); - customerDS.createResolver({ - typeName: 'Mutation', - fieldName: 'saveCustomerWithFirstOrder', - requestMappingTemplate: MappingTemplate.dynamoDbPutItem( - PrimaryKey - .partition('order').auto() - .sort('customer').is('customer.id'), - Values - .projecting('order') - .attribute('referral').is('referral')), - responseMappingTemplate: MappingTemplate.dynamoDbResultItem(), - }); - customerDS.createResolver({ - typeName: 'Mutation', - fieldName: 'removeCustomer', - requestMappingTemplate: MappingTemplate.dynamoDbDeleteItem('id', 'id'), - responseMappingTemplate: MappingTemplate.dynamoDbResultItem(), - }); - - const httpDS = api.addHttpDataSource('http', 'The http data source', 'https://aws.amazon.com/'); - - httpDS.createResolver({ - typeName: 'Mutation', - fieldName: 'doPostOnAws', - requestMappingTemplate: MappingTemplate.fromString(`{ - "version": "2018-05-29", - "method": "POST", - # if full path is https://api.xxxxxxxxx.com/posts then resourcePath would be /posts - "resourcePath": "/path/123", - "params":{ - "body": $util.toJson($ctx.args), - "headers":{ - "Content-Type": "application/json", - "Authorization": "$ctx.request.headers.Authorization" - } - } - }`), - responseMappingTemplate: MappingTemplate.fromString(` - ## Raise a GraphQL field error in case of a datasource invocation error - #if($ctx.error) - $util.error($ctx.error.message, $ctx.error.type) - #end - ## if the response status code is not 200, then return an error. Else return the body ** - #if($ctx.result.statusCode == 200) - ## If response is 200, return the body. - $ctx.result.body - #else - ## If response is not 200, append the response to error block. - $utils.appendError($ctx.result.body, "$ctx.result.statusCode") - #end - `), - }); - } -} +const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), +}); +const api = new appsync.GraphQLApi(stack, 'API', { + definition +}); + +api.grant(role, appsync.IamResource.custom('types/Mutation/fields/updateExample'), 'appsync:GraphQL') ``` + +### IamResource + +In order to use the `grant` functions, you need to use the class `IamResource`. + +- `IamResource.custom(...arns)` permits custom ARNs and requires an argument. + +- `IamResouce.ofType(type, ...fields)` permits ARNs for types and their fields. + +- `IamResource.all()` permits ALL resources. + +### Generic Permissions + +Alternatively, you can use more generic `grant` functions to accomplish the same usage. + +These include: +- grantMutation (use to grant access to Mutation fields) +- grantQuery (use to grant access to Query fields) +- grantSubscription (use to grant access to Subscription fields) + +```ts +// For generic types +api.grantMutation(role, 'updateExample'); + +// For custom types and granular design +api.grant(role, appsync.IamResource.ofType('Mutation', 'updateExample'), 'appsync:GraphQL'); +``` \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts index c01883c81b011..9678c0dd3fee9 100644 --- a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts +++ b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts @@ -1,18 +1,10 @@ +import { readFileSync } from 'fs'; import { IUserPool } from '@aws-cdk/aws-cognito'; import { ITable } from '@aws-cdk/aws-dynamodb'; -import { - ManagedPolicy, - Role, - ServicePrincipal, -} from '@aws-cdk/aws-iam'; +import { Grant, IGrantable, ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import { IFunction } from '@aws-cdk/aws-lambda'; -import { Construct, Duration, IResolvable } from '@aws-cdk/core'; -import { readFileSync } from 'fs'; -import { - CfnApiKey, - CfnGraphQLApi, - CfnGraphQLSchema, -} from './appsync.generated'; +import { Construct, Duration, IResolvable, Stack } from '@aws-cdk/core'; +import { CfnApiKey, CfnGraphQLApi, CfnGraphQLSchema } from './appsync.generated'; import { DynamoDbDataSource, HttpDataSource, LambdaDataSource, NoneDataSource } from './data-source'; /** @@ -56,7 +48,7 @@ export interface AuthorizationMode { readonly userPoolConfig?: UserPoolConfig; /** * If authorizationType is `AuthorizationType.API_KEY`, this option can be configured. - * @default - check default values of `ApiKeyConfig` memebers + * @default - name: 'DefaultAPIKey' | description: 'Default API Key created by CDK' */ readonly apiKeyConfig?: ApiKeyConfig; /** @@ -249,6 +241,65 @@ export interface GraphQLApiProps { } +/** + * A class used to generate resource arns for AppSync + */ +export class IamResource { + /** + * Generate the resource names given custom arns + * + * @param arns The custom arns that need to be permissioned + * + * Example: custom('/types/Query/fields/getExample') + */ + public static custom(...arns: string[]): IamResource { + if (arns.length === 0) { + throw new Error('At least 1 custom ARN must be provided.'); + } + return new IamResource(arns); + } + + /** + * Generate the resource names given a type and fields + * + * @param type The type that needs to be allowed + * @param fields The fields that need to be allowed, if empty grant permissions to ALL fields + * + * Example: ofType('Query', 'GetExample') + */ + public static ofType(type: string, ...fields: string[]): IamResource { + const arns = fields.length ? fields.map((field) => `types/${type}/fields/${field}`) : [ `types/${type}/*` ]; + return new IamResource(arns); + } + + /** + * Generate the resource names that accepts all types: `*` + */ + public static all(): IamResource { + return new IamResource(['*']); + } + + private arns: string[]; + + private constructor(arns: string[]){ + this.arns = arns; + } + + /** + * Return the Resource ARN + * + * @param api The GraphQL API to give permissions + */ + public resourceArns(api: GraphQLApi): string[] { + return this.arns.map((arn) => Stack.of(api).formatArn({ + service: 'appsync', + resource: `apis/${api.apiId}`, + sep: '/', + resourceName: `${arn}`, + })); + } +} + /** * An AppSync GraphQL API */ @@ -269,7 +320,7 @@ export class GraphQLApi extends Construct { /** * the name of the API */ - public name: string; + public readonly name: string; /** * underlying CFN schema resource */ @@ -350,7 +401,7 @@ export class GraphQLApi extends Construct { name: 'DefaultAPIKey', description: 'Default API Key created by CDK', }; - this.createAPIKey(apiKeyConfig); + this._apiKey = this.createAPIKey(apiKeyConfig); } let definition; @@ -433,6 +484,56 @@ export class GraphQLApi extends Construct { }); } + /** + * Adds an IAM policy statement associated with this GraphQLApi to an IAM + * principal's policy. + * + * @param grantee The principal + * @param resources The set of resources to allow (i.e. ...:[region]:[accountId]:apis/GraphQLId/...) + * @param actions The actions that should be granted to the principal (i.e. appsync:graphql ) + */ + public grant(grantee: IGrantable, resources: IamResource, ...actions: string[]): Grant { + return Grant.addToPrincipal({ + grantee, + actions, + resourceArns: resources.resourceArns(this), + scope: this, + }); + } + + /** + * Adds an IAM policy statement for Mutation access to this GraphQLApi to an IAM + * principal's policy. + * + * @param grantee The principal + * @param fields The fields to grant access to that are Mutations (leave blank for all) + */ + public grantMutation(grantee: IGrantable, ...fields: string[]): Grant { + return this.grant(grantee, IamResource.ofType('Mutation', ...fields), 'appsync:GraphQL'); + } + + /** + * Adds an IAM policy statement for Query access to this GraphQLApi to an IAM + * principal's policy. + * + * @param grantee The principal + * @param fields The fields to grant access to that are Queries (leave blank for all) + */ + public grantQuery(grantee: IGrantable, ...fields: string[]): Grant { + return this.grant(grantee, IamResource.ofType('Query', ...fields), 'appsync:GraphQL'); + } + + /** + * Adds an IAM policy statement for Subscription access to this GraphQLApi to an IAM + * principal's policy. + * + * @param grantee The principal + * @param fields The fields to grant access to that are Subscriptions (leave blank for all) + */ + public grantSubscription(grantee: IGrantable, ...fields: string[]): Grant { + return this.grant(grantee, IamResource.ofType('Subscription', ...fields), 'appsync:GraphQL'); + } + private validateAuthorizationProps(props: GraphQLApiProps) { const defaultAuthorizationType = props.authorizationConfig?.defaultAuthorization?.authorizationType || @@ -534,7 +635,7 @@ export class GraphQLApi extends Construct { description: config.description || 'Default API Key created by CDK', apiId: this.apiId, }); - this._apiKey = key.attrApiKey; + return key.attrApiKey; } private formatAdditionalAuthorizationModes( diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-apikey.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-apikey.test.ts index 14b87e5eee463..da49317ee5a64 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-apikey.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-apikey.test.ts @@ -11,7 +11,7 @@ describe('AppSync Authorization Config', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinitionFile: path.join(__dirname, 'schema.graphql'), + schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), }); // THEN @@ -25,7 +25,7 @@ describe('AppSync Authorization Config', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinitionFile: path.join(__dirname, 'schema.graphql'), + schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.IAM, @@ -47,7 +47,7 @@ describe('AppSync Authorization Config', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinitionFile: path.join(__dirname, 'schema.graphql'), + schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.IAM, @@ -66,7 +66,7 @@ describe('AppSync Authorization Config', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinitionFile: path.join(__dirname, 'schema.graphql'), + schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.IAM, diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-grant.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-grant.test.ts new file mode 100644 index 0000000000000..f770cbad26319 --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/appsync-grant.test.ts @@ -0,0 +1,558 @@ +import { join } from 'path'; +import '@aws-cdk/assert/jest'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import * as appsync from '../lib'; + +let stack: cdk.Stack; +let role: iam.Role; +let api: appsync.GraphQLApi; +beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + api = new appsync.GraphQLApi(stack, 'API', { + name: 'demo', + schemaDefinitionFile: join(__dirname, 'appsync.test.graphql'), + authorizationConfig: { + defaultAuthorization: { + authorizationType: appsync.AuthorizationType.IAM, + }, + }, + }); +}); + +describe('grant Permissions', () => { + + test('IamResource throws error when custom is called with no arguments', () => { + // WHEN + const when = () => { + api.grant(role, appsync.IamResource.custom(), 'appsync:GraphQL'); + }; + + //THEN + expect(when).toThrowError('At least 1 custom ARN must be provided.'); + }); + + test('grant provides custom permissions when called with `custom` argument', () => { + // WHEN + api.grant(role, appsync.IamResource.custom('types/Mutation/fields/addTest'), 'appsync:GraphQL'); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'appsync:GraphQL', + Resource: { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/types/Mutation/fields/addTest', + ]], + }, + }, + ], + }, + }); + }); + + test('grant provides [type parameter]/* permissions when called with `type` argument', () => { + // WHEN + api.grant(role, appsync.IamResource.ofType('Mutation'), 'appsync:GraphQL'); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'appsync:GraphQL', + Resource: { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/types/Mutation/*', + ]], + }, + }, + ], + }, + }); + }); + + test('grant provides fields/[field param] permissions when called with `type` and `field` argument', () => { + // WHEN + api.grant(role, appsync.IamResource.ofType('Mutation', 'addTest'), 'appsync:GraphQL'); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'appsync:GraphQL', + Resource: { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/types/Mutation/fields/addTest', + ]], + }, + }, + ], + }, + }); + }); + + test('grant provides all permissions when called with IamResource.all()', () => { + // WHEN + api.grant(role, appsync.IamResource.all(), 'appsync:GraphQL'); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'appsync:GraphQL', + Resource: { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/*', + ]], + }, + }, + ], + }, + }); + }); + + test('grant provides multiple permissions using one IamResource custom call', () => { + // WHEN + api.grant(role, appsync.IamResource.custom('I', 'am', 'custom'), 'appsync:GraphQL'); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'appsync:GraphQL', + Resource: [ + { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/I', + ]]}, + { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/am', + ]]}, + { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/custom', + ]]}, + ], + }, + ], + }, + }); + }); + + test('grant provides multiple permissions using one IamResource ofType call', () => { + // WHEN + api.grant(role, appsync.IamResource.ofType('I', 'am', 'custom'), 'appsync:GraphQL'); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'appsync:GraphQL', + Resource: [ + { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/types/I/fields/am', + ]]}, + { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/types/I/fields/custom', + ]]}, + ], + }, + ], + }, + }); + }); +}); + +describe('grantMutation Permissions', () => { + + test('grantMutation provides Mutation/* permissions when called with no `fields` argument', () => { + // WHEN + api.grantMutation(role); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'appsync:GraphQL', + Resource: { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/types/Mutation/*', + ]], + }, + }, + ], + }, + }); + }); + + test('grantMutation provides fields/[field param] permissions when called with `fields` argument', () => { + // WHEN + api.grantMutation(role, 'addTest'); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'appsync:GraphQL', + Resource: { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/types/Mutation/fields/addTest', + ]], + }, + }, + ], + }, + }); + }); + + test('grantMutation provides multiple permissions when called with `fields` argument', () => { + // WHEN + api.grantMutation(role, 'addTest', 'removeTest'); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'appsync:GraphQL', + Resource: [ + { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/types/Mutation/fields/addTest', + ]]}, + { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/types/Mutation/fields/removeTest', + ]]}, + ], + }, + ], + }, + }); + }); +}); + +describe('grantQuery Permissions', () => { + + test('grantQuery provides Query/* permissions when called without the `fields` argument', () => { + // WHEN + api.grantQuery(role); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'appsync:GraphQL', + Resource: { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/types/Query/*', + ]], + }, + }, + ], + }, + }); + }); + + test('grantQuery provides fields/[field param] permissions when called with `fields` arugment', () => { + // WHEN + api.grantQuery(role, 'getTest'); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'appsync:GraphQL', + Resource: { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/types/Query/fields/getTest', + ]], + }, + }, + ], + }, + }); + }); + + test('grantQuery provides multiple permissions when called with `fields` argument', () => { + // WHEN + api.grantQuery(role, 'getTests', 'getTest'); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'appsync:GraphQL', + Resource: [ + { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/types/Query/fields/getTests', + ]]}, + { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/types/Query/fields/getTest', + ]]}, + ], + }, + ], + }, + }); + }); +}); + +describe('grantSubscription Permissions', () => { + + test('grantSubscription provides Subscription/* permissions when called without `fields` argument', () => { + // WHEN + api.grantSubscription(role); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'appsync:GraphQL', + Resource: { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/types/Subscription/*', + ]], + }, + }, + ], + }, + }); + }); + + test('grantSubscription provides fields/[field param] when called with `field` argument', () => { + api.grantSubscription(role, 'subscribe'); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'appsync:GraphQL', + Resource: { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/types/Subscription/fields/subscribe', + ]], + }, + }, + ], + }, + }); + }); + + test('grantSubscription provides multiple permissions when called with `fields` argument', () => { + // WHEN + api.grantSubscription(role, 'subscribe', 'custom'); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'appsync:GraphQL', + Resource: [ + { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/types/Subscription/fields/subscribe', + ]]}, + { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': [ 'API62EA1CFF', 'ApiId' ] }, + '/types/Subscription/fields/custom', + ]]}, + ], + }, + ], + }, + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/appsync.test.graphql b/packages/@aws-cdk/aws-appsync/test/appsync.test.graphql new file mode 100644 index 0000000000000..b5d531d31fbe9 --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/appsync.test.graphql @@ -0,0 +1,11 @@ +type test { + version: String! +} + +type Query { + getTests: [ test! ]! +} + +type Mutation { + addTest(version: String!): test +} diff --git a/packages/@aws-cdk/aws-appsync/test/appsync.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync.test.ts index edb263eed6800..7a4a1fec1fab6 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync.test.ts @@ -1,6 +1,6 @@ +import * as path from 'path'; import '@aws-cdk/assert/jest'; import * as cdk from '@aws-cdk/core'; -import * as path from 'path'; import * as appsync from '../lib'; test('should not throw an Error', () => { @@ -12,7 +12,7 @@ test('should not throw an Error', () => { new appsync.GraphQLApi(stack, 'api', { authorizationConfig: {}, name: 'api', - schemaDefinitionFile: path.join(__dirname, 'schema.graphql'), + schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), }); }; diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.expected.json b/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.expected.json new file mode 100644 index 0000000000000..e83e633d67676 --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.expected.json @@ -0,0 +1,583 @@ +{ + "Resources": { + "PoolsmsRoleC3352CE6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "awsappsyncintegPool5D14B05B" + } + }, + "Effect": "Allow", + "Principal": { + "Service": "cognito-idp.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "sns-publish" + } + ] + } + }, + "PoolD3F588B8": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsConfiguration": { + "ExternalId": "awsappsyncintegPool5D14B05B", + "SnsCallerArn": { + "Fn::GetAtt": [ + "PoolsmsRoleC3352CE6", + "Arn" + ] + } + }, + "SmsVerificationMessage": "The verification code to your new account is {####}", + "UserPoolName": "myPool", + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + } + }, + "ApiF70053CD": { + "Type": "AWS::AppSync::GraphQLApi", + "Properties": { + "AuthenticationType": "AMAZON_COGNITO_USER_POOLS", + "Name": "Integ_Test_IAM", + "AdditionalAuthenticationProviders": [ + { + "AuthenticationType": "AWS_IAM" + } + ], + "UserPoolConfig": { + "AwsRegion": { + "Ref": "AWS::Region" + }, + "DefaultAction": "ALLOW", + "UserPoolId": { + "Ref": "PoolD3F588B8" + } + } + } + }, + "ApiSchema510EECD7": { + "Type": "AWS::AppSync::GraphQLSchema", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "ApiF70053CD", + "ApiId" + ] + }, + "Definition": "type test @aws_iam {\n id: String!\n version: String!\n}\n\ntype Query {\n getTest(id: String!): test\n getTests: [ test! ]\n @aws_iam \n}\n\ninput TestInput {\n version: String!\n}\n\ntype Mutation {\n addTest(input: TestInput!): test\n @aws_iam\n}\n" + } + }, + "ApitestDataSourceDSServiceRoleE543E310": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "appsync.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ApitestDataSourceDSServiceRoleDefaultPolicy53D2252C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TestTable5769773A", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ApitestDataSourceDSServiceRoleDefaultPolicy53D2252C", + "Roles": [ + { + "Ref": "ApitestDataSourceDSServiceRoleE543E310" + } + ] + } + }, + "ApitestDataSourceDS776EA507": { + "Type": "AWS::AppSync::DataSource", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "ApiF70053CD", + "ApiId" + ] + }, + "Name": "testDataSource", + "Type": "AMAZON_DYNAMODB", + "Description": "Table for Tests\"", + "DynamoDBConfig": { + "AwsRegion": { + "Ref": "AWS::Region" + }, + "TableName": { + "Ref": "TestTable5769773A" + } + }, + "ServiceRoleArn": { + "Fn::GetAtt": [ + "ApitestDataSourceDSServiceRoleE543E310", + "Arn" + ] + } + } + }, + "ApitestDataSourceDSQuerygetTestResolver1A3C9201": { + "Type": "AWS::AppSync::Resolver", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "ApiF70053CD", + "ApiId" + ] + }, + "FieldName": "getTest", + "TypeName": "Query", + "DataSourceName": "testDataSource", + "Kind": "UNIT", + "RequestMappingTemplate": "{\"version\": \"2017-02-28\", \"operation\": \"GetItem\", \"key\": {\"id\": $util.dynamodb.toDynamoDBJson($ctx.args.id)}}", + "ResponseMappingTemplate": "$util.toJson($ctx.result)" + }, + "DependsOn": [ + "ApiSchema510EECD7", + "ApitestDataSourceDS776EA507" + ] + }, + "ApitestDataSourceDSQuerygetTestsResolver61ED88B6": { + "Type": "AWS::AppSync::Resolver", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "ApiF70053CD", + "ApiId" + ] + }, + "FieldName": "getTests", + "TypeName": "Query", + "DataSourceName": "testDataSource", + "Kind": "UNIT", + "RequestMappingTemplate": "{\"version\" : \"2017-02-28\", \"operation\" : \"Scan\"}", + "ResponseMappingTemplate": "$util.toJson($ctx.result.items)" + }, + "DependsOn": [ + "ApiSchema510EECD7", + "ApitestDataSourceDS776EA507" + ] + }, + "ApitestDataSourceDSMutationaddTestResolver0D3A4591": { + "Type": "AWS::AppSync::Resolver", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "ApiF70053CD", + "ApiId" + ] + }, + "FieldName": "addTest", + "TypeName": "Mutation", + "DataSourceName": "testDataSource", + "Kind": "UNIT", + "RequestMappingTemplate": "\n #set($input = $ctx.args.test)\n \n {\n \"version\": \"2017-02-28\",\n \"operation\": \"PutItem\",\n \"key\" : {\n \"id\" : $util.dynamodb.toDynamoDBJson($util.autoId())\n },\n \"attributeValues\": $util.dynamodb.toMapValuesJson($input)\n }", + "ResponseMappingTemplate": "$util.toJson($ctx.result)" + }, + "DependsOn": [ + "ApiSchema510EECD7", + "ApitestDataSourceDS776EA507" + ] + }, + "TestTable5769773A": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "LambdaIAM687B49AF": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "LambdaIAMDefaultPolicy96DEA124": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "appsync:graphql", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":appsync:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":apis/", + { + "Fn::GetAtt": [ + "ApiF70053CD", + "ApiId" + ] + }, + "/types/Query/fields/getTests" + ] + ] + } + }, + { + "Action": "appsync:GraphQL", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":appsync:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":apis/", + { + "Fn::GetAtt": [ + "ApiF70053CD", + "ApiId" + ] + }, + "/types/test/*" + ] + ] + } + }, + { + "Action": "appsync:GraphQL", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":appsync:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":apis/", + { + "Fn::GetAtt": [ + "ApiF70053CD", + "ApiId" + ] + }, + "/types/Mutation/fields/addTest" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaIAMDefaultPolicy96DEA124", + "Roles": [ + { + "Ref": "LambdaIAM687B49AF" + } + ] + } + }, + "testQuery6695AADE": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParametersf38244b9028d22d4e265a5b466bdba928d93b5a4ac2b4bbf583309b3f027f044S3Bucket7A0FC067" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersf38244b9028d22d4e265a5b466bdba928d93b5a4ac2b4bbf583309b3f027f044S3VersionKeyCF9AAB81" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersf38244b9028d22d4e265a5b466bdba928d93b5a4ac2b4bbf583309b3f027f044S3VersionKeyCF9AAB81" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "iam-query.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaIAM687B49AF", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Environment": { + "Variables": { + "APPSYNC_ENDPOINT": { + "Fn::GetAtt": [ + "ApiF70053CD", + "GraphQLUrl" + ] + } + } + } + }, + "DependsOn": [ + "LambdaIAMDefaultPolicy96DEA124", + "LambdaIAM687B49AF" + ] + }, + "testFailServiceRole9FF22F85": { + "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" + ] + ] + } + ] + } + }, + "testFail33CE01D1": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParametersf38244b9028d22d4e265a5b466bdba928d93b5a4ac2b4bbf583309b3f027f044S3Bucket7A0FC067" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersf38244b9028d22d4e265a5b466bdba928d93b5a4ac2b4bbf583309b3f027f044S3VersionKeyCF9AAB81" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersf38244b9028d22d4e265a5b466bdba928d93b5a4ac2b4bbf583309b3f027f044S3VersionKeyCF9AAB81" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "iam-query.handler", + "Role": { + "Fn::GetAtt": [ + "testFailServiceRole9FF22F85", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Environment": { + "Variables": { + "APPSYNC_ENDPOINT": { + "Fn::GetAtt": [ + "ApiF70053CD", + "GraphQLUrl" + ] + } + } + } + }, + "DependsOn": [ + "testFailServiceRole9FF22F85" + ] + } + }, + "Parameters": { + "AssetParametersf38244b9028d22d4e265a5b466bdba928d93b5a4ac2b4bbf583309b3f027f044S3Bucket7A0FC067": { + "Type": "String", + "Description": "S3 bucket for asset \"f38244b9028d22d4e265a5b466bdba928d93b5a4ac2b4bbf583309b3f027f044\"" + }, + "AssetParametersf38244b9028d22d4e265a5b466bdba928d93b5a4ac2b4bbf583309b3f027f044S3VersionKeyCF9AAB81": { + "Type": "String", + "Description": "S3 key for asset version \"f38244b9028d22d4e265a5b466bdba928d93b5a4ac2b4bbf583309b3f027f044\"" + }, + "AssetParametersf38244b9028d22d4e265a5b466bdba928d93b5a4ac2b4bbf583309b3f027f044ArtifactHash2150188D": { + "Type": "String", + "Description": "Artifact hash for asset \"f38244b9028d22d4e265a5b466bdba928d93b5a4ac2b4bbf583309b3f027f044\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.graphql b/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.graphql new file mode 100644 index 0000000000000..bae8d70f18fde --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.graphql @@ -0,0 +1,19 @@ +type test @aws_iam { + id: String! + version: String! +} + +type Query { + getTest(id: String!): test + getTests: [ test! ] + @aws_iam +} + +input TestInput { + version: String! +} + +type Mutation { + addTest(input: TestInput!): test + @aws_iam +} diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.ts b/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.ts new file mode 100644 index 0000000000000..710975379030d --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.ts @@ -0,0 +1,110 @@ +import { join } from 'path'; +import { UserPool } from '@aws-cdk/aws-cognito'; +import { AttributeType, BillingMode, Table } from '@aws-cdk/aws-dynamodb'; +import { Role, ServicePrincipal } from '@aws-cdk/aws-iam'; +import { Code, Function, Runtime } from '@aws-cdk/aws-lambda'; +import { App, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { + AuthorizationType, + GraphQLApi, + MappingTemplate, + PrimaryKey, + UserPoolDefaultAction, + Values, + IamResource, +} from '../lib'; + +/* + * Creates an Appsync GraphQL API and Lambda with IAM Roles. + * Testing for IAM Auth and grantFullAccess. + * + * Stack verification steps: + * Install dependencies and deploy integration test. Invoke Lambda + * function with different permissions to test policies. + * + * -- bash verify.integ.graphql-iam.sh --start -- get dependencies/deploy -- + * -- aws lambda list-functions -- obtain testFail/testQuery -- + * -- aws lambda invoke /dev/stdout --function-name [FAIL] -- fails beacuse no IAM Role` -- + * -- aws lambda invoke /dev/stdout --function-name [Query]-- succeeds with empty get ` -- + * -- bash verify.integ.graphql-iam.sh --clean -- clean dependencies/deploy -- + */ + +const app = new App(); +const stack = new Stack(app, 'aws-appsync-integ'); +const userPool = new UserPool(stack, 'Pool', { + userPoolName: 'myPool', +}); + +const api = new GraphQLApi(stack, 'Api', { + name: 'Integ_Test_IAM', + schemaDefinitionFile: join(__dirname, 'integ.graphql-iam.graphql'), + authorizationConfig: { + defaultAuthorization: { + authorizationType: AuthorizationType.USER_POOL, + userPoolConfig: { + userPool, + defaultAction: UserPoolDefaultAction.ALLOW, + }, + }, + additionalAuthorizationModes: [ + { + authorizationType: AuthorizationType.IAM, + }, + ], + }, +}); + +const testTable = new Table(stack, 'TestTable', { + billingMode: BillingMode.PAY_PER_REQUEST, + partitionKey: { + name: 'id', + type: AttributeType.STRING, + }, + removalPolicy: RemovalPolicy.DESTROY, +}); + +const testDS = api.addDynamoDbDataSource('testDataSource', 'Table for Tests"', testTable); + +testDS.createResolver({ + typeName: 'Query', + fieldName: 'getTest', + requestMappingTemplate: MappingTemplate.dynamoDbGetItem('id', 'id'), + responseMappingTemplate: MappingTemplate.dynamoDbResultItem(), +}); + +testDS.createResolver({ + typeName: 'Query', + fieldName: 'getTests', + requestMappingTemplate: MappingTemplate.dynamoDbScanTable(), + responseMappingTemplate: MappingTemplate.dynamoDbResultList(), +}); + +testDS.createResolver({ + typeName: 'Mutation', + fieldName: 'addTest', + requestMappingTemplate: MappingTemplate.dynamoDbPutItem(PrimaryKey.partition('id').auto(), Values.projecting('test')), + responseMappingTemplate: MappingTemplate.dynamoDbResultItem(), +}); + +const lambdaIAM = new Role(stack, 'LambdaIAM', {assumedBy: new ServicePrincipal('lambda')}); + + +api.grant(lambdaIAM, IamResource.custom('types/Query/fields/getTests'), 'appsync:graphql'); +api.grant(lambdaIAM, IamResource.ofType('test'), 'appsync:GraphQL'); +api.grantMutation(lambdaIAM, 'addTest'); + +new Function(stack, 'testQuery', { + code: Code.fromAsset('verify'), + handler: 'iam-query.handler', + runtime: Runtime.NODEJS_12_X, + environment: {APPSYNC_ENDPOINT: api.graphQlUrl }, + role: lambdaIAM, +}); +new Function(stack, 'testFail', { + code: Code.fromAsset('verify'), + handler: 'iam-query.handler', + runtime: Runtime.NODEJS_12_X, + environment: {APPSYNC_ENDPOINT: api.graphQlUrl }, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-appsync/test/schema.graphql b/packages/@aws-cdk/aws-appsync/test/integ.graphql.graphql similarity index 100% rename from packages/@aws-cdk/aws-appsync/test/schema.graphql rename to packages/@aws-cdk/aws-appsync/test/integ.graphql.graphql diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts b/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts index 5ced7472fcb05..7b3444cda5842 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts @@ -1,7 +1,7 @@ +import { join } from 'path'; import { UserPool } from '@aws-cdk/aws-cognito'; import { AttributeType, BillingMode, Table } from '@aws-cdk/aws-dynamodb'; import { App, RemovalPolicy, Stack } from '@aws-cdk/core'; -import { join } from 'path'; import { AuthorizationType, GraphQLApi, @@ -35,7 +35,7 @@ const userPool = new UserPool(stack, 'Pool', { const api = new GraphQLApi(stack, 'Api', { name: 'demoapi', - schemaDefinitionFile: join(__dirname, 'schema.graphql'), + schemaDefinitionFile: join(__dirname, 'integ.graphql.graphql'), authorizationConfig: { defaultAuthorization: { authorizationType: AuthorizationType.USER_POOL, diff --git a/packages/@aws-cdk/aws-appsync/test/verify.integ.graphql-iam.sh b/packages/@aws-cdk/aws-appsync/test/verify.integ.graphql-iam.sh new file mode 100644 index 0000000000000..a5cc34a9071ad --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/verify.integ.graphql-iam.sh @@ -0,0 +1,18 @@ +#!bin/bash + +function error { + printf "\e[91;5;81m$@\e[0m\n" +} + +if [[ "$1" == "--start" ]]; then + mkdir -p ./verify/node_modules + npm install --prefix ./verify graphql-tag isomorphic-fetch aws-appsync + cdk deploy --app 'node integ.graphql-iam.js' +elif [[ "$1" == "--clean" ]]; then + rm -rf ./verify/node_modules + rm -f ./verify/package-lock.json + cdk destroy --app 'node integ.graphql-iam.js' +else + error "Error: run with --start or --clean flag" + exit 1 +fi \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/verify.integ.graphql.sh b/packages/@aws-cdk/aws-appsync/test/verify.integ.graphql.sh index f92fab3c176e6..71d285befcbc6 100644 --- a/packages/@aws-cdk/aws-appsync/test/verify.integ.graphql.sh +++ b/packages/@aws-cdk/aws-appsync/test/verify.integ.graphql.sh @@ -1,7 +1,7 @@ #!/bin/bash echo "---" echo "Before mutation (query for customers)" -curl -XPOST -H "Content-Type:application/graphql" -H "x-api-key:da2-kegodibyrzfbhiubdnjfmvy52u" -d '{ "query": "query { getCustomers { id name } }" }' https://ws3az3nucjcjjbm4o6ykdqltge.appsync-api.us-east-1.amazonaws.com/graphql +curl -XPOST -H "Content-Type:application/graphql" -H "x-api-key:$1" -d '{ "query": "query { getCustomers { id name } }" }' $2 echo "" echo "---" echo "Sending mutation (add customer)" @@ -9,5 +9,5 @@ curl -XPOST -H "Content-Type:application/graphql" -H "x-api-key:$1" -d '{ "query echo "" echo "---" echo "After mutation (query for customer)" -curl -XPOST -H "Content-Type:application/graphql" -H "x-api-key:da2-kegodibyrzfbhiubdnjfmvy52u" -d '{ "query": "query { getCustomers { id name } }" }' https://ws3az3nucjcjjbm4o6ykdqltge.appsync-api.us-east-1.amazonaws.com/graphql +curl -XPOST -H "Content-Type:application/graphql" -H "x-api-key:$1" -d '{ "query": "query { getCustomers { id name } }" }' $2 echo "" \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/verify/iam-query.js b/packages/@aws-cdk/aws-appsync/test/verify/iam-query.js new file mode 100644 index 0000000000000..11e895d2e2c54 --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/verify/iam-query.js @@ -0,0 +1,40 @@ +require('isomorphic-fetch'); +const AWS = require('aws-sdk/global'); +const gql = require('graphql-tag'); +const appsync = require('aws-appsync'); + +const config = { + url: process.env.APPSYNC_ENDPOINT, + region: process.env.AWS_REGION, + auth: { + type: appsync.AUTH_TYPE.AWS_IAM, + credentials: AWS.config.credentials, + }, + disableOffline: true +}; + +const getTests = +`query getTests { + getTests { + id + version + } +}`; + +const client = new appsync.AWSAppSyncClient(config); + +exports.handler = (event, context, callback) => { + + (async () => { + try { + const result = await client.query({ + query: gql(getTests) + }); + console.log(result.data); + callback(null, result.data); + } catch (e) { + console.warn('Error sending mutation: ', e); + callback(Error(e)); + } + })(); +}; \ No newline at end of file From d3e553348d93a0a8aa1617391772e4883e6c52c1 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Fri, 24 Jul 2020 01:55:44 -0700 Subject: [PATCH 18/30] feat(cloudfront): support Lambda@Edge for behaviors (#9220) This change adds support for support for defining function associations when adding both default and additional behaviors to the Distribution. Fixes #9108 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-cloudfront/README.md | 61 ++++++++++++ .../aws-cloudfront/lib/distribution.ts | 52 ++++++++++ .../lib/private/cache-behavior.ts | 12 ++- .../aws-cloudfront/lib/web_distribution.ts | 23 +---- packages/@aws-cdk/aws-cloudfront/package.json | 1 - .../aws-cloudfront/test/distribution.test.ts | 98 ++++++++++++++++++- 6 files changed, 222 insertions(+), 25 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront/README.md b/packages/@aws-cdk/aws-cloudfront/README.md index 69bf6f7cf9250..e0a38d797088d 100644 --- a/packages/@aws-cdk/aws-cloudfront/README.md +++ b/packages/@aws-cdk/aws-cloudfront/README.md @@ -161,6 +161,67 @@ new cloudfront.Distribution(this, 'myDist', { }); ``` +### Lambda@Edge + +Lambda@Edge is an extension of AWS Lambda, a compute service that lets you execute functions that customize the content that CloudFront delivers. +You can author Node.js or Python functions in the US East (N. Virginia) region, +and then execute them in AWS locations globally that are closer to the viewer, +without provisioning or managing servers. +Lambda@Edge functions are associated with a specific behavior and event type. +Lambda@Edge can be used rewrite URLs, +alter responses based on headers or cookies, +or authorize requests based on headers or authorization tokens. + +The following shows a Lambda@Edge function added to the default behavior and triggered on every request: + +```typescript +const myFunc = new lambda.Function(...); +new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { + origin: new origins.S3Origin(myBucket), + edgeLambdas: [ + { + functionVersion: myFunc.currentVersion, + eventType: cloudfront.LambdaEdgeEventType.VIEWER_REQUEST, + } + ], + }, +}); +``` + +Lambda@Edge functions can also be associated with additional behaviors, +either at Distribution creation time, +or after. + +```typescript +// assigning at Distribution creation +const myOrigin = new origins.S3Origin(myBucket); +new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { origin: myOrigin }, + additionalBehaviors: { + 'images/*': { + origin: myOrigin, + edgeLambdas: [ + { + functionVersion: myFunc.currentVersion, + eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, + }, + ], + }, + }, +}); + +// assigning after creation +myDistribution.addBehavior('images/*', myOrigin, { + edgeLambdas: [ + { + functionVersion: myFunc.currentVersion, + eventType: cloudfront.LambdaEdgeEventType.VIEWER_RESPONSE, + }, + ], +}); +``` + ## CloudFrontWebDistribution API - Stable ![cdk-constructs: Stable](https://img.shields.io/badge/cdk--constructs-stable-success.svg?style=for-the-badge) diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index 81d84515a4736..e32693aa79888 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -1,4 +1,5 @@ import * as acm from '@aws-cdk/aws-certificatemanager'; +import * as lambda from '@aws-cdk/aws-lambda'; import { Construct, IResource, Lazy, Resource, Stack, Token, Duration } from '@aws-cdk/core'; import { CfnDistribution } from './cloudfront.generated'; import { Origin } from './origin'; @@ -351,6 +352,49 @@ export interface ErrorResponse { readonly responsePagePath?: string; } +/** + * The type of events that a Lambda@Edge function can be invoked in response to. + */ +export enum LambdaEdgeEventType { + /** + * The origin-request specifies the request to the + * origin location (e.g. S3) + */ + ORIGIN_REQUEST = 'origin-request', + + /** + * The origin-response specifies the response from the + * origin location (e.g. S3) + */ + ORIGIN_RESPONSE = 'origin-response', + + /** + * The viewer-request specifies the incoming request + */ + VIEWER_REQUEST = 'viewer-request', + + /** + * The viewer-response specifies the outgoing reponse + */ + VIEWER_RESPONSE = 'viewer-response', +} + +/** + * Represents a Lambda function version and event type when using Lambda@Edge. + * The type of the {@link AddBehaviorOptions.edgeLambdas} property. + */ +export interface EdgeLambda { + /** + * The version of the Lambda function that will be invoked. + * + * **Note**: it's not possible to use the '$LATEST' function version for Lambda@Edge! + */ + readonly functionVersion: lambda.IVersion; + + /** The type of event in response to which should the function be invoked. */ + readonly eventType: LambdaEdgeEventType; +} + /** * Options for adding a new behavior to a Distribution. * @@ -380,6 +424,14 @@ export interface AddBehaviorOptions { * @default [] */ readonly forwardQueryStringCacheKeys?: string[]; + + /** + * The Lambda@Edge functions to invoke before serving the contents. + * + * @default - no Lambda functions will be invoked + * @see https://aws.amazon.com/lambda/edge + */ + readonly edgeLambdas?: EdgeLambda[]; } /** diff --git a/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts b/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts index 59f0226a92a31..363590333e777 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts @@ -49,7 +49,17 @@ export class CacheBehavior { queryStringCacheKeys: this.props.forwardQueryStringCacheKeys, }, viewerProtocolPolicy: ViewerProtocolPolicy.ALLOW_ALL, + lambdaFunctionAssociations: this.props.edgeLambdas + ? this.props.edgeLambdas.map(edgeLambda => { + if (edgeLambda.functionVersion.version === '$LATEST') { + throw new Error('$LATEST function version cannot be used for Lambda@Edge'); + } + return { + lambdaFunctionArn: edgeLambda.functionVersion.functionArn, + eventType: edgeLambda.eventType.toString(), + }; + }) + : undefined, }; } - } diff --git a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts index 450201170f98e..21392c625aad6 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts @@ -4,7 +4,7 @@ import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; import { CfnDistribution } from './cloudfront.generated'; -import { IDistribution, OriginProtocolPolicy, PriceClass, ViewerProtocolPolicy, SSLMethod, SecurityPolicyProtocol } from './distribution'; +import { IDistribution, LambdaEdgeEventType, OriginProtocolPolicy, PriceClass, ViewerProtocolPolicy, SSLMethod, SecurityPolicyProtocol } from './distribution'; import { IOriginAccessIdentity } from './origin_access_identity'; export enum HttpVersion { @@ -430,27 +430,6 @@ export interface LambdaFunctionAssociation { readonly lambdaFunction: lambda.IVersion; } -export enum LambdaEdgeEventType { - /** - * The origin-request specifies the request to the - * origin location (e.g. S3) - */ - ORIGIN_REQUEST = 'origin-request', - /** - * The origin-response specifies the response from the - * origin location (e.g. S3) - */ - ORIGIN_RESPONSE = 'origin-response', - /** - * The viewer-request specifies the incoming request - */ - VIEWER_REQUEST = 'viewer-request', - /** - * The viewer-response specifies the outgoing reponse - */ - VIEWER_RESPONSE = 'viewer-response', -} - export interface ViewerCertificateOptions { /** * How CloudFront should serve HTTPS requests. diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index 1e12b8d85ce67..45b22ae133343 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -129,7 +129,6 @@ "docs-public-apis:@aws-cdk/aws-cloudfront.HttpVersion", "docs-public-apis:@aws-cdk/aws-cloudfront.HttpVersion.HTTP1_1", "docs-public-apis:@aws-cdk/aws-cloudfront.HttpVersion.HTTP2", - "docs-public-apis:@aws-cdk/aws-cloudfront.LambdaEdgeEventType", "docs-public-apis:@aws-cdk/aws-cloudfront.OriginSslPolicy", "docs-public-apis:@aws-cdk/aws-cloudfront.OriginSslPolicy.SSL_V3", "docs-public-apis:@aws-cdk/aws-cloudfront.OriginSslPolicy.TLS_V1", diff --git a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts index 4a8ea27d3fbec..3db81d0b7ae1c 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts @@ -1,8 +1,9 @@ import '@aws-cdk/assert/jest'; import * as acm from '@aws-cdk/aws-certificatemanager'; +import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; import { App, Duration, Stack } from '@aws-cdk/core'; -import { Distribution, Origin, PriceClass, S3Origin } from '../lib'; +import { Distribution, LambdaEdgeEventType, Origin, PriceClass, S3Origin } from '../lib'; let app: App; let stack: Stack; @@ -288,6 +289,101 @@ describe('custom error responses', () => { }); +describe('with Lambda@Edge functions', () => { + let lambdaFunction: lambda.Function; + let origin: Origin; + + beforeEach(() => { + lambdaFunction = new lambda.Function(stack, 'Function', { + runtime: lambda.Runtime.NODEJS, + code: lambda.Code.fromInline('whatever'), + handler: 'index.handler', + }); + + origin = defaultS3Origin(); + }); + + test('can add an edge lambdas to the default behavior', () => { + new Distribution(stack, 'MyDist', { + defaultBehavior: { + origin, + edgeLambdas: [ + { + functionVersion: lambdaFunction.currentVersion, + eventType: LambdaEdgeEventType.ORIGIN_REQUEST, + }, + ], + }, + }); + + expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + DistributionConfig: { + DefaultCacheBehavior: { + LambdaFunctionAssociations: [ + { + EventType: 'origin-request', + LambdaFunctionARN: { + Ref: 'FunctionCurrentVersion4E2B22619c0305f954e58f25575548280c0a3629', + }, + }, + ], + }, + }, + }); + }); + + test('can add an edge lambdas to additional behaviors', () => { + new Distribution(stack, 'MyDist', { + defaultBehavior: { origin }, + additionalBehaviors: { + 'images/*': { + origin, + edgeLambdas: [ + { + functionVersion: lambdaFunction.currentVersion, + eventType: LambdaEdgeEventType.VIEWER_REQUEST, + }, + ], + }, + }, + }); + + expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + DistributionConfig: { + CacheBehaviors: [ + { + PathPattern: 'images/*', + LambdaFunctionAssociations: [ + { + EventType: 'viewer-request', + LambdaFunctionARN: { + Ref: 'FunctionCurrentVersion4E2B22619c0305f954e58f25575548280c0a3629', + }, + }, + ], + }, + ], + }, + }); + }); + + test('fails creation when attempting to add the $LATEST function version as an edge Lambda to the default behavior', () => { + expect(() => { + new Distribution(stack, 'MyDist', { + defaultBehavior: { + origin, + edgeLambdas: [ + { + functionVersion: lambdaFunction.latestVersion, + eventType: LambdaEdgeEventType.ORIGIN_RESPONSE, + }, + ], + }, + }); + }).toThrow(/\$LATEST function version cannot be used for Lambda@Edge/); + }); +}); + test('price class is included if provided', () => { const origin = defaultS3Origin(); new Distribution(stack, 'Dist', { From dac9bb312f5b0a9c83d929c862e30b49f3b8654a Mon Sep 17 00:00:00 2001 From: Bryan Pan Date: Fri, 24 Jul 2020 10:08:38 -0700 Subject: [PATCH 19/30] fix(appsync): resolver unable to set pipelineConfig (#9093) **[ISSUE]** `pipelineConfig` was be labeled as `undefined` while using the `Resolver` class instead of `createResolver`. Also, no way to set `kind` parameter for resolvers, so implemented that as well. **[APPROACH]** Created a property that takes `pipelineConfig` for `AppSync` functions. **[NOTE]** `pipelineConfig` takes a string array for the name of `AppSync Functions` not `Lambda Functions` Fixes #6923 BREAKING CHANGE: `pipelineConfig` is now an array of `string` instead of `CfnResolver.PipelineConfigProperty` for usability. - **appsync**: `pipelineConfig` parameter takes in `string []` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-appsync/lib/resolver.ts | 13 +++- .../@aws-cdk/aws-appsync/test/appsync.test.ts | 78 ++++++++++++++++++- 2 files changed, 84 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-appsync/lib/resolver.ts b/packages/@aws-cdk/aws-appsync/lib/resolver.ts index ef5e524ac75ae..5a226c448069a 100644 --- a/packages/@aws-cdk/aws-appsync/lib/resolver.ts +++ b/packages/@aws-cdk/aws-appsync/lib/resolver.ts @@ -1,8 +1,9 @@ -import { Construct, IResolvable } from '@aws-cdk/core'; +import { Construct } from '@aws-cdk/core'; import { CfnResolver } from './appsync.generated'; import { BaseDataSource } from './data-source'; import { GraphQLApi } from './graphqlapi'; import { MappingTemplate } from './mapping-template'; + /** * Basic properties for an AppSync resolver */ @@ -18,9 +19,10 @@ export interface BaseResolverProps { /** * configuration of the pipeline resolver * - * @default - create a UNIT resolver + * @default - no pipeline resolver configuration + * An empty array | undefined sets resolver to be of kind, unit */ - readonly pipelineConfig?: CfnResolver.PipelineConfigProperty | IResolvable; + readonly pipelineConfig?: string[]; /** * The request mapping template for this resolver * @@ -65,12 +67,15 @@ export class Resolver extends Construct { constructor(scope: Construct, id: string, props: ResolverProps) { super(scope, id); + const pipelineConfig = props.pipelineConfig && props.pipelineConfig.length ? { functions: props.pipelineConfig } : undefined; + this.resolver = new CfnResolver(this, 'Resource', { apiId: props.api.apiId, typeName: props.typeName, fieldName: props.fieldName, dataSourceName: props.dataSource ? props.dataSource.name : undefined, - kind: props.pipelineConfig ? 'PIPELINE' : 'UNIT', + kind: pipelineConfig ? 'PIPELINE' : 'UNIT', + pipelineConfig: pipelineConfig, requestMappingTemplate: props.requestMappingTemplate ? props.requestMappingTemplate.renderTemplate() : undefined, responseMappingTemplate: props.responseMappingTemplate ? props.responseMappingTemplate.renderTemplate() : undefined, }); diff --git a/packages/@aws-cdk/aws-appsync/test/appsync.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync.test.ts index 7a4a1fec1fab6..bc37922c85f82 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync.test.ts @@ -4,10 +4,10 @@ import * as cdk from '@aws-cdk/core'; import * as appsync from '../lib'; test('should not throw an Error', () => { - // Given + // GIVEN const stack = new cdk.Stack(); - // When + // WHEN const when = () => { new appsync.GraphQLApi(stack, 'api', { authorizationConfig: {}, @@ -16,6 +16,78 @@ test('should not throw an Error', () => { }); }; - // Then + // THEN expect(when).not.toThrow(); }); + +test('appsync should configure pipeline when pipelineConfig has contents', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const api = new appsync.GraphQLApi(stack, 'api', { + authorizationConfig: {}, + name: 'api', + schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + }); + + new appsync.Resolver(stack, 'resolver', { + api: api, + typeName: 'test', + fieldName: 'test2', + pipelineConfig: ['test', 'test'], + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::Resolver', { + Kind: 'PIPELINE', + PipelineConfig: { Functions: [ 'test', 'test' ] }, + }); +}); + +test('appsync should configure resolver as unit when pipelineConfig is empty', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const api = new appsync.GraphQLApi(stack, 'api', { + authorizationConfig: {}, + name: 'api', + schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + }); + + new appsync.Resolver(stack, 'resolver', { + api: api, + typeName: 'test', + fieldName: 'test2', + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::Resolver', { + Kind: 'UNIT', + }); +}); + +test('appsync should configure resolver as unit when pipelineConfig is empty array', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const api = new appsync.GraphQLApi(stack, 'api', { + authorizationConfig: {}, + name: 'api', + schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + }); + + new appsync.Resolver(stack, 'resolver', { + api: api, + typeName: 'test', + fieldName: 'test2', + pipelineConfig: [], + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::Resolver', { + Kind: 'UNIT', + }); +}); \ No newline at end of file From ae0cf2a3fa936771e66fa45f24af5efec52a3f21 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Sun, 26 Jul 2020 08:25:14 +0200 Subject: [PATCH 20/30] feat(lambda): official lambda build docker images (#9211) Use the official lambda build docker images instead of the ones provided by https://github.com/lambci/docker-lambda. BREAKING CHANGE: the `bundlingDockerImage` prop of a `Runtime` now points to the AWS SAM build image (`amazon/aws-sam-cli-build-image-`) instead of the LambCI build image (`lambci/lambda:build-`) Closes #9205 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-lambda-nodejs/README.md | 2 +- packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts | 2 +- packages/@aws-cdk/aws-lambda-nodejs/parcel/Dockerfile | 4 ++-- packages/@aws-cdk/aws-lambda-python/README.md | 2 +- packages/@aws-cdk/aws-lambda/README.md | 2 +- packages/@aws-cdk/aws-lambda/lib/runtime.ts | 6 +++--- packages/@aws-cdk/aws-lambda/test/test.runtime.ts | 9 +++++++++ 7 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/@aws-cdk/aws-lambda-nodejs/README.md b/packages/@aws-cdk/aws-lambda-nodejs/README.md index e82f79bdefce5..e6db75d0eb42d 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/README.md +++ b/packages/@aws-cdk/aws-lambda-nodejs/README.md @@ -102,4 +102,4 @@ new lambda.NodejsFunction(this, 'my-handler', { The modules listed in `nodeModules` must be present in the `package.json`'s dependencies. The same version will be used for installation. If a lock file is detected (`package-lock.json` or `yarn.lock`) it will be used along with the right installer (`npm` or `yarn`). The modules are -installed in a [Lambda compatible Docker container](https://github.com/lambci/docker-lambda). +installed in a [Lambda compatible Docker container](https://hub.docker.com/r/amazon/aws-sam-cli-build-image-nodejs12.x). diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts index 392726c75298d..b612709216d41 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts @@ -109,7 +109,7 @@ export class Bundling { throw new Error('Cannot find project root. Please specify it with `projectRoot`.'); } - // Bundling image derived from runtime bundling image (lambci) + // Bundling image derived from runtime bundling image (AWS SAM docker image) const image = cdk.BundlingDockerImage.fromAsset(path.join(__dirname, '../parcel'), { buildArgs: { ...options.buildArgs ?? {}, diff --git a/packages/@aws-cdk/aws-lambda-nodejs/parcel/Dockerfile b/packages/@aws-cdk/aws-lambda-nodejs/parcel/Dockerfile index e73b56d4fab04..f3c90a65be151 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/parcel/Dockerfile +++ b/packages/@aws-cdk/aws-lambda-nodejs/parcel/Dockerfile @@ -1,6 +1,6 @@ -# The correct lambci build image based on the runtime of the function will be +# 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=lambci/lambda:build-nodejs12.x +ARG IMAGE=amazon/aws-sam-cli-build-image-nodejs12.x FROM $IMAGE # Install yarn diff --git a/packages/@aws-cdk/aws-lambda-python/README.md b/packages/@aws-cdk/aws-lambda-python/README.md index 1e89a206f869a..dbfaea0e42222 100644 --- a/packages/@aws-cdk/aws-lambda-python/README.md +++ b/packages/@aws-cdk/aws-lambda-python/README.md @@ -30,7 +30,7 @@ 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 -all required modules in a [Lambda compatible Docker container](https://github.com/lambci/docker-lambda) +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`. ``` . diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index d9bd0466c3ed9..dcccaeececaf5 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -371,7 +371,7 @@ new lambda.Function(this, 'Function', { handler: 'index.handler', }); ``` -Runtimes expose a `bundlingDockerImage` property that points to the [lambci/lambda](https://hub.docker.com/r/lambci/lambda/) build image. +Runtimes expose a `bundlingDockerImage` property that points to the [AWS SAM](https://github.com/awslabs/aws-sam-cli) build image. Use `cdk.BundlingDockerImage.fromRegistry(image)` to use an existing image or `cdk.BundlingDockerImage.fromAsset(path)` to build a specific image: diff --git a/packages/@aws-cdk/aws-lambda/lib/runtime.ts b/packages/@aws-cdk/aws-lambda/lib/runtime.ts index 25f36b6a4f53f..207d8071d996e 100644 --- a/packages/@aws-cdk/aws-lambda/lib/runtime.ts +++ b/packages/@aws-cdk/aws-lambda/lib/runtime.ts @@ -158,9 +158,9 @@ export class Runtime { /** * The bundling Docker image for this runtime. - * Points to the lambci/lambda build image for this runtime. + * Points to the AWS SAM build image for this runtime. * - * @see https://hub.docker.com/r/lambci/lambda/ + * @see https://github.com/awslabs/aws-sam-cli */ public readonly bundlingDockerImage: BundlingDockerImage; @@ -168,7 +168,7 @@ export class Runtime { this.name = name; this.supportsInlineCode = !!props.supportsInlineCode; this.family = family; - this.bundlingDockerImage = BundlingDockerImage.fromRegistry(`lambci/lambda:build-${name}`); + this.bundlingDockerImage = BundlingDockerImage.fromRegistry(`amazon/aws-sam-cli-build-image-${name}`); Runtime.ALL.push(this); } diff --git a/packages/@aws-cdk/aws-lambda/test/test.runtime.ts b/packages/@aws-cdk/aws-lambda/test/test.runtime.ts index 2d92a96bdd218..df5cf230ce15c 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.runtime.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.runtime.ts @@ -64,6 +64,15 @@ export = testCase({ // THEN test.strictEqual(result, false, 'Runtimes should be unequal when supportsInlineCode changes'); + test.done(); + }, + 'bundlingDockerImage points to AWS SAM build image'(test: Test) { + // GIVEN + const runtime = new lambda.Runtime('my-runtime-name'); + + // THEN + test.equal(runtime.bundlingDockerImage.image, 'amazon/aws-sam-cli-build-image-my-runtime-name'); + test.done(); }, }); From 49d56eeb56dc0a97fb7472a04bc6fd3b48057593 Mon Sep 17 00:00:00 2001 From: Taylor Date: Mon, 27 Jul 2020 07:44:08 -0700 Subject: [PATCH 21/30] fix(route53-targets): CloudFrontTarget uses concrete resource class instead of the resource interface `IDistribution` (#9265) With the new Distribution construct, there is now more than a single construct that can be used as a target in CloudFrontTarget. Using the interface in the constructor allows both constructs to be targets. fixes: https://github.com/aws/aws-cdk/issues/9264 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-route53-targets/lib/cloudfront-target.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-route53-targets/lib/cloudfront-target.ts b/packages/@aws-cdk/aws-route53-targets/lib/cloudfront-target.ts index bb765702f5c87..bcb2b9f0085f2 100644 --- a/packages/@aws-cdk/aws-route53-targets/lib/cloudfront-target.ts +++ b/packages/@aws-cdk/aws-route53-targets/lib/cloudfront-target.ts @@ -11,7 +11,7 @@ export class CloudFrontTarget implements route53.IAliasRecordTarget { */ public static readonly CLOUDFRONT_ZONE_ID = 'Z2FDTNDATAQYW2'; - constructor(private readonly distribution: cloudfront.CloudFrontWebDistribution) { + constructor(private readonly distribution: cloudfront.IDistribution) { } public bind(_record: route53.IRecordSet): route53.AliasRecordTargetConfig { From eee86899f6836ceca608fb5a1f867d0062f4e5b9 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Mon, 27 Jul 2020 16:19:53 -0700 Subject: [PATCH 22/30] fix(rds): SQL Server instance engine uses incorrect major version (#9215) Apparently, even though SQL Server ParameterGroup family ends in '.0', the major version used in OptionGroup and Instance needs to end in '.00'. Fixes #9171 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-rds/lib/instance-engine.ts | 78 ++++++++++--------- .../test.sql-server.instance-engine.ts | 46 +++++++++++ 2 files changed, 88 insertions(+), 36 deletions(-) create mode 100644 packages/@aws-cdk/aws-rds/test/sql-server/test.sql-server.instance-engine.ts diff --git a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts index b2099933e24be..35ccb3fbea7f2 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts @@ -792,67 +792,67 @@ class OracleEeInstanceEngine extends OracleInstanceEngineBase { * and {@link DatabaseInstanceEngine.sqlServerEe}). */ export class SqlServerEngineVersion { - /** Version "11.0" (only a major version, without a specific minor version). */ - public static readonly VER_11 = SqlServerEngineVersion.of('11.0', '11.0'); + /** Version "11.00" (only a major version, without a specific minor version). */ + public static readonly VER_11 = SqlServerEngineVersion.of('11.00', '11.00'); /** Version "11.00.5058.0.v1". */ - public static readonly VER_11_00_5058_0_V1 = SqlServerEngineVersion.of('11.00.5058.0.v1', '11.0'); + public static readonly VER_11_00_5058_0_V1 = SqlServerEngineVersion.of('11.00.5058.0.v1', '11.00'); /** Version "11.00.6020.0.v1". */ - public static readonly VER_11_00_6020_0_V1 = SqlServerEngineVersion.of('11.00.6020.0.v1', '11.0'); + public static readonly VER_11_00_6020_0_V1 = SqlServerEngineVersion.of('11.00.6020.0.v1', '11.00'); /** Version "11.00.6594.0.v1". */ - public static readonly VER_11_00_6594_0_V1 = SqlServerEngineVersion.of('11.00.6594.0.v1', '11.0'); + public static readonly VER_11_00_6594_0_V1 = SqlServerEngineVersion.of('11.00.6594.0.v1', '11.00'); /** Version "11.00.7462.6.v1". */ - public static readonly VER_11_00_7462_6_V1 = SqlServerEngineVersion.of('11.00.7462.6.v1', '11.0'); + public static readonly VER_11_00_7462_6_V1 = SqlServerEngineVersion.of('11.00.7462.6.v1', '11.00'); /** Version "11.00.7493.4.v1". */ - public static readonly VER_11_00_7493_4_V1 = SqlServerEngineVersion.of('11.00.7493.4.v1', '11.0'); + public static readonly VER_11_00_7493_4_V1 = SqlServerEngineVersion.of('11.00.7493.4.v1', '11.00'); - /** Version "12.0" (only a major version, without a specific minor version). */ - public static readonly VER_12 = SqlServerEngineVersion.of('12.0', '12.0'); + /** Version "12.00" (only a major version, without a specific minor version). */ + public static readonly VER_12 = SqlServerEngineVersion.of('12.00', '12.00'); /** Version "12.00.5000.0.v1". */ - public static readonly VER_12_00_5000_0_V1 = SqlServerEngineVersion.of('12.00.5000.0.v1', '12.0'); + public static readonly VER_12_00_5000_0_V1 = SqlServerEngineVersion.of('12.00.5000.0.v1', '12.00'); /** Version "12.00.5546.0.v1". */ - public static readonly VER_12_00_5546_0_V1 = SqlServerEngineVersion.of('12.00.5546.0.v1', '12.0'); + public static readonly VER_12_00_5546_0_V1 = SqlServerEngineVersion.of('12.00.5546.0.v1', '12.00'); /** Version "12.00.5571.0.v1". */ - public static readonly VER_12_00_5571_0_V1 = SqlServerEngineVersion.of('12.00.5571.0.v1', '12.0'); + public static readonly VER_12_00_5571_0_V1 = SqlServerEngineVersion.of('12.00.5571.0.v1', '12.00'); /** Version "12.00.6293.0.v1". */ - public static readonly VER_12_00_6293_0_V1 = SqlServerEngineVersion.of('12.00.6293.0.v1', '12.0'); + public static readonly VER_12_00_6293_0_V1 = SqlServerEngineVersion.of('12.00.6293.0.v1', '12.00'); /** Version "12.00.6329.1.v1". */ - public static readonly VER_12_00_6329_1_V1 = SqlServerEngineVersion.of('12.00.6329.1.v1', '12.0'); + public static readonly VER_12_00_6329_1_V1 = SqlServerEngineVersion.of('12.00.6329.1.v1', '12.00'); - /** Version "13.0" (only a major version, without a specific minor version). */ - public static readonly VER_13 = SqlServerEngineVersion.of('13.0', '13.0'); + /** Version "13.00" (only a major version, without a specific minor version). */ + public static readonly VER_13 = SqlServerEngineVersion.of('13.00', '13.00'); /** Version "13.00.2164.0.v1". */ - public static readonly VER_13_00_2164_0_V1 = SqlServerEngineVersion.of('13.00.2164.0.v1', '13.0'); + public static readonly VER_13_00_2164_0_V1 = SqlServerEngineVersion.of('13.00.2164.0.v1', '13.00'); /** Version "13.00.4422.0.v1". */ - public static readonly VER_13_00_4422_0_V1 = SqlServerEngineVersion.of('13.00.4422.0.v1', '13.0'); + public static readonly VER_13_00_4422_0_V1 = SqlServerEngineVersion.of('13.00.4422.0.v1', '13.00'); /** Version "13.00.4451.0.v1". */ - public static readonly VER_13_00_4451_0_V1 = SqlServerEngineVersion.of('13.00.4451.0.v1', '13.0'); + public static readonly VER_13_00_4451_0_V1 = SqlServerEngineVersion.of('13.00.4451.0.v1', '13.00'); /** Version "13.00.4466.4.v1". */ - public static readonly VER_13_00_4466_4_V1 = SqlServerEngineVersion.of('13.00.4466.4.v1', '13.0'); + public static readonly VER_13_00_4466_4_V1 = SqlServerEngineVersion.of('13.00.4466.4.v1', '13.00'); /** Version "13.00.4522.0.v1". */ - public static readonly VER_13_00_4522_0_V1 = SqlServerEngineVersion.of('13.00.4522.0.v1', '13.0'); + public static readonly VER_13_00_4522_0_V1 = SqlServerEngineVersion.of('13.00.4522.0.v1', '13.00'); /** Version "13.00.5216.0.v1". */ - public static readonly VER_13_00_5216_0_V1 = SqlServerEngineVersion.of('13.00.5216.0.v1', '13.0'); + public static readonly VER_13_00_5216_0_V1 = SqlServerEngineVersion.of('13.00.5216.0.v1', '13.00'); /** Version "13.00.5292.0.v1". */ - public static readonly VER_13_00_5292_0_V1 = SqlServerEngineVersion.of('13.00.5292.0.v1', '13.0'); + public static readonly VER_13_00_5292_0_V1 = SqlServerEngineVersion.of('13.00.5292.0.v1', '13.00'); /** Version "13.00.5366.0.v1". */ - public static readonly VER_13_00_5366_0_V1 = SqlServerEngineVersion.of('13.00.5366.0.v1', '13.0'); + public static readonly VER_13_00_5366_0_V1 = SqlServerEngineVersion.of('13.00.5366.0.v1', '13.00'); /** Version "13.00.5426.0.v1". */ - public static readonly VER_13_00_5426_0_V1 = SqlServerEngineVersion.of('13.00.5426.0.v1', '13.0'); + public static readonly VER_13_00_5426_0_V1 = SqlServerEngineVersion.of('13.00.5426.0.v1', '13.00'); /** Version "13.00.5598.27.v1". */ - public static readonly VER_13_00_5598_27_V1 = SqlServerEngineVersion.of('13.00.5598.27.v1', '13.0'); + public static readonly VER_13_00_5598_27_V1 = SqlServerEngineVersion.of('13.00.5598.27.v1', '13.00'); - /** Version "14.0" (only a major version, without a specific minor version). */ - public static readonly VER_14 = SqlServerEngineVersion.of('14.0', '14.0'); + /** Version "14.00" (only a major version, without a specific minor version). */ + public static readonly VER_14 = SqlServerEngineVersion.of('14.00', '14.00'); /** Version "14.00.1000.169.v1". */ - public static readonly VER_14_00_1000_169_V1 = SqlServerEngineVersion.of('14.00.1000.169.v1', '14.0'); + public static readonly VER_14_00_1000_169_V1 = SqlServerEngineVersion.of('14.00.1000.169.v1', '14.00'); /** Version "14.00.3015.40.v1". */ - public static readonly VER_14_00_3015_40_V1 = SqlServerEngineVersion.of('14.00.3015.40.v1', '14.0'); + public static readonly VER_14_00_3015_40_V1 = SqlServerEngineVersion.of('14.00.3015.40.v1', '14.00'); /** Version "14.00.3035.2.v1". */ - public static readonly VER_14_00_3035_2_V1 = SqlServerEngineVersion.of('14.00.3035.2.v1', '14.0'); + public static readonly VER_14_00_3035_2_V1 = SqlServerEngineVersion.of('14.00.3035.2.v1', '14.00'); /** Version "14.00.3049.1.v1". */ - public static readonly VER_14_00_3049_1_V1 = SqlServerEngineVersion.of('14.00.3049.1.v1', '14.0'); + public static readonly VER_14_00_3049_1_V1 = SqlServerEngineVersion.of('14.00.3049.1.v1', '14.00'); /** Version "14.00.3192.2.v1". */ - public static readonly VER_14_00_3192_2_V1 = SqlServerEngineVersion.of('14.00.3192.2.v1', '14.0'); + public static readonly VER_14_00_3192_2_V1 = SqlServerEngineVersion.of('14.00.3192.2.v1', '14.00'); /** * Create a new SqlServerEngineVersion with an arbitrary version. @@ -860,7 +860,7 @@ export class SqlServerEngineVersion { * @param sqlServerFullVersion the full version string, * for example "15.00.3049.1.v1" * @param sqlServerMajorVersion the major version of the engine, - * for example "15.0" + * for example "15.00" */ public static of(sqlServerFullVersion: string, sqlServerMajorVersion: string): SqlServerEngineVersion { return new SqlServerEngineVersion(sqlServerFullVersion, sqlServerMajorVersion); @@ -868,7 +868,7 @@ export class SqlServerEngineVersion { /** The full version string, for example, "15.00.3049.1.v1". */ public readonly sqlServerFullVersion: string; - /** The major version of the engine, for example, "15.0". */ + /** The major version of the engine, for example, "15.00". */ public readonly sqlServerMajorVersion: string; private constructor(sqlServerFullVersion: string, sqlServerMajorVersion: string) { @@ -899,7 +899,13 @@ abstract class SqlServerInstanceEngineBase extends InstanceEngineBase { majorVersion: props.version.sqlServerMajorVersion, } : undefined, - parameterGroupFamily: props.version ? `${props.engineType}-${props.version.sqlServerMajorVersion}` : undefined, + parameterGroupFamily: props.version + // for some reason, even though SQL Server major versions usually end in '.00', + // the ParameterGroup family has to end in '.0' + ? `${props.engineType}-${props.version.sqlServerMajorVersion.endsWith('.00') + ? props.version.sqlServerMajorVersion.slice(0, -1) + : props.version.sqlServerMajorVersion}` + : undefined, }); } diff --git a/packages/@aws-cdk/aws-rds/test/sql-server/test.sql-server.instance-engine.ts b/packages/@aws-cdk/aws-rds/test/sql-server/test.sql-server.instance-engine.ts new file mode 100644 index 0000000000000..7f9ecf0311faa --- /dev/null +++ b/packages/@aws-cdk/aws-rds/test/sql-server/test.sql-server.instance-engine.ts @@ -0,0 +1,46 @@ +import { expect, haveResourceLike } from '@aws-cdk/assert'; +import * as core from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as rds from '../../lib'; + +export = { + 'SQL Server instance engine': { + "has ParameterGroup family ending in '11.0' for major version 11"(test: Test) { + const stack = new core.Stack(); + new rds.ParameterGroup(stack, 'ParameterGroup', { + engine: rds.DatabaseInstanceEngine.sqlServerWeb({ + version: rds.SqlServerEngineVersion.VER_11, + }), + }).bindToInstance({}); + + expect(stack).to(haveResourceLike('AWS::RDS::DBParameterGroup', { + Family: 'sqlserver-web-11.0', + })); + + test.done(); + }, + + "has MajorEngineVersion ending in '11.00' for major version 11"(test: Test) { + const stack = new core.Stack(); + new rds.OptionGroup(stack, 'OptionGroup', { + engine: rds.DatabaseInstanceEngine.sqlServerWeb({ + version: rds.SqlServerEngineVersion.VER_11, + }), + configurations: [ + { + name: 'SQLSERVER_BACKUP_RESTORE', + settings: { + IAM_ROLE_ARN: 'some-role-arn', + }, + }, + ], + }); + + expect(stack).to(haveResourceLike('AWS::RDS::OptionGroup', { + MajorEngineVersion: '11.00', + })); + + test.done(); + }, + }, +}; From ac612c6b70c01162761f6a51bdb25445da1cbf0d Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Mon, 27 Jul 2020 16:40:15 -0700 Subject: [PATCH 23/30] fix(codepipeline-actions): CodeDeployEcsDeployAction does not properly handle unnamed Artifacts (#9147) If the Artifacts passed to the CodeDeployEcsDeployAction were unnamed, the action incorrectly passed undefined to the configuration populated by them. Change to using Lazy values to fix this problem. Fixes #8971 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/codedeploy/ecs-deploy-action.ts | 10 ++++++---- .../test/codedeploy/test.ecs-deploy-action.ts | 20 +++++++++---------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codedeploy/ecs-deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codedeploy/ecs-deploy-action.ts index 9f0df0fbb0066..771a1b8e3e448 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codedeploy/ecs-deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codedeploy/ecs-deploy-action.ts @@ -1,7 +1,7 @@ import * as codedeploy from '@aws-cdk/aws-codedeploy'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as iam from '@aws-cdk/aws-iam'; -import { Construct } from '@aws-cdk/core'; +import { Construct, Lazy } from '@aws-cdk/core'; import { Action } from '../action'; /** @@ -177,17 +177,19 @@ export class CodeDeployEcsDeployAction extends Action { // the Action's Role needs to read from the Bucket to get artifacts options.bucket.grantRead(options.role); + const taskDefinitionTemplateArtifact = determineTaskDefinitionArtifact(this.actionProps); + const appSpecTemplateArtifact = determineAppSpecArtifact(this.actionProps); const actionConfig: codepipeline.ActionConfig = { configuration: { ApplicationName: this.actionProps.deploymentGroup.application.applicationName, DeploymentGroupName: this.actionProps.deploymentGroup.deploymentGroupName, - TaskDefinitionTemplateArtifact: determineTaskDefinitionArtifact(this.actionProps).artifactName, + TaskDefinitionTemplateArtifact: Lazy.stringValue({ produce: () => taskDefinitionTemplateArtifact.artifactName }), TaskDefinitionTemplatePath: this.actionProps.taskDefinitionTemplateFile ? this.actionProps.taskDefinitionTemplateFile.fileName : 'taskdef.json', - AppSpecTemplateArtifact: determineAppSpecArtifact(this.actionProps).artifactName, + AppSpecTemplateArtifact: Lazy.stringValue({ produce: () => appSpecTemplateArtifact.artifactName }), AppSpecTemplatePath: this.actionProps.appSpecTemplateFile ? this.actionProps.appSpecTemplateFile.fileName : 'appspec.yaml', @@ -197,7 +199,7 @@ export class CodeDeployEcsDeployAction extends Action { if (this.actionProps.containerImageInputs) { for (let i = 1; i <= this.actionProps.containerImageInputs.length; i++) { const imageInput = this.actionProps.containerImageInputs[i - 1]; - actionConfig.configuration[`Image${i}ArtifactName`] = imageInput.input.artifactName; + actionConfig.configuration[`Image${i}ArtifactName`] = Lazy.stringValue({ produce: () => imageInput.input.artifactName }); actionConfig.configuration[`Image${i}ContainerName`] = imageInput.taskDefinitionPlaceholder ? imageInput.taskDefinitionPlaceholder : 'IMAGE'; diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/codedeploy/test.ecs-deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/codedeploy/test.ecs-deploy-action.ts index 7e160c0fed364..b75809ad8f515 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/codedeploy/test.ecs-deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/codedeploy/test.ecs-deploy-action.ts @@ -146,13 +146,13 @@ export = { 'defaults task definition placeholder string'(test: Test) { const stack = new cdk.Stack(); const deploymentGroup = addEcsDeploymentGroup(stack); - const artifact1 = new codepipeline.Artifact('Artifact1'); - const artifact2 = new codepipeline.Artifact('Artifact2'); + const artifact1 = new codepipeline.Artifact(); + const artifact2 = new codepipeline.Artifact(); addCodeDeployECSCodePipeline(stack, { actionName: 'DeployToECS', deploymentGroup, - taskDefinitionTemplateFile: new codepipeline.ArtifactPath(artifact1, 'task-definition.json'), - appSpecTemplateFile: new codepipeline.ArtifactPath(artifact2, 'appspec-test.yaml'), + taskDefinitionTemplateFile: artifact1.atPath('task-definition.json'), + appSpecTemplateFile: artifact2.atPath('appspec-test.yaml'), containerImageInputs: [ { input: artifact1, @@ -172,21 +172,21 @@ export = { Configuration: { ApplicationName: 'MyApplication', DeploymentGroupName: 'MyDeploymentGroup', - TaskDefinitionTemplateArtifact: 'Artifact1', - AppSpecTemplateArtifact: 'Artifact2', + TaskDefinitionTemplateArtifact: 'Artifact_Source_GitHub', + AppSpecTemplateArtifact: 'Artifact_Source_GitHub2', TaskDefinitionTemplatePath: 'task-definition.json', AppSpecTemplatePath: 'appspec-test.yaml', - Image1ArtifactName: 'Artifact1', + Image1ArtifactName: 'Artifact_Source_GitHub', Image1ContainerName: 'IMAGE', - Image2ArtifactName: 'Artifact2', + Image2ArtifactName: 'Artifact_Source_GitHub2', Image2ContainerName: 'IMAGE', }, InputArtifacts: [ { - Name: 'Artifact1', + Name: 'Artifact_Source_GitHub', }, { - Name: 'Artifact2', + Name: 'Artifact_Source_GitHub2', }, ], }, From 19254dd0ea4014a52773fd0e44b2a68fbf704cca Mon Sep 17 00:00:00 2001 From: Neta Nir Date: Mon, 27 Jul 2020 19:40:03 -0700 Subject: [PATCH 24/30] chore: output junit files to coverage folder (#9280) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk/test/integ/cli/jest.config.js | 2 +- .../test/integ/test-cli-regression-against-current-code.sh | 2 +- tools/cdk-build-tools/config/jest.config.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/aws-cdk/test/integ/cli/jest.config.js b/packages/aws-cdk/test/integ/cli/jest.config.js index faed5273edbff..1e3fe3d13f96b 100644 --- a/packages/aws-cdk/test/integ/cli/jest.config.js +++ b/packages/aws-cdk/test/integ/cli/jest.config.js @@ -10,6 +10,6 @@ module.exports = { verbose: true, reporters: [ "default", - [ "jest-junit", { suiteName: "jest tests" } ] + [ "jest-junit", { suiteName: "jest tests", outputDirectory: "coverage" } ] ] }; diff --git a/packages/aws-cdk/test/integ/test-cli-regression-against-current-code.sh b/packages/aws-cdk/test/integ/test-cli-regression-against-current-code.sh index fbef55098288e..1a62b61d04b33 100755 --- a/packages/aws-cdk/test/integ/test-cli-regression-against-current-code.sh +++ b/packages/aws-cdk/test/integ/test-cli-regression-against-current-code.sh @@ -14,7 +14,7 @@ temp_dir=$(mktemp -d) function cleanup { # keep junit file to allow report creation - cp ${integ_under_test}/junit.xml . + cp ${integ_under_test}/coverage/junit.xml . rm -rf ${temp_dir} rm -rf ${integ_under_test} } diff --git a/tools/cdk-build-tools/config/jest.config.js b/tools/cdk-build-tools/config/jest.config.js index f01aaab769bb4..a71d55e8d0089 100644 --- a/tools/cdk-build-tools/config/jest.config.js +++ b/tools/cdk-build-tools/config/jest.config.js @@ -24,6 +24,6 @@ module.exports = { ], reporters: [ "default", - [ "jest-junit", { suiteName: "jest tests" } ] + [ "jest-junit", { suiteName: "jest tests", outputDirectory: "coverage" } ] ] }; From c80cf5670717fcab488202ba048ef4a271f1184e Mon Sep 17 00:00:00 2001 From: Neta Nir Date: Tue, 28 Jul 2020 01:27:41 -0700 Subject: [PATCH 25/30] chore: use current path (#9284) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../test/integ/test-cli-regression-against-current-code.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk/test/integ/test-cli-regression-against-current-code.sh b/packages/aws-cdk/test/integ/test-cli-regression-against-current-code.sh index 1a62b61d04b33..fbef55098288e 100755 --- a/packages/aws-cdk/test/integ/test-cli-regression-against-current-code.sh +++ b/packages/aws-cdk/test/integ/test-cli-regression-against-current-code.sh @@ -14,7 +14,7 @@ temp_dir=$(mktemp -d) function cleanup { # keep junit file to allow report creation - cp ${integ_under_test}/coverage/junit.xml . + cp ${integ_under_test}/junit.xml . rm -rf ${temp_dir} rm -rf ${integ_under_test} } From 1ac686384a84afae6c3254f787f2f23542b2a948 Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Tue, 28 Jul 2020 10:10:36 +0100 Subject: [PATCH 26/30] fix(pipelines): Reduce template size by combining IAM roles and policies (#9243) This change combines the two roles (and policies) per pipeline asset into a single role + policy for all assets in a pipeline. In a small pipeline with only 3 assets, this reduced the overall template size by a third. With a pipeline stack with 30 assets, this change reduces the template size from 363111 bytes to 190223 bytes. Tested on a cross-account pipeline to verify permissions were retained. fixes #9066 mitigates #9225 mitigates #9237 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/actions/publish-assets-action.ts | 9 + packages/@aws-cdk/pipelines/lib/pipeline.ts | 19 +- .../integ.pipeline-with-assets.expected.json | 1745 +++++++++++++++++ .../test/integ.pipeline-with-assets.ts | 90 + .../test/integ.pipeline.expected.json | 2 +- 5 files changed, 1861 insertions(+), 4 deletions(-) create mode 100644 packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.expected.json create mode 100644 packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.ts diff --git a/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts b/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts index 668d8f831b548..c9fb4a334c6f2 100644 --- a/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts +++ b/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts @@ -52,6 +52,13 @@ export interface PublishAssetsActionProps { * @default - Automatically generated */ readonly projectName?: string; + + /** + * Role to use for CodePipeline and CodeBuild to build and publish the assets. + * + * @default - Automatically generated + */ + readonly role?: iam.IRole; } /** @@ -87,6 +94,7 @@ export class PublishAssetsAction extends Construct implements codepipeline.IActi }), // Needed to perform Docker builds environment: props.assetType === AssetType.DOCKER_IMAGE ? { privileged: true } : undefined, + role: props.role, }); const rolePattern = props.assetType === AssetType.DOCKER_IMAGE @@ -102,6 +110,7 @@ export class PublishAssetsAction extends Construct implements codepipeline.IActi actionName: props.actionName, project, input: this.props.cloudAssemblyInput, + role: props.role, }); } diff --git a/packages/@aws-cdk/pipelines/lib/pipeline.ts b/packages/@aws-cdk/pipelines/lib/pipeline.ts index e496bfd1e5eee..6dd7af9e472ed 100644 --- a/packages/@aws-cdk/pipelines/lib/pipeline.ts +++ b/packages/@aws-cdk/pipelines/lib/pipeline.ts @@ -1,6 +1,7 @@ -import * as codepipeline from '@aws-cdk/aws-codepipeline'; -import { App, CfnOutput, Construct, Stack, Stage } from '@aws-cdk/core'; import * as path from 'path'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as iam from '@aws-cdk/aws-iam'; +import { App, CfnOutput, Construct, PhysicalName, Stack, Stage } from '@aws-cdk/core'; import { AssetType, DeployCdkStackAction, PublishAssetsAction, UpdatePipelineAction } from './actions'; import { appOf, assemblyBuilderOf } from './private/construct-internals'; import { AddStageOptions, AssetPublishingCommand, CdkStage, StackOutput } from './stage'; @@ -244,6 +245,7 @@ class AssetPublishing extends Construct { private readonly myCxAsmRoot: string; private readonly stage: codepipeline.IStage; + private assetRole?: iam.Role; private _fileAssetCtr = 1; private _dockerAssetCtr = 1; @@ -267,6 +269,17 @@ class AssetPublishing extends Construct { // FIXME: this is silly, we need the relative path here but no easy way to get it const relativePath = path.relative(this.myCxAsmRoot, command.assetManifestPath); + // This role is used by both the CodePipeline build action and related CodeBuild project. Consolidating these two + // roles into one, and re-using across all assets, saves significant size of the final synthesized output. + // Modeled after the CodePipeline role and 'CodePipelineActionRole' roles. + // Late-binding here to prevent creating the role in cases where no asset actions are created. + if (!this.assetRole) { + this.assetRole = new iam.Role(this, 'Role', { + roleName: PhysicalName.GENERATE_IF_NEEDED, + assumedBy: new iam.CompositePrincipal(new iam.ServicePrincipal('codebuild.amazonaws.com'), new iam.AccountPrincipal(Stack.of(this).account)), + }); + } + let action = this.publishers[command.assetId]; if (!action) { // The asset ID would be a logical candidate for the construct path and project names, but if the asset @@ -275,7 +288,6 @@ class AssetPublishing extends Construct { // // FIXME: The ultimate best solution is probably to generate a single Project per asset type // and reuse that for all assets. - const id = command.assetType === AssetType.FILE ? `FileAsset${this._fileAssetCtr++}` : `DockerAsset${this._dockerAssetCtr++}`; // NOTE: It's important that asset changes don't force a pipeline self-mutation. @@ -286,6 +298,7 @@ class AssetPublishing extends Construct { cloudAssemblyInput: this.props.cloudAssemblyInput, cdkCliVersion: this.props.cdkCliVersion, assetType: command.assetType, + role: this.assetRole, }); this.stage.addAction(action); } diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.expected.json b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.expected.json new file mode 100644 index 0000000000000..7795321cf451a --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.expected.json @@ -0,0 +1,1745 @@ +{ + "Resources": { + "PipelineUpdatePipelineSelfMutationRole57E559E8": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineUpdatePipelineSelfMutationRoleDefaultPolicyA225DA4E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" + }, + "-*" + ] + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": [ + "arn:*:iam::*:role/*-deploy-role-*", + "arn:*:iam::*:role/*-publishing-role-*" + ] + }, + { + "Action": "cloudformation:DescribeStacks", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "s3:ListBucket", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKeyF5BF0670", + "Arn" + ] + } + }, + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKeyF5BF0670", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineUpdatePipelineSelfMutationRoleDefaultPolicyA225DA4E", + "Roles": [ + { + "Ref": "PipelineUpdatePipelineSelfMutationRole57E559E8" + } + ] + } + }, + "PipelineUpdatePipelineSelfMutationDAA41400": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:1.0", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "PipelineUpdatePipelineSelfMutationRole57E559E8", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": \"npm install -g aws-cdk\"\n },\n \"build\": {\n \"commands\": [\n \"cdk -a . deploy PipelineStack --require-approval=never --verbose\"\n ]\n }\n }\n}", + "Type": "CODEPIPELINE" + }, + "EncryptionKey": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKeyF5BF0670", + "Arn" + ] + } + } + }, + "PipelineArtifactsBucketEncryptionKeyF5BF0670": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "PipelineRoleB27FAA37", + "Arn" + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "PipelineBuildSynthCdkBuildProjectRole231EEA2A", + "Arn" + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "PipelineBuildSynthCdkBuildProjectRole231EEA2A", + "Arn" + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "PipelineUpdatePipelineSelfMutationRole57E559E8", + "Arn" + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "PipelineUpdatePipelineSelfMutationRole57E559E8", + "Arn" + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "PipelineAssetsRole9B011B83", + "Arn" + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "PipelineAssetsRole9B011B83", + "Arn" + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "PipelinePreProdUseSourceProjectRole69B20A71", + "Arn" + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "PipelinePreProdUseSourceProjectRole69B20A71", + "Arn" + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Sub": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}" + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "PipelineArtifactsBucketAEA9A052": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "KMSMasterKeyID": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKeyF5BF0670", + "Arn" + ] + }, + "SSEAlgorithm": "aws:kms" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "PipelineArtifactsBucketEncryptionKeyAlias94A07392": { + "Type": "AWS::KMS::Alias", + "Properties": { + "AliasName": "alias/codepipeline-pipelinestackpipelinee95eedaa", + "TargetKeyId": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKeyF5BF0670", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "PipelineRoleB27FAA37": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleDefaultPolicy7BDC1ABB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKeyF5BF0670", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineBuildSynthCodePipelineActionRole4E7A6C97", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleD6D4E5CF", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineAssetsRole9B011B83", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelinePreProdUseSourceCodePipelineActionRoleA2043BDA", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::Sub": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineRoleDefaultPolicy7BDC1ABB", + "Roles": [ + { + "Ref": "PipelineRoleB27FAA37" + } + ] + } + }, + "Pipeline9850B417": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "PipelineRoleB27FAA37", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "ThirdParty", + "Provider": "GitHub", + "Version": "1" + }, + "Configuration": { + "Owner": "OWNER", + "Repo": "REPO", + "Branch": "master", + "OAuthToken": "not-a-secret", + "PollForSourceChanges": true + }, + "Name": "GitHub", + "OutputArtifacts": [ + { + "Name": "Artifact_Source_GitHub" + } + ], + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + } + }, + "InputArtifacts": [ + { + "Name": "Artifact_Source_GitHub" + } + ], + "Name": "Synth", + "OutputArtifacts": [ + { + "Name": "CloudAsm" + }, + { + "Name": "IntegTests" + } + ], + "RoleArn": { + "Fn::GetAtt": [ + "PipelineBuildSynthCodePipelineActionRole4E7A6C97", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "Build" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" + } + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "SelfMutate", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleD6D4E5CF", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "UpdatePipeline" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "PipelineAssetsFileAsset185A67CB4" + } + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "FileAsset1", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineAssetsRole9B011B83", + "Arn" + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "PipelineAssetsFileAsset24D2D639B" + } + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "FileAsset2", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineAssetsRole9B011B83", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "Assets" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "PipelinePreProdUseSourceProject2E711EB4" + } + }, + "InputArtifacts": [ + { + "Name": "Artifact_Source_GitHub" + } + ], + "Name": "UseSource", + "RoleArn": { + "Fn::GetAtt": [ + "PipelinePreProdUseSourceCodePipelineActionRoleA2043BDA", + "Arn" + ] + }, + "RunOrder": 100 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "PreProd-Stack", + "Capabilities": "CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND", + "RoleArn": { + "Fn::Sub": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}" + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "CloudAsm::assembly-PipelineStack-PreProd/PipelineStackPreProdStack65A0AD1F.template.json" + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "Stack.Prepare", + "RoleArn": { + "Fn::Sub": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}" + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "PreProd-Stack", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Stack.Deploy", + "RoleArn": { + "Fn::Sub": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}" + }, + "RunOrder": 2 + } + ], + "Name": "PreProd" + } + ], + "ArtifactStore": { + "EncryptionKey": { + "Id": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKeyF5BF0670", + "Arn" + ] + }, + "Type": "KMS" + }, + "Location": { + "Ref": "PipelineArtifactsBucketAEA9A052" + }, + "Type": "S3" + }, + "RestartExecutionOnUpdate": true + }, + "DependsOn": [ + "PipelineRoleDefaultPolicy7BDC1ABB", + "PipelineRoleB27FAA37" + ] + }, + "PipelineBuildSynthCodePipelineActionRole4E7A6C97": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineBuildSynthCodePipelineActionRoleDefaultPolicy92C90290": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineBuildSynthCdkBuildProject6BEFA8E6", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineBuildSynthCodePipelineActionRoleDefaultPolicy92C90290", + "Roles": [ + { + "Ref": "PipelineBuildSynthCodePipelineActionRole4E7A6C97" + } + ] + } + }, + "PipelineBuildSynthCdkBuildProjectRole231EEA2A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineBuildSynthCdkBuildProjectRoleDefaultPolicyFB6C941C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + }, + "-*" + ] + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKeyF5BF0670", + "Arn" + ] + } + }, + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKeyF5BF0670", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineBuildSynthCdkBuildProjectRoleDefaultPolicyFB6C941C", + "Roles": [ + { + "Ref": "PipelineBuildSynthCdkBuildProjectRole231EEA2A" + } + ] + } + }, + "PipelineBuildSynthCdkBuildProject6BEFA8E6": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:1.0", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "PipelineBuildSynthCdkBuildProjectRole231EEA2A", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"pre_build\": {\n \"commands\": [\n \"npm ci\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"npx cdk synth\"\n ]\n }\n },\n \"artifacts\": {\n \"secondary-artifacts\": {\n \"CloudAsm\": {\n \"base-directory\": \"cdk-integ.out\",\n \"files\": \"**/*\"\n },\n \"IntegTests\": {\n \"base-directory\": \"test\",\n \"files\": \"**/*\"\n }\n }\n }\n}", + "Type": "CODEPIPELINE" + }, + "EncryptionKey": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKeyF5BF0670", + "Arn" + ] + }, + "Name": "MyServicePipeline-synth" + } + }, + "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleD6D4E5CF": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleDefaultPolicyE626265B": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineUpdatePipelineSelfMutationDAA41400", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleDefaultPolicyE626265B", + "Roles": [ + { + "Ref": "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleD6D4E5CF" + } + ] + } + }, + "PipelinePreProdUseSourceCodePipelineActionRoleA2043BDA": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelinePreProdUseSourceCodePipelineActionRoleDefaultPolicy9BE325AD": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelinePreProdUseSourceProject2E711EB4", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelinePreProdUseSourceCodePipelineActionRoleDefaultPolicy9BE325AD", + "Roles": [ + { + "Ref": "PipelinePreProdUseSourceCodePipelineActionRoleA2043BDA" + } + ] + } + }, + "PipelinePreProdUseSourceProjectRole69B20A71": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelinePreProdUseSourceProjectRoleDefaultPolicy50F68DF3": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelinePreProdUseSourceProject2E711EB4" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelinePreProdUseSourceProject2E711EB4" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "PipelinePreProdUseSourceProject2E711EB4" + }, + "-*" + ] + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKeyF5BF0670", + "Arn" + ] + } + }, + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKeyF5BF0670", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelinePreProdUseSourceProjectRoleDefaultPolicy50F68DF3", + "Roles": [ + { + "Ref": "PipelinePreProdUseSourceProjectRole69B20A71" + } + ] + } + }, + "PipelinePreProdUseSourceProject2E711EB4": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:1.0", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "PipelinePreProdUseSourceProjectRole69B20A71", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"set -eu\",\n \"cat README.md\"\n ]\n }\n }\n}", + "Type": "CODEPIPELINE" + }, + "EncryptionKey": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKeyF5BF0670", + "Arn" + ] + } + } + }, + "PipelineAssetsRole9B011B83": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com", + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineAssetsRoleDefaultPolicyB41726CA": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineAssetsFileAsset185A67CB4" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineAssetsFileAsset185A67CB4" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "PipelineAssetsFileAsset185A67CB4" + }, + "-*" + ] + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": "arn:*:iam::*:role/*-file-publishing-role-*" + }, + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineAssetsFileAsset185A67CB4", + "Arn" + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKeyF5BF0670", + "Arn" + ] + } + }, + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKeyF5BF0670", + "Arn" + ] + } + }, + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineAssetsFileAsset24D2D639B" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineAssetsFileAsset24D2D639B" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "PipelineAssetsFileAsset24D2D639B" + }, + "-*" + ] + ] + } + }, + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineAssetsFileAsset24D2D639B", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineAssetsRoleDefaultPolicyB41726CA", + "Roles": [ + { + "Ref": "PipelineAssetsRole9B011B83" + } + ] + } + }, + "PipelineAssetsFileAsset185A67CB4": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:1.0", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "PipelineAssetsRole9B011B83", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": \"npm install -g cdk-assets\"\n },\n \"build\": {\n \"commands\": [\n \"cdk-assets --path \\\"assembly-PipelineStack-PreProd/PipelineStackPreProdStack65A0AD1F.assets.json\\\" --verbose publish \\\"8289faf53c7da377bb2b90615999171adef5e1d8f6b88810e5fef75e6ca09ba5:current_account-current_region\\\"\"\n ]\n }\n }\n}", + "Type": "CODEPIPELINE" + }, + "EncryptionKey": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKeyF5BF0670", + "Arn" + ] + } + } + }, + "PipelineAssetsFileAsset24D2D639B": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:1.0", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "PipelineAssetsRole9B011B83", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": \"npm install -g cdk-assets\"\n },\n \"build\": {\n \"commands\": [\n \"cdk-assets --path \\\"assembly-PipelineStack-PreProd/PipelineStackPreProdStack65A0AD1F.assets.json\\\" --verbose publish \\\"ac76997971c3f6ddf37120660003f1ced72b4fc58c498dfd99c78fa77e721e0e:current_account-current_region\\\"\"\n ]\n }\n }\n}", + "Type": "CODEPIPELINE" + }, + "EncryptionKey": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKeyF5BF0670", + "Arn" + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.ts b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.ts new file mode 100644 index 0000000000000..ce3698f5aba24 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.ts @@ -0,0 +1,90 @@ +/// !cdk-integ PipelineStack +import * as path from 'path'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; +import * as s3_assets from '@aws-cdk/aws-s3-assets'; +import { App, CfnResource, Construct, SecretValue, Stack, StackProps, Stage, StageProps } from '@aws-cdk/core'; +import * as cdkp from '../lib'; + +class MyStage extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + const stack = new Stack(this, 'Stack'); + + new s3_assets.Asset(stack, 'Asset', { + path: path.join(__dirname, 'test-file-asset.txt'), + }); + new s3_assets.Asset(stack, 'Asset2', { + path: path.join(__dirname, 'test-file-asset-two.txt'), + }); + + new CfnResource(stack, 'Resource', { + type: 'AWS::Test::SomeResource', + }); + } +} + +/** + * The stack that defines the application pipeline + */ +class CdkpipelinesDemoPipelineStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const sourceArtifact = new codepipeline.Artifact(); + const cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); + const integTestArtifact = new codepipeline.Artifact('IntegTests'); + + const pipeline = new cdkp.CdkPipeline(this, 'Pipeline', { + cloudAssemblyArtifact, + + // Where the source can be found + sourceAction: new codepipeline_actions.GitHubSourceAction({ + actionName: 'GitHub', + output: sourceArtifact, + oauthToken: SecretValue.plainText('not-a-secret'), + owner: 'OWNER', + repo: 'REPO', + trigger: codepipeline_actions.GitHubTrigger.POLL, + }), + + // How it will be built + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + projectName: 'MyServicePipeline-synth', + additionalArtifacts: [ + { + directory: 'test', + artifact: integTestArtifact, + }, + ], + }), + }); + + // This is where we add the application stages + // ... + const stage = pipeline.addApplicationStage(new MyStage(this, 'PreProd')); + stage.addActions( + new cdkp.ShellScriptAction({ + actionName: 'UseSource', + commands: [ + // Comes from source + 'cat README.md', + ], + additionalArtifacts: [sourceArtifact], + }), + ); + } +} + +const app = new App({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': 'true', + }, +}); +new CdkpipelinesDemoPipelineStack(app, 'PipelineStack', { + env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, +}); +app.synth(); diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline.expected.json b/packages/@aws-cdk/pipelines/test/integ.pipeline.expected.json index b30983fa38619..bd0840aeee7ba 100644 --- a/packages/@aws-cdk/pipelines/test/integ.pipeline.expected.json +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline.expected.json @@ -1313,4 +1313,4 @@ } } } -} +} \ No newline at end of file From 32b4bf01c9bb5725c62b57c46c097a66dcf81027 Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Tue, 28 Jul 2020 13:41:17 +0100 Subject: [PATCH 27/30] chore(certificatemanager): convert tests to Jest (#9290) Converted all tests in the module to use Jest instead of nodeunit. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-certificatemanager/.gitignore | 3 +- .../aws-certificatemanager/.npmignore | 3 +- .../aws-certificatemanager/jest.config.js | 2 + .../aws-certificatemanager/package.json | 5 +- .../test/certificate.test.ts | 211 ++++++++++++++++ .../test/dns-validated-certificate.test.ts | 192 +++++++++++++++ .../test/test.certificate.ts | 233 ------------------ .../test/test.dns-validated-certificate.ts | 207 ---------------- .../aws-certificatemanager/test/test.util.ts | 108 -------- .../aws-certificatemanager/test/util.test.ts | 105 ++++++++ 10 files changed, 516 insertions(+), 553 deletions(-) create mode 100644 packages/@aws-cdk/aws-certificatemanager/jest.config.js create mode 100644 packages/@aws-cdk/aws-certificatemanager/test/certificate.test.ts create mode 100644 packages/@aws-cdk/aws-certificatemanager/test/dns-validated-certificate.test.ts delete mode 100644 packages/@aws-cdk/aws-certificatemanager/test/test.certificate.ts delete mode 100644 packages/@aws-cdk/aws-certificatemanager/test/test.dns-validated-certificate.ts delete mode 100644 packages/@aws-cdk/aws-certificatemanager/test/test.util.ts create mode 100644 packages/@aws-cdk/aws-certificatemanager/test/util.test.ts diff --git a/packages/@aws-cdk/aws-certificatemanager/.gitignore b/packages/@aws-cdk/aws-certificatemanager/.gitignore index 018c65919d67c..266c0684c6844 100644 --- a/packages/@aws-cdk/aws-certificatemanager/.gitignore +++ b/packages/@aws-cdk/aws-certificatemanager/.gitignore @@ -14,5 +14,6 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js -junit.xml \ No newline at end of file +junit.xml diff --git a/packages/@aws-cdk/aws-certificatemanager/.npmignore b/packages/@aws-cdk/aws-certificatemanager/.npmignore index 5b76353bc079b..0a336369a6bfb 100644 --- a/packages/@aws-cdk/aws-certificatemanager/.npmignore +++ b/packages/@aws-cdk/aws-certificatemanager/.npmignore @@ -25,4 +25,5 @@ tsconfig.json # exclude cdk artifacts **/cdk.out -junit.xml \ No newline at end of file +junit.xml +jest.config.js diff --git a/packages/@aws-cdk/aws-certificatemanager/jest.config.js b/packages/@aws-cdk/aws-certificatemanager/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-certificatemanager/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-certificatemanager/package.json b/packages/@aws-cdk/aws-certificatemanager/package.json index adb40b6ddbe4e..000d643df48ec 100644 --- a/packages/@aws-cdk/aws-certificatemanager/package.json +++ b/packages/@aws-cdk/aws-certificatemanager/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::CertificateManager" + "cloudformation": "AWS::CertificateManager", + "jest": true }, "keywords": [ "aws", @@ -63,10 +64,8 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", - "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-certificatemanager/test/certificate.test.ts b/packages/@aws-cdk/aws-certificatemanager/test/certificate.test.ts new file mode 100644 index 0000000000000..4b122b7eae754 --- /dev/null +++ b/packages/@aws-cdk/aws-certificatemanager/test/certificate.test.ts @@ -0,0 +1,211 @@ +import '@aws-cdk/assert/jest'; +import * as route53 from '@aws-cdk/aws-route53'; +import { Lazy, Stack } from '@aws-cdk/core'; +import { Certificate, CertificateValidation, ValidationMethod } from '../lib'; + +test('apex domain selection by default', () => { + const stack = new Stack(); + + new Certificate(stack, 'Certificate', { + domainName: 'test.example.com', + }); + + expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + DomainName: 'test.example.com', + DomainValidationOptions: [{ + DomainName: 'test.example.com', + ValidationDomain: 'example.com', + }], + }); +}); + +test('validation domain can be overridden', () => { + const stack = new Stack(); + + new Certificate(stack, 'Certificate', { + domainName: 'test.example.com', + validationDomains: { + 'test.example.com': 'test.example.com', + }, + }); + + expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + DomainValidationOptions: [{ + DomainName: 'test.example.com', + ValidationDomain: 'test.example.com', + }], + }); +}); + +test('export and import', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const c = Certificate.fromCertificateArn(stack, 'Imported', 'cert-arn'); + + // THEN + expect(c.certificateArn).toBe('cert-arn'); +}); + +test('can configure validation method', () => { + const stack = new Stack(); + + new Certificate(stack, 'Certificate', { + domainName: 'test.example.com', + validationMethod: ValidationMethod.DNS, + }); + + expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + DomainName: 'test.example.com', + ValidationMethod: 'DNS', + }); +}); + +test('needs validation domain supplied if domain contains a token', () => { + const stack = new Stack(); + + expect(() => { + const domainName = Lazy.stringValue({ produce: () => 'example.com' }); + new Certificate(stack, 'Certificate', { + domainName, + }); + }).toThrow(/'validationDomains' needs to be supplied/); +}); + +test('validationdomains can be given for a Token', () => { + const stack = new Stack(); + + const domainName = Lazy.stringValue({ produce: () => 'my.example.com' }); + new Certificate(stack, 'Certificate', { + domainName, + validationDomains: { + [domainName]: 'example.com', + }, + }); + + expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + DomainName: 'my.example.com', + DomainValidationOptions: [{ + DomainName: 'my.example.com', + ValidationDomain: 'example.com', + }], + }); +}); + +test('CertificateValidation.fromEmail', () => { + const stack = new Stack(); + + new Certificate(stack, 'Certificate', { + domainName: 'test.example.com', + subjectAlternativeNames: ['extra.example.com'], + validation: CertificateValidation.fromEmail({ + 'test.example.com': 'example.com', + }), + }); + + expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + DomainName: 'test.example.com', + SubjectAlternativeNames: ['extra.example.com'], + DomainValidationOptions: [ + { + DomainName: 'test.example.com', + ValidationDomain: 'example.com', + }, + { + DomainName: 'extra.example.com', + ValidationDomain: 'example.com', + }, + ], + ValidationMethod: 'EMAIL', + }); +}); + +test('CertificateValidation.fromDns', () => { + const stack = new Stack(); + + new Certificate(stack, 'Certificate', { + domainName: 'test.example.com', + subjectAlternativeNames: ['extra.example.com'], + validation: CertificateValidation.fromDns(), + }); + + expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + DomainName: 'test.example.com', + SubjectAlternativeNames: ['extra.example.com'], + ValidationMethod: 'DNS', + }); +}); + +test('CertificateValidation.fromDns with hosted zone', () => { + const stack = new Stack(); + + const exampleCom = new route53.HostedZone(stack, 'ExampleCom', { + zoneName: 'example.com', + }); + + new Certificate(stack, 'Certificate', { + domainName: 'test.example.com', + validation: CertificateValidation.fromDns(exampleCom), + }); + + expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + DomainName: 'test.example.com', + DomainValidationOptions: [ + { + DomainName: 'test.example.com', + HostedZoneId: { + Ref: 'ExampleCom20E1324B', + }, + }, + ], + ValidationMethod: 'DNS', + }); +}); + +test('CertificateValidation.fromDnsMultiZone', () => { + const stack = new Stack(); + + const exampleCom = new route53.HostedZone(stack, 'ExampleCom', { + zoneName: 'example.com', + }); + + const exampleNet = new route53.HostedZone(stack, 'ExampleNet', { + zoneName: 'example.com', + }); + + new Certificate(stack, 'Certificate', { + domainName: 'test.example.com', + subjectAlternativeNames: ['cool.example.com', 'test.example.net'], + validation: CertificateValidation.fromDnsMultiZone({ + 'test.example.com': exampleCom, + 'cool.example.com': exampleCom, + 'test.example.net': exampleNet, + }), + }); + + expect(stack).toHaveResource('AWS::CertificateManager::Certificate', { + DomainName: 'test.example.com', + DomainValidationOptions: [ + { + DomainName: 'test.example.com', + HostedZoneId: { + Ref: 'ExampleCom20E1324B', + }, + }, + { + DomainName: 'cool.example.com', + HostedZoneId: { + Ref: 'ExampleCom20E1324B', + }, + }, + { + DomainName: 'test.example.net', + HostedZoneId: { + Ref: 'ExampleNetF7CA40C9', + }, + }, + ], + ValidationMethod: 'DNS', + }); +}); diff --git a/packages/@aws-cdk/aws-certificatemanager/test/dns-validated-certificate.test.ts b/packages/@aws-cdk/aws-certificatemanager/test/dns-validated-certificate.test.ts new file mode 100644 index 0000000000000..1e283f558880e --- /dev/null +++ b/packages/@aws-cdk/aws-certificatemanager/test/dns-validated-certificate.test.ts @@ -0,0 +1,192 @@ +import '@aws-cdk/assert/jest'; +import { SynthUtils } from '@aws-cdk/assert'; +import * as iam from '@aws-cdk/aws-iam'; +import { HostedZone, PublicHostedZone } from '@aws-cdk/aws-route53'; +import { App, Stack, Token } from '@aws-cdk/core'; +import { DnsValidatedCertificate } from '../lib/dns-validated-certificate'; + +test('creates CloudFormation Custom Resource', () => { + const stack = new Stack(); + + const exampleDotComZone = new PublicHostedZone(stack, 'ExampleDotCom', { + zoneName: 'example.com', + }); + + new DnsValidatedCertificate(stack, 'Certificate', { + domainName: 'test.example.com', + hostedZone: exampleDotComZone, + }); + + expect(stack).toHaveResource('AWS::CloudFormation::CustomResource', { + DomainName: 'test.example.com', + ServiceToken: { + 'Fn::GetAtt': [ + 'CertificateCertificateRequestorFunction5E845413', + 'Arn', + ], + }, + HostedZoneId: { + Ref: 'ExampleDotCom4D1B83AA', + }, + }); + expect(stack).toHaveResource('AWS::Lambda::Function', { + Handler: 'index.certificateRequestHandler', + Runtime: 'nodejs10.x', + Timeout: 900, + }); + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyName: 'CertificateCertificateRequestorFunctionServiceRoleDefaultPolicy3C8845BC', + Roles: [ + { + Ref: 'CertificateCertificateRequestorFunctionServiceRoleC04C13DA', + }, + ], + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: [ + 'acm:RequestCertificate', + 'acm:DescribeCertificate', + 'acm:DeleteCertificate', + ], + Effect: 'Allow', + Resource: '*', + }, + { + Action: 'route53:GetChange', + Effect: 'Allow', + Resource: '*', + }, + { + Action: 'route53:changeResourceRecordSets', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':route53:::hostedzone/', + { Ref: 'ExampleDotCom4D1B83AA' }, + ], + ], + }, + }, + ], + }, + }); +}); + +test('adds validation error on domain mismatch', () => { + const stack = new Stack(); + + const helloDotComZone = new PublicHostedZone(stack, 'HelloDotCom', { + zoneName: 'hello.com', + }); + + new DnsValidatedCertificate(stack, 'Cert', { + domainName: 'example.com', + hostedZone: helloDotComZone, + }); + + expect(() => { + SynthUtils.synthesize(stack); + }).toThrow(/DNS zone hello.com is not authoritative for certificate domain name example.com/); +}); + +test('does not try to validate unresolved tokens', () => { + const stack = new Stack(); + + const helloDotComZone = new PublicHostedZone(stack, 'HelloDotCom', { + zoneName: Token.asString('hello.com'), + }); + + new DnsValidatedCertificate(stack, 'Cert', { + domainName: 'hello.com', + hostedZone: helloDotComZone, + }); + + SynthUtils.synthesize(stack); // does not throw +}); + +test('test root certificate', () => { + const stack = new Stack(); + + const exampleDotComZone = new PublicHostedZone(stack, 'ExampleDotCom', { + zoneName: 'example.com', + }); + + new DnsValidatedCertificate(stack, 'Cert', { + domainName: 'example.com', + hostedZone: exampleDotComZone, + }); + + expect(stack).toHaveResource('AWS::CloudFormation::CustomResource', { + ServiceToken: { + 'Fn::GetAtt': [ + 'CertCertificateRequestorFunction98FDF273', + 'Arn', + ], + }, + DomainName: 'example.com', + HostedZoneId: { + Ref: 'ExampleDotCom4D1B83AA', + }, + }); +}); + +test('works with imported zone', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'Stack', { + env: { account: '12345678', region: 'us-blue-5' }, + }); + const imported = HostedZone.fromLookup(stack, 'ExampleDotCom', { + domainName: 'mydomain.com', + }); + + // WHEN + new DnsValidatedCertificate(stack, 'Cert', { + domainName: 'mydomain.com', + hostedZone: imported, + route53Endpoint: 'https://api.route53.xxx.com', + }); + + // THEN + expect(stack).toHaveResource('AWS::CloudFormation::CustomResource', { + ServiceToken: { + 'Fn::GetAtt': [ + 'CertCertificateRequestorFunction98FDF273', + 'Arn', + ], + }, + DomainName: 'mydomain.com', + HostedZoneId: 'DUMMY', + Route53Endpoint: 'https://api.route53.xxx.com', + }); +}); + +test('works with imported role', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'Stack', { + env: { account: '12345678', region: 'us-blue-5' }, + }); + const helloDotComZone = new PublicHostedZone(stack, 'HelloDotCom', { + zoneName: 'hello.com', + }); + const role = iam.Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::account-id:role/role-name'); + + // WHEN + new DnsValidatedCertificate(stack, 'Cert', { + domainName: 'hello.com', + hostedZone: helloDotComZone, + customResourceRole: role, + }); + + // THEN + expect(stack).toHaveResource('AWS::Lambda::Function', { + Role: 'arn:aws:iam::account-id:role/role-name', + }); +}); diff --git a/packages/@aws-cdk/aws-certificatemanager/test/test.certificate.ts b/packages/@aws-cdk/aws-certificatemanager/test/test.certificate.ts deleted file mode 100644 index 3b0de2e1ba1f2..0000000000000 --- a/packages/@aws-cdk/aws-certificatemanager/test/test.certificate.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { expect, haveResource } from '@aws-cdk/assert'; -import * as route53 from '@aws-cdk/aws-route53'; -import { Lazy, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import { Certificate, CertificateValidation, ValidationMethod } from '../lib'; - -export = { - 'apex domain selection by default'(test: Test) { - const stack = new Stack(); - - new Certificate(stack, 'Certificate', { - domainName: 'test.example.com', - }); - - expect(stack).to(haveResource('AWS::CertificateManager::Certificate', { - DomainName: 'test.example.com', - DomainValidationOptions: [{ - DomainName: 'test.example.com', - ValidationDomain: 'example.com', - }], - })); - - test.done(); - }, - - 'validation domain can be overridden'(test: Test) { - const stack = new Stack(); - - new Certificate(stack, 'Certificate', { - domainName: 'test.example.com', - validationDomains: { - 'test.example.com': 'test.example.com', - }, - }); - - expect(stack).to(haveResource('AWS::CertificateManager::Certificate', { - DomainValidationOptions: [{ - DomainName: 'test.example.com', - ValidationDomain: 'test.example.com', - }], - })); - - test.done(); - }, - - 'export and import'(test: Test) { - // GIVEN - const stack = new Stack(); - - // WHEN - const c = Certificate.fromCertificateArn(stack, 'Imported', 'cert-arn'); - - // THEN - test.deepEqual(c.certificateArn, 'cert-arn'); - test.done(); - }, - - 'can configure validation method'(test: Test) { - const stack = new Stack(); - - new Certificate(stack, 'Certificate', { - domainName: 'test.example.com', - validationMethod: ValidationMethod.DNS, - }); - - expect(stack).to(haveResource('AWS::CertificateManager::Certificate', { - DomainName: 'test.example.com', - ValidationMethod: 'DNS', - })); - - test.done(); - }, - - 'needs validation domain supplied if domain contains a token'(test: Test) { - const stack = new Stack(); - - test.throws(() => { - const domainName = Lazy.stringValue({ produce: () => 'example.com' }); - new Certificate(stack, 'Certificate', { - domainName, - }); - }, /'validationDomains' needs to be supplied/); - - test.done(); - }, - - 'validationdomains can be given for a Token'(test: Test) { - const stack = new Stack(); - - const domainName = Lazy.stringValue({ produce: () => 'my.example.com' }); - new Certificate(stack, 'Certificate', { - domainName, - validationDomains: { - [domainName]: 'example.com', - }, - }); - - expect(stack).to(haveResource('AWS::CertificateManager::Certificate', { - DomainName: 'my.example.com', - DomainValidationOptions: [{ - DomainName: 'my.example.com', - ValidationDomain: 'example.com', - }], - })); - - test.done(); - }, - - 'CertificateValidation.fromEmail'(test: Test) { - const stack = new Stack(); - - new Certificate(stack, 'Certificate', { - domainName: 'test.example.com', - subjectAlternativeNames: ['extra.example.com'], - validation: CertificateValidation.fromEmail({ - 'test.example.com': 'example.com', - }), - }); - - expect(stack).to(haveResource('AWS::CertificateManager::Certificate', { - DomainName: 'test.example.com', - SubjectAlternativeNames: ['extra.example.com'], - DomainValidationOptions: [ - { - DomainName: 'test.example.com', - ValidationDomain: 'example.com', - }, - { - DomainName: 'extra.example.com', - ValidationDomain: 'example.com', - }, - ], - ValidationMethod: 'EMAIL', - })); - - test.done(); - }, - - 'CertificateValidation.fromDns'(test: Test) { - const stack = new Stack(); - - new Certificate(stack, 'Certificate', { - domainName: 'test.example.com', - subjectAlternativeNames: ['extra.example.com'], - validation: CertificateValidation.fromDns(), - }); - - expect(stack).to(haveResource('AWS::CertificateManager::Certificate', { - DomainName: 'test.example.com', - SubjectAlternativeNames: ['extra.example.com'], - ValidationMethod: 'DNS', - })); - - test.done(); - }, - - 'CertificateValidation.fromDns with hosted zone'(test: Test) { - const stack = new Stack(); - - const exampleCom = new route53.HostedZone(stack, 'ExampleCom', { - zoneName: 'example.com', - }); - - new Certificate(stack, 'Certificate', { - domainName: 'test.example.com', - validation: CertificateValidation.fromDns(exampleCom), - }); - - expect(stack).to(haveResource('AWS::CertificateManager::Certificate', { - DomainName: 'test.example.com', - DomainValidationOptions: [ - { - DomainName: 'test.example.com', - HostedZoneId: { - Ref: 'ExampleCom20E1324B', - }, - }, - ], - ValidationMethod: 'DNS', - })); - - test.done(); - }, - - 'CertificateValidation.fromDnsMultiZone'(test: Test) { - const stack = new Stack(); - - const exampleCom = new route53.HostedZone(stack, 'ExampleCom', { - zoneName: 'example.com', - }); - - const exampleNet = new route53.HostedZone(stack, 'ExampleNet', { - zoneName: 'example.com', - }); - - new Certificate(stack, 'Certificate', { - domainName: 'test.example.com', - subjectAlternativeNames: ['cool.example.com', 'test.example.net'], - validation: CertificateValidation.fromDnsMultiZone({ - 'test.example.com': exampleCom, - 'cool.example.com': exampleCom, - 'test.example.net': exampleNet, - }), - }); - - expect(stack).to(haveResource('AWS::CertificateManager::Certificate', { - DomainName: 'test.example.com', - DomainValidationOptions: [ - { - DomainName: 'test.example.com', - HostedZoneId: { - Ref: 'ExampleCom20E1324B', - }, - }, - { - DomainName: 'cool.example.com', - HostedZoneId: { - Ref: 'ExampleCom20E1324B', - }, - }, - { - DomainName: 'test.example.net', - HostedZoneId: { - Ref: 'ExampleNetF7CA40C9', - }, - }, - ], - ValidationMethod: 'DNS', - })); - - test.done(); - }, -}; diff --git a/packages/@aws-cdk/aws-certificatemanager/test/test.dns-validated-certificate.ts b/packages/@aws-cdk/aws-certificatemanager/test/test.dns-validated-certificate.ts deleted file mode 100644 index 6a718bb39d39d..0000000000000 --- a/packages/@aws-cdk/aws-certificatemanager/test/test.dns-validated-certificate.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { expect, haveResource, SynthUtils } from '@aws-cdk/assert'; -import * as iam from '@aws-cdk/aws-iam'; -import { HostedZone, PublicHostedZone } from '@aws-cdk/aws-route53'; -import { App, Stack, Token } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import { DnsValidatedCertificate } from '../lib/dns-validated-certificate'; - -export = { - 'creates CloudFormation Custom Resource'(test: Test) { - const stack = new Stack(); - - const exampleDotComZone = new PublicHostedZone(stack, 'ExampleDotCom', { - zoneName: 'example.com', - }); - - new DnsValidatedCertificate(stack, 'Certificate', { - domainName: 'test.example.com', - hostedZone: exampleDotComZone, - }); - - expect(stack).to(haveResource('AWS::CloudFormation::CustomResource', { - DomainName: 'test.example.com', - ServiceToken: { - 'Fn::GetAtt': [ - 'CertificateCertificateRequestorFunction5E845413', - 'Arn', - ], - }, - HostedZoneId: { - Ref: 'ExampleDotCom4D1B83AA', - }, - })); - expect(stack).to(haveResource('AWS::Lambda::Function', { - Handler: 'index.certificateRequestHandler', - Runtime: 'nodejs10.x', - Timeout: 900, - })); - expect(stack).to(haveResource('AWS::IAM::Policy', { - PolicyName: 'CertificateCertificateRequestorFunctionServiceRoleDefaultPolicy3C8845BC', - Roles: [ - { - Ref: 'CertificateCertificateRequestorFunctionServiceRoleC04C13DA', - }, - ], - PolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Action: [ - 'acm:RequestCertificate', - 'acm:DescribeCertificate', - 'acm:DeleteCertificate', - ], - Effect: 'Allow', - Resource: '*', - }, - { - Action: 'route53:GetChange', - Effect: 'Allow', - Resource: '*', - }, - { - Action: 'route53:changeResourceRecordSets', - Effect: 'Allow', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':route53:::hostedzone/', - { Ref: 'ExampleDotCom4D1B83AA' }, - ], - ], - }, - }, - ], - }, - })); - - test.done(); - }, - - 'adds validation error on domain mismatch'(test: Test) { - const stack = new Stack(); - - const helloDotComZone = new PublicHostedZone(stack, 'HelloDotCom', { - zoneName: 'hello.com', - }); - - new DnsValidatedCertificate(stack, 'Cert', { - domainName: 'example.com', - hostedZone: helloDotComZone, - }); - - test.throws(() => { - SynthUtils.synthesize(stack); - }, /DNS zone hello.com is not authoritative for certificate domain name example.com/); - - test.done(); - }, - - 'does not try to validate unresolved tokens'(test: Test) { - const stack = new Stack(); - - const helloDotComZone = new PublicHostedZone(stack, 'HelloDotCom', { - zoneName: Token.asString('hello.com'), - }); - - new DnsValidatedCertificate(stack, 'Cert', { - domainName: 'hello.com', - hostedZone: helloDotComZone, - }); - - test.doesNotThrow(() => { - SynthUtils.synthesize(stack); - }); - - test.done(); - }, - - 'test root certificate'(test: Test) { - const stack = new Stack(); - - const exampleDotComZone = new PublicHostedZone(stack, 'ExampleDotCom', { - zoneName: 'example.com', - }); - - new DnsValidatedCertificate(stack, 'Cert', { - domainName: 'example.com', - hostedZone: exampleDotComZone, - }); - - expect(stack).to(haveResource('AWS::CloudFormation::CustomResource', { - ServiceToken: { - 'Fn::GetAtt': [ - 'CertCertificateRequestorFunction98FDF273', - 'Arn', - ], - }, - DomainName: 'example.com', - HostedZoneId: { - Ref: 'ExampleDotCom4D1B83AA', - }, - })); - test.done(); - }, - - 'works with imported zone'(test: Test) { - // GIVEN - const app = new App(); - const stack = new Stack(app, 'Stack', { - env: { account: '12345678', region: 'us-blue-5' }, - }); - const imported = HostedZone.fromLookup(stack, 'ExampleDotCom', { - domainName: 'mydomain.com', - }); - - // WHEN - new DnsValidatedCertificate(stack, 'Cert', { - domainName: 'mydomain.com', - hostedZone: imported, - route53Endpoint: 'https://api.route53.xxx.com', - }); - - // THEN - expect(stack).to(haveResource('AWS::CloudFormation::CustomResource', { - ServiceToken: { - 'Fn::GetAtt': [ - 'CertCertificateRequestorFunction98FDF273', - 'Arn', - ], - }, - DomainName: 'mydomain.com', - HostedZoneId: 'DUMMY', - Route53Endpoint: 'https://api.route53.xxx.com', - })); - - test.done(); - }, - - 'works with imported role'(test: Test) { - // GIVEN - const app = new App(); - const stack = new Stack(app, 'Stack', { - env: { account: '12345678', region: 'us-blue-5' }, - }); - const helloDotComZone = new PublicHostedZone(stack, 'HelloDotCom', { - zoneName: 'hello.com', - }); - const role = iam.Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::account-id:role/role-name'); - - // WHEN - new DnsValidatedCertificate(stack, 'Cert', { - domainName: 'hello.com', - hostedZone: helloDotComZone, - customResourceRole: role, - }); - - // THEN - expect(stack).to(haveResource('AWS::Lambda::Function', { - Role: 'arn:aws:iam::account-id:role/role-name', - })); - - test.done(); - }, -}; diff --git a/packages/@aws-cdk/aws-certificatemanager/test/test.util.ts b/packages/@aws-cdk/aws-certificatemanager/test/test.util.ts deleted file mode 100644 index 9751d3e0b7f7e..0000000000000 --- a/packages/@aws-cdk/aws-certificatemanager/test/test.util.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { PublicHostedZone } from '@aws-cdk/aws-route53'; -import { App, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import { Certificate, DnsValidatedCertificate } from '../lib'; -import { apexDomain, getCertificateRegion, isDnsValidatedCertificate } from '../lib/util'; - -export = { - 'apex domain': { - 'returns right domain'(test: Test) { - test.equals('domain.com', apexDomain('domain.com')); - test.equals('domain.com', apexDomain('test.domain.com')); - test.done(); - }, - - 'understands eTLDs'(test: Test) { - test.equals('domain.co.uk', apexDomain('test.domain.co.uk')); - test.done(); - }, - }, - 'isDnsValidatedCertificate': { - 'new DnsValidatedCertificate is a DnsValidatedCertificate'(test: Test) { - const stack = new Stack(); - - const hostedZone = new PublicHostedZone(stack, 'ExampleDotCom', { - zoneName: 'example.com', - }); - const cert = new DnsValidatedCertificate(stack, 'Certificate', { - domainName: 'test.example.com', - hostedZone, - }); - - test.ok(isDnsValidatedCertificate(cert)); - test.done(); - }, - 'new Certificate is not a DnsValidatedCertificate'(test: Test) { - const stack = new Stack(); - - const cert = new Certificate(stack, 'Certificate', { - domainName: 'test.example.com', - }); - - test.ok(!isDnsValidatedCertificate(cert)); - test.done(); - }, - 'fromCertificateArn is not a DnsValidatedCertificate'(test: Test) { - const stack = new Stack(); - - const cert = Certificate.fromCertificateArn(stack, 'Certificate', 'cert-arn'); - - test.ok(!isDnsValidatedCertificate(cert)); - test.done(); - }, - }, - 'getCertificateRegion': { - 'from stack'(test: Test) { - // GIVEN - const app = new App(); - const stack = new Stack(app, 'RegionStack', {env: {region: 'eu-west-1'}}); - - const certificate = new Certificate(stack, 'TestCertificate', { - domainName: 'www.example.com', - }); - - test.equals(getCertificateRegion(certificate), 'eu-west-1'); - test.done(); - }, - 'from DnsValidatedCertificate region'(test: Test) { - // GIVEN - const app = new App(); - const stack = new Stack(app, 'RegionStack', {env: {region: 'eu-west-1'}}); - const hostedZone = new PublicHostedZone(stack, 'ExampleDotCom', { - zoneName: 'example.com', - }); - - const certificate = new DnsValidatedCertificate(stack, 'TestCertificate', { - domainName: 'www.example.com', - hostedZone, - region: 'eu-west-3', - }); - - test.equals(getCertificateRegion(certificate), 'eu-west-3'); - test.done(); - }, - 'fromCertificateArn'(test: Test) { - // GIVEN - const app = new App(); - const stack = new Stack(app, 'RegionStack', {env: {region: 'eu-west-1'}}); - - const certificate = Certificate.fromCertificateArn( - stack, 'TestCertificate', 'arn:aws:acm:us-east-2:1111111:certificate/11-3336f1-44483d-adc7-9cd375c5169d', - ); - - test.equals(getCertificateRegion(certificate), 'us-east-2'); - test.done(); - }, - 'region agnostic stack'(test: Test) { - // GIVEN - const stack = new Stack(); - - const certificate = new Certificate(stack, 'TestCertificate', { - domainName: 'www.example.com', - }); - - test.equals(getCertificateRegion(certificate), '${Token[AWS::Region.4]}'); - test.done(); - }, - }, -}; diff --git a/packages/@aws-cdk/aws-certificatemanager/test/util.test.ts b/packages/@aws-cdk/aws-certificatemanager/test/util.test.ts new file mode 100644 index 0000000000000..ff9018ad4ca4d --- /dev/null +++ b/packages/@aws-cdk/aws-certificatemanager/test/util.test.ts @@ -0,0 +1,105 @@ +import '@aws-cdk/assert/jest'; +import { PublicHostedZone } from '@aws-cdk/aws-route53'; +import { App, Stack } from '@aws-cdk/core'; +import { Certificate, DnsValidatedCertificate } from '../lib'; +import { apexDomain, getCertificateRegion, isDnsValidatedCertificate } from '../lib/util'; + +describe('apex domain', () => { + test('returns right domain', () => { + expect(apexDomain('domain.com')).toEqual('domain.com'); + expect(apexDomain('test.domain.com')).toEqual('domain.com'); + }); + + test('understands eTLDs', () => { + expect(apexDomain('test.domain.co.uk')).toEqual('domain.co.uk'); + }); +}); + +describe('isDnsValidatedCertificate', () => { + test('new DnsValidatedCertificate is a DnsValidatedCertificate', () => { + const stack = new Stack(); + + const hostedZone = new PublicHostedZone(stack, 'ExampleDotCom', { + zoneName: 'example.com', + }); + const cert = new DnsValidatedCertificate(stack, 'Certificate', { + domainName: 'test.example.com', + hostedZone, + }); + + expect(isDnsValidatedCertificate(cert)).toBeTruthy(); + }); + + test('new Certificate is not a DnsValidatedCertificate', () => { + const stack = new Stack(); + + const cert = new Certificate(stack, 'Certificate', { + domainName: 'test.example.com', + }); + + expect(isDnsValidatedCertificate(cert)).toBeFalsy(); + }); + + test('fromCertificateArn is not a DnsValidatedCertificate', () => { + const stack = new Stack(); + + const cert = Certificate.fromCertificateArn(stack, 'Certificate', 'cert-arn'); + + expect(isDnsValidatedCertificate(cert)).toBeFalsy(); + }); +}); + +describe('getCertificateRegion', () => { + test('from stack', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'RegionStack', {env: {region: 'eu-west-1'}}); + + const certificate = new Certificate(stack, 'TestCertificate', { + domainName: 'www.example.com', + }); + + expect(getCertificateRegion(certificate)).toEqual('eu-west-1'); + }); + + test('from DnsValidatedCertificate region', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'RegionStack', {env: {region: 'eu-west-1'}}); + const hostedZone = new PublicHostedZone(stack, 'ExampleDotCom', { + zoneName: 'example.com', + }); + + const certificate = new DnsValidatedCertificate(stack, 'TestCertificate', { + domainName: 'www.example.com', + hostedZone, + region: 'eu-west-3', + }); + + expect(getCertificateRegion(certificate)).toEqual('eu-west-3'); + }); + + test('fromCertificateArn', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'RegionStack', {env: {region: 'eu-west-1'}}); + + const certificate = Certificate.fromCertificateArn( + stack, 'TestCertificate', 'arn:aws:acm:us-east-2:1111111:certificate/11-3336f1-44483d-adc7-9cd375c5169d', + ); + + expect(getCertificateRegion(certificate)).toEqual('us-east-2'); + }); + + test('region agnostic stack', () => { + // GIVEN + const stack = new Stack(); + + const certificate = new Certificate(stack, 'TestCertificate', { + domainName: 'www.example.com', + }); + + expect(getCertificateRegion(certificate)).toEqual('${Token[AWS::Region.4]}'); + }); + +}); From 52a966a2fa6b72fefc73859a1253b36a235cd631 Mon Sep 17 00:00:00 2001 From: Florian Nagel Date: Tue, 28 Jul 2020 20:17:59 +0200 Subject: [PATCH 28/30] fix(route53-targets): Add China Partition Support for CloudFrontTarget (#9174) Unfortunately `CloudFrontTarget` doesn't support the China partition right now due the hard-coded CloudFront Hosted Zone Id. This PR introduces a `CfnMapping` for CloudFront support in multiple partitions. Notice that CloudFront is not yet supported in `AWS GovCloud` hence its absence in the `CfnMapping` mapping. Resolves https://github.com/aws/aws-cdk/issues/9238 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/cloudfront-target.ts | 28 +++++++- .../lib/userpool-domain.ts | 2 +- .../test/cloudfront-target.test.ts | 64 ++++++++++++++++++- ...nteg.cloudfront-alias-target.expected.json | 20 +++++- .../test/userpool-domain.test.ts | 19 ++++-- 5 files changed, 121 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/aws-route53-targets/lib/cloudfront-target.ts b/packages/@aws-cdk/aws-route53-targets/lib/cloudfront-target.ts index bcb2b9f0085f2..217ba72cfc079 100644 --- a/packages/@aws-cdk/aws-route53-targets/lib/cloudfront-target.ts +++ b/packages/@aws-cdk/aws-route53-targets/lib/cloudfront-target.ts @@ -1,5 +1,6 @@ import * as cloudfront from '@aws-cdk/aws-cloudfront'; import * as route53 from '@aws-cdk/aws-route53'; +import { Aws, CfnMapping, IConstruct, Stack } from '@aws-cdk/core'; /** * Use a CloudFront Distribution as an alias record target @@ -11,12 +12,37 @@ export class CloudFrontTarget implements route53.IAliasRecordTarget { */ public static readonly CLOUDFRONT_ZONE_ID = 'Z2FDTNDATAQYW2'; + /** + * Get the hosted zone id for the current scope. + * + * @param scope - scope in which this resource is defined + */ + public static getHostedZoneId(scope: IConstruct) { + const mappingName = 'AWSCloudFrontPartitionHostedZoneIdMap'; + const scopeStack = Stack.of(scope); + + let mapping = + (scopeStack.node.tryFindChild(mappingName) as CfnMapping) ?? + new CfnMapping(scopeStack, mappingName, { + mapping: { + ['aws']: { + zoneId: 'Z2FDTNDATAQYW2', // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-aliastarget.html + }, + ['aws-cn']: { + zoneId: 'Z3RFFRIM2A3IF5', // https://docs.amazonaws.cn/en_us/aws/latest/userguide/route53.html + }, + }, + }); + + return mapping.findInMap(Aws.PARTITION, 'zoneId'); + } + constructor(private readonly distribution: cloudfront.IDistribution) { } public bind(_record: route53.IRecordSet): route53.AliasRecordTargetConfig { return { - hostedZoneId: CloudFrontTarget.CLOUDFRONT_ZONE_ID, + hostedZoneId: CloudFrontTarget.getHostedZoneId(this.distribution), dnsName: this.distribution.domainName, }; } diff --git a/packages/@aws-cdk/aws-route53-targets/lib/userpool-domain.ts b/packages/@aws-cdk/aws-route53-targets/lib/userpool-domain.ts index f3b9f0a550ff1..20b04f4e64f3a 100644 --- a/packages/@aws-cdk/aws-route53-targets/lib/userpool-domain.ts +++ b/packages/@aws-cdk/aws-route53-targets/lib/userpool-domain.ts @@ -12,7 +12,7 @@ export class UserPoolDomainTarget implements IAliasRecordTarget { public bind(_record: IRecordSet): AliasRecordTargetConfig { return { dnsName: this.domain.cloudFrontDomainName, - hostedZoneId: CloudFrontTarget.CLOUDFRONT_ZONE_ID, + hostedZoneId: CloudFrontTarget.getHostedZoneId(this.domain), }; } } diff --git a/packages/@aws-cdk/aws-route53-targets/test/cloudfront-target.test.ts b/packages/@aws-cdk/aws-route53-targets/test/cloudfront-target.test.ts index 42d5084c8079e..8e8d51341d21f 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/cloudfront-target.test.ts +++ b/packages/@aws-cdk/aws-route53-targets/test/cloudfront-target.test.ts @@ -1,10 +1,60 @@ import '@aws-cdk/assert/jest'; +import { SynthUtils } from '@aws-cdk/assert'; import * as cloudfront from '@aws-cdk/aws-cloudfront'; import * as route53 from '@aws-cdk/aws-route53'; import * as s3 from '@aws-cdk/aws-s3'; -import { Stack } from '@aws-cdk/core'; +import { NestedStack, Stack } from '@aws-cdk/core'; import * as targets from '../lib'; +test('use CloudFrontTarget partition hosted zone id mapping', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + targets.CloudFrontTarget.getHostedZoneId(stack); + + // THEN + expect(SynthUtils.toCloudFormation(stack)).toEqual({ + Mappings: { + AWSCloudFrontPartitionHostedZoneIdMap: { + 'aws': { + zoneId: 'Z2FDTNDATAQYW2', + }, + 'aws-cn': { + zoneId: 'Z3RFFRIM2A3IF5', + }, + }, + }, + }); +}); + +test('use CloudFrontTarget hosted zone id mappings in nested stacks', () => { + // GIVEN + const stack = new Stack(); + const nestedStackA = new NestedStack(stack, 'nestedStackA'); + const nestedStackB = new NestedStack(stack, 'nestedStackB'); + + // WHEN + targets.CloudFrontTarget.getHostedZoneId(nestedStackA); + targets.CloudFrontTarget.getHostedZoneId(nestedStackB); + + // THEN + for (let nestedStack of [nestedStackA, nestedStackB]) { + expect(SynthUtils.toCloudFormation(nestedStack)).toEqual({ + Mappings: { + AWSCloudFrontPartitionHostedZoneIdMap: { + 'aws': { + zoneId: 'Z2FDTNDATAQYW2', + }, + 'aws-cn': { + zoneId: 'Z3RFFRIM2A3IF5', + }, + }, + }, + }); + } +}); + test('use CloudFront as record target', () => { // GIVEN const stack = new Stack(); @@ -33,8 +83,16 @@ test('use CloudFront as record target', () => { // THEN expect(stack).toHaveResource('AWS::Route53::RecordSet', { AliasTarget: { - DNSName: { 'Fn::GetAtt': [ 'MyDistributionCFDistributionDE147309', 'DomainName' ] }, - HostedZoneId: 'Z2FDTNDATAQYW2', + DNSName: { 'Fn::GetAtt': ['MyDistributionCFDistributionDE147309', 'DomainName'] }, + HostedZoneId: { + 'Fn::FindInMap': [ + 'AWSCloudFrontPartitionHostedZoneIdMap', + { + Ref: 'AWS::Partition', + }, + 'zoneId', + ], + }, }, }); }); diff --git a/packages/@aws-cdk/aws-route53-targets/test/integ.cloudfront-alias-target.expected.json b/packages/@aws-cdk/aws-route53-targets/test/integ.cloudfront-alias-target.expected.json index 7a91ba087aaab..b70f6a2274aee 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/integ.cloudfront-alias-target.expected.json +++ b/packages/@aws-cdk/aws-route53-targets/test/integ.cloudfront-alias-target.expected.json @@ -18,7 +18,15 @@ "DomainName" ] }, - "HostedZoneId": "Z2FDTNDATAQYW2" + "HostedZoneId": { + "Fn::FindInMap": [ + "AWSCloudFrontPartitionHostedZoneIdMap", + { + "Ref": "AWS::Partition" + }, + "zoneId" + ] + } }, "HostedZoneId": { "Ref": "HostedZoneDB99F866" @@ -78,5 +86,15 @@ } } } + }, + "Mappings": { + "AWSCloudFrontPartitionHostedZoneIdMap": { + "aws": { + "zoneId": "Z2FDTNDATAQYW2" + }, + "aws-cn": { + "zoneId": "Z3RFFRIM2A3IF5" + } + } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53-targets/test/userpool-domain.test.ts b/packages/@aws-cdk/aws-route53-targets/test/userpool-domain.test.ts index 9c8655a0e2e69..8b211eddd1cb5 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/userpool-domain.test.ts +++ b/packages/@aws-cdk/aws-route53-targets/test/userpool-domain.test.ts @@ -23,11 +23,18 @@ test('use user pool domain as record target', () => { // THEN expect(stack).toHaveResource('AWS::Route53::RecordSet', { AliasTarget: { - DNSName: { 'Fn::GetAtt': [ - 'UserPoolDomainCloudFrontDomainName0B254952', - 'DomainDescription.CloudFrontDistribution', - ] }, - HostedZoneId: 'Z2FDTNDATAQYW2', + DNSName: { + 'Fn::GetAtt': ['UserPoolDomainCloudFrontDomainName0B254952', 'DomainDescription.CloudFrontDistribution'], + }, + HostedZoneId: { + 'Fn::FindInMap': [ + 'AWSCloudFrontPartitionHostedZoneIdMap', + { + Ref: 'AWS::Partition', + }, + 'zoneId', + ], + }, }, }); -}); \ No newline at end of file +}); From b4339a19c3fbada5b9407871ccb59461102081fc Mon Sep 17 00:00:00 2001 From: Neta Nir Date: Tue, 28 Jul 2020 12:23:44 -0700 Subject: [PATCH 29/30] fix(cloudtrail): missing sns publish permissions (#9239) * fix(cloudtrail): missing sns publish permissions --- .../@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts | 8 +++- .../aws-cloudtrail/test/cloudtrail.test.ts | 26 +++++++++++++ .../test/integ.cloudtrail.lit.expected.json | 37 ++++++++++++++++++- .../test/integ.cloudtrail.lit.ts | 6 ++- 4 files changed, 74 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts index a802c32d79d9f..e41a50b1cb66d 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts @@ -199,6 +199,7 @@ export class Trail extends Resource { private s3bucket: s3.IBucket; private eventSelectors: EventSelector[] = []; + private topic: sns.ITopic | undefined; constructor(scope: Construct, id: string, props: TrailProps = {}) { super(scope, id, { @@ -226,6 +227,11 @@ export class Trail extends Resource { }, })); + this.topic = props.snsTopic; + if (this.topic) { + this.topic.grantPublish(cloudTrailPrincipal); + } + let logsRole: iam.IRole | undefined; if (props.sendToCloudWatchLogs) { @@ -276,7 +282,7 @@ export class Trail extends Resource { s3KeyPrefix: props.s3KeyPrefix, cloudWatchLogsLogGroupArn: this.logGroup?.logGroupArn, cloudWatchLogsRoleArn: logsRole?.roleArn, - snsTopicName: props.snsTopic?.topicName, + snsTopicName: this.topic?.topicName, eventSelectors: this.eventSelectors, }); diff --git a/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts b/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts index cf0766ad43032..8a6e107fdc231 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts @@ -5,6 +5,7 @@ import * as kms from '@aws-cdk/aws-kms'; import * as lambda from '@aws-cdk/aws-lambda'; import { LogGroup, RetentionDays } from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; +import * as sns from '@aws-cdk/aws-sns'; import { Stack } from '@aws-cdk/core'; import { ReadWriteType, Trail } from '../lib'; @@ -102,6 +103,31 @@ describe('cloudtrail', () => { expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); }); + test('with sns topic', () => { + const stack = getTestStack(); + const topic = new sns.Topic(stack, 'Topic'); + + + new Trail(stack, 'Trail', { snsTopic: topic}); + + expect(stack).toHaveResource('AWS::CloudTrail::Trail'); + expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); + expect(stack).toHaveResource('AWS::SNS::TopicPolicy', { + PolicyDocument: { + Statement: [ + { + Action: 'sns:Publish', + Effect: 'Allow', + Principal: { Service: 'cloudtrail.amazonaws.com' }, + Resource: { Ref: 'TopicBFC7AF6E' }, + Sid: '0', + }, + ], + Version: '2012-10-17', + }, + }); + }); + test('with imported s3 bucket', () => { // GIVEN const stack = getTestStack(); diff --git a/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.expected.json b/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.expected.json index 06cf3a82e0331..5b90761ade1ff 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.expected.json +++ b/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.expected.json @@ -5,6 +5,35 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "TopicBFC7AF6E": { + "Type": "AWS::SNS::Topic" + }, + "TopicPolicyA1747468": { + "Type": "AWS::SNS::TopicPolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Principal": { + "Service": "cloudtrail.amazonaws.com" + }, + "Resource": { + "Ref": "TopicBFC7AF6E" + }, + "Sid": "0" + } + ], + "Version": "2012-10-17" + }, + "Topics": [ + { + "Ref": "TopicBFC7AF6E" + } + ] + } + }, "LambdaFunctionServiceRoleC555A460": { "Type": "AWS::IAM::Role", "Properties": { @@ -165,7 +194,13 @@ } ], "IncludeGlobalServiceEvents": true, - "IsMultiRegionTrail": true + "IsMultiRegionTrail": true, + "SnsTopicName": { + "Fn::GetAtt": [ + "TopicBFC7AF6E", + "TopicName" + ] + } }, "DependsOn": [ "TrailS3PolicyE42170FE" diff --git a/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.ts b/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.ts index 5f53f4efeb0fa..2a90a981b54e3 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.ts @@ -1,5 +1,6 @@ import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; +import * as sns from '@aws-cdk/aws-sns'; import * as cdk from '@aws-cdk/core'; import * as cloudtrail from '../lib'; @@ -7,13 +8,16 @@ const app = new cdk.App(); const stack = new cdk.Stack(app, 'integ-cloudtrail'); const bucket = new s3.Bucket(stack, 'Bucket', { removalPolicy: cdk.RemovalPolicy.DESTROY }); +const topic = new sns.Topic(stack, 'Topic'); const lambdaFunction = new lambda.Function(stack, 'LambdaFunction', { runtime: lambda.Runtime.NODEJS_10_X, handler: 'hello.handler', code: lambda.Code.fromInline('exports.handler = {}'), }); -const trail = new cloudtrail.Trail(stack, 'Trail'); +const trail = new cloudtrail.Trail(stack, 'Trail', { + snsTopic: topic, +}); trail.addLambdaEventSelector([lambdaFunction]); trail.addS3EventSelector([{bucket}]); From 72b44e291355a46f764feab90de638dc592797f8 Mon Sep 17 00:00:00 2001 From: AWS CDK Team Date: Tue, 28 Jul 2020 20:29:05 +0000 Subject: [PATCH 30/30] chore(release): 1.55.0 --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ lerna.json | 2 +- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c50c9bd5454b7..c6a19b6edba20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,40 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.55.0](https://github.com/aws/aws-cdk/compare/v1.54.0...v1.55.0) (2020-07-28) + + +### ⚠ BREAKING CHANGES + +* **lambda:** the `bundlingDockerImage` prop of a `Runtime` now points to the AWS SAM build image (`amazon/aws-sam-cli-build-image-`) instead of the LambCI build image (`lambci/lambda:build-`) +* **appsync:** `pipelineConfig` is now an array of `string` instead of `CfnResolver.PipelineConfigProperty` for usability. +- **appsync**: `pipelineConfig` parameter takes in `string []` + +### Features + +* **appsync:** grant APIs for managing permissions ([#8993](https://github.com/aws/aws-cdk/issues/8993)) ([e6dca52](https://github.com/aws/aws-cdk/commit/e6dca529098e54c91f0706019b9ee06522ddb025)), closes [#6772](https://github.com/aws/aws-cdk/issues/6772) [#7871](https://github.com/aws/aws-cdk/issues/7871) [#7313](https://github.com/aws/aws-cdk/issues/7313) +* **aws-codepipeline:** experimental support for ServiceCatalog deploy action ([#9214](https://github.com/aws/aws-cdk/issues/9214)) ([950e51f](https://github.com/aws/aws-cdk/commit/950e51f1edab335a3fd323b6d51f7444738bb9dc)) +* **cfn-include:** handle resources not in the CloudFormation schema ([#9199](https://github.com/aws/aws-cdk/issues/9199)) ([d287525](https://github.com/aws/aws-cdk/commit/d28752513175c94fb2bc4da43374d7f2e66d6550)), closes [#9197](https://github.com/aws/aws-cdk/issues/9197) +* **cfnspec:** cloudformation spec v16.1.0 ([#9074](https://github.com/aws/aws-cdk/issues/9074)) ([d1ca04f](https://github.com/aws/aws-cdk/commit/d1ca04f7a136be437a0538d7606803bdf0a73f98)) +* **cfnspec:** cloudformation spec v16.1.0 ([#9216](https://github.com/aws/aws-cdk/issues/9216)) ([d4b68d3](https://github.com/aws/aws-cdk/commit/d4b68d3040a96451f2b708c512af5afa8fa33bb8)) +* **cloudfront:** new aws-cloudfront-origins module, support for ALB/NLB origins ([#9209](https://github.com/aws/aws-cdk/issues/9209)) ([27ee332](https://github.com/aws/aws-cdk/commit/27ee332cba66ec9bd2ac369c657c4f94464f1f4c)), closes [#9207](https://github.com/aws/aws-cdk/issues/9207) +* **cloudfront:** support Lambda@Edge for behaviors ([#9220](https://github.com/aws/aws-cdk/issues/9220)) ([d3e5533](https://github.com/aws/aws-cdk/commit/d3e553348d93a0a8aa1617391772e4883e6c52c1)), closes [#9108](https://github.com/aws/aws-cdk/issues/9108) +* **lambda:** official lambda build docker images ([#9211](https://github.com/aws/aws-cdk/issues/9211)) ([ae0cf2a](https://github.com/aws/aws-cdk/commit/ae0cf2a3fa936771e66fa45f24af5efec52a3f21)), closes [#9205](https://github.com/aws/aws-cdk/issues/9205) +* **lambda-python:** introducing LambdaPython ([#9182](https://github.com/aws/aws-cdk/issues/9182)) ([4cc2834](https://github.com/aws/aws-cdk/commit/4cc2834e0ef2683b99c4a6258cf104f8a714479f)) +* **route53-patterns:** the route53-patterns module is now stable ([#9232](https://github.com/aws/aws-cdk/issues/9232)) ([add23bf](https://github.com/aws/aws-cdk/commit/add23bf3331f73830c918953566e1d772da34cc0)) + + +### Bug Fixes + +* **appsync:** resolver unable to set pipelineConfig ([#9093](https://github.com/aws/aws-cdk/issues/9093)) ([dac9bb3](https://github.com/aws/aws-cdk/commit/dac9bb312f5b0a9c83d929c862e30b49f3b8654a)), closes [#6923](https://github.com/aws/aws-cdk/issues/6923) +* **cloudfront:** Set MinimumProtocolVersion and SslSupportMethod when specifying distribution certificate ([#9200](https://github.com/aws/aws-cdk/issues/9200)) ([f99c327](https://github.com/aws/aws-cdk/commit/f99c3271ed2b4c68f3cd2970a1b38571f5ddc911)) +* **cloudtrail:** missing sns publish permissions ([#9239](https://github.com/aws/aws-cdk/issues/9239)) ([b4339a1](https://github.com/aws/aws-cdk/commit/b4339a19c3fbada5b9407871ccb59461102081fc)) +* **codepipeline-actions:** CodeDeployEcsDeployAction does not properly handle unnamed Artifacts ([#9147](https://github.com/aws/aws-cdk/issues/9147)) ([ac612c6](https://github.com/aws/aws-cdk/commit/ac612c6b70c01162761f6a51bdb25445da1cbf0d)), closes [#8971](https://github.com/aws/aws-cdk/issues/8971) +* **pipelines:** Reduce template size by combining IAM roles and policies ([#9243](https://github.com/aws/aws-cdk/issues/9243)) ([1ac6863](https://github.com/aws/aws-cdk/commit/1ac686384a84afae6c3254f787f2f23542b2a948)), closes [#9066](https://github.com/aws/aws-cdk/issues/9066) [#9225](https://github.com/aws/aws-cdk/issues/9225) [#9237](https://github.com/aws/aws-cdk/issues/9237) +* **rds:** SQL Server instance engine uses incorrect major version ([#9215](https://github.com/aws/aws-cdk/issues/9215)) ([eee8689](https://github.com/aws/aws-cdk/commit/eee86899f6836ceca608fb5a1f867d0062f4e5b9)), closes [#9171](https://github.com/aws/aws-cdk/issues/9171) +* **route53-targets:** Add China Partition Support for CloudFrontTarget ([#9174](https://github.com/aws/aws-cdk/issues/9174)) ([52a966a](https://github.com/aws/aws-cdk/commit/52a966a2fa6b72fefc73859a1253b36a235cd631)) +* **stepfunctions-tasks:** EvaluateExpression error when key specified multiple times ([#8858](https://github.com/aws/aws-cdk/issues/8858)) ([6506327](https://github.com/aws/aws-cdk/commit/65063275b64f647c570a06f7a28d37c5d403113b)), closes [#8856](https://github.com/aws/aws-cdk/issues/8856) + ## [1.54.0](https://github.com/aws/aws-cdk/compare/v1.53.0...v1.54.0) (2020-07-22) diff --git a/lerna.json b/lerna.json index 93411d1614ff3..0a38efa354fad 100644 --- a/lerna.json +++ b/lerna.json @@ -10,5 +10,5 @@ "tools/*" ], "rejectCycles": "true", - "version": "1.54.0" + "version": "1.55.0" }