diff --git a/.github/workflows/issue-label-assign.yml b/.github/workflows/issue-label-assign.yml index 22f20538795c7..01afcd841138d 100644 --- a/.github/workflows/issue-label-assign.yml +++ b/.github/workflows/issue-label-assign.yml @@ -54,8 +54,8 @@ jobs: {"keywords":["(@aws-cdk/aws-cloudfront)","(aws-cloudfront)","(cloudfront)","(cloud front)"],"labels":["@aws-cdk/aws-cloudfront"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-cloudfront-origins)","(aws-cloudfront-origins)","(cloudfront-origins)","(cloudfront origins)"],"labels":["@aws-cdk/aws-cloudfront-origins"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-cloudtrail)","(aws-cloudtrail)","(cloudtrail)","(cloud trail)"],"labels":["@aws-cdk/aws-cloudtrail"],"assignees":["NetaNir"]}, - {"keywords":["(@aws-cdk/aws-cloudwatch)","(aws-cloudwatch)","(cloudwatch)","(cloud watch)"],"labels":["@aws-cdk/aws-cloudwatch"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-cloudwatch-actions)","(aws-cloudwatch-actions)","(cloudwatch-actions)","(cloudwatch actions)"],"labels":["@aws-cdk/aws-cloudwatch-actions"],"assignees":["rix0rrr"]}, + {"keywords":["(@aws-cdk/aws-cloudwatch)","(aws-cloudwatch)","(cloudwatch)","(cloud watch)"],"labels":["@aws-cdk/aws-cloudwatch"],"assignees":["NetaNir"]}, + {"keywords":["(@aws-cdk/aws-cloudwatch-actions)","(aws-cloudwatch-actions)","(cloudwatch-actions)","(cloudwatch actions)"],"labels":["@aws-cdk/aws-cloudwatch-actions"],"assignees":["NetaNir"]}, {"keywords":["(@aws-cdk/aws-codeartifact)","(aws-codeartifact)","(codeartifact)","(code artifact)","(code-artifact)"],"labels":["@aws-cdk/aws-codeartifact"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-codebuild)","(aws-codebuild)","(codebuild)","(code build)","(code-build)"],"labels":["@aws-cdk/aws-codebuild"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-codecommit)","(aws-codecommit)","(codecommit)","(code commit)", "(code-commit)"],"labels":["@aws-cdk/aws-codecommit"],"assignees":["skinny85"]}, @@ -76,8 +76,8 @@ jobs: {"keywords":["(@aws-cdk/aws-dlm)","(aws-dlm)","(dlm)"],"labels":["@aws-cdk/aws-dlm"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-dms)","(aws-dms)","(dms)"],"labels":["@aws-cdk/aws-dms"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-docdb)","(aws-docdb)","(docdb)","(doc db)","(doc-db)"],"labels":["@aws-cdk/aws-docdb"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-dynamodb)","(aws-dynamodb)","(dynamodb)","(dynamo db)","(dynamo-db)"],"labels":["@aws-cdk/aws-dynamodb"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/aws-dynamodb-global)","(aws-dynamodb-global)","(dynamodb-global)","(dynamodb global)"],"labels":["@aws-cdk/aws-dynamodb-global"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-dynamodb)","(aws-dynamodb)","(dynamodb)","(dynamo db)","(dynamo-db)"],"labels":["@aws-cdk/aws-dynamodb"],"assignees":["RomainMuller"]}, + {"keywords":["(@aws-cdk/aws-dynamodb-global)","(aws-dynamodb-global)","(dynamodb-global)","(dynamodb global)"],"labels":["@aws-cdk/aws-dynamodb-global"],"assignees":["RomainMuller"]}, {"keywords":["(@aws-cdk/aws-ec2)","(aws-ec2)","(ec2)","(vpc)"],"labels":["@aws-cdk/aws-ec2"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-ecr)","(aws-ecr)","(ecr)"],"labels":["@aws-cdk/aws-ecr"],"assignees":["MrArnoldPalmer"]}, {"keywords":["(@aws-cdk/aws-ecr-assets)","(aws-ecr-assets)","(ecr-assets)","(ecr assets)","(ecrassets)"],"labels":["@aws-cdk/aws-ecr-assets"],"assignees":["eladb"]}, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2b1a950a551fe..7615d7b10db4e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,6 +14,7 @@ and let us know if it's not up-to-date (even better, submit a PR with your corr - [Step 4: Commit](#step-4-commit) - [Step 5: Pull Request](#step-5-pull-request) - [Step 6: Merge](#step-6-merge) +- [Breaking Changes](#breaking-changes) - [Tools](#tools) - [Main build scripts](#main-build-scripts) - [Partial build tools](#partial-build-tools) @@ -266,6 +267,143 @@ BREAKING CHANGE: Description of what broke and how to achieve this behavior now * Once approved and tested, a maintainer will squash-merge to master and will use your PR title/description as the commit message. +## Breaking Changes + +Whenever you are making changes, there is a chance for those changes to be +*breaking* existing users of the library. A change is breaking if there are +programs that customers could have been writing against the current version +of the CDK, that will no longer "work correctly" with the proposed new +version of the CDK. + +Breaking changes are not allowed in *stable* libraries¹. They are permissible +but still *highly discouraged* in experimental libraries, and require explicit +callouts in the bodies of Pull Requests that introduce them. + +> ¹) Note that starting in version 2 of the CDK, the majority of library code will be +> bundled into a single main CDK library which will be considered stable, and so +> no code in there can undergo breaking changes. + +Breaking changes come in two flavors: + +* API surface changes +* Behavior changes + +### API surface changes + +This encompasses any changes that affect the shape of the API. Changes that +will make existing programs fail to compile are not allowed. Typical examples +of that are: + +* Renaming classes or methods +* Adding required properties to a struct that is used as an input to a constructor + or method. This also includes changing a type from nullable to non-nullable. +* Removing properties from a struct that is returned from a method, or removing + properties from a class. This also includes changing a type from non-nullable + to nullable. + +To see why the latter is a problem, consider the following class: + +```ts +class SomeClass { + public readonly count: number; + // ❓ let's say I want to change this to 'count?: number', + // i.e. make it optional. +} + +// Someone could have written the following code: +const obj = new SomeClass(); +console.log(obj.count + 1); + +// After the proposed change, this code that used to compile fine will now throw: +console.log(obj.count + 1); +// ~~~~~~~~~ Error: Object is possibly 'undefined'. +``` + +CDK comes with build tooling to check whether changes you made introduce breaking +changes to the API surface. In a package directory, run: + +```shell +$ yarn build +$ yarn compat +``` + +To figure out if the changes you made were breaking. See the section [API Compatibility +Checks](#api-compatibility-checks) for more information. + +#### Dealing with breaking API surface changes + +If you need to change the type of some API element, introduce a new API +element and mark the old API element as `@deprecated`. + +If you need to pretend to have a value for the purposes of implementing an API +and you don't actually have a useful value to return, it is acceptable to make +the property a `getter` and throw an exception (keeping in mind to write error +messages that will be useful to a user of your construct): + +```ts +class SomeClass implements ICountable { + constructor(private readonly _count?: number) { + } + + public get count(): number { + if (this._count === undefined) { + // ✅ DO: throw a descriptive error that tells the user what to do + throw new Error('This operation requires that a \'count\' is specified when SomeClass is created.'); + // ❌ DO NOT: just throw an error like 'count is missing' + } + return this._count; + } +} +``` + +### Behavior changes + +These are changes that do not directly affect the compilation of programs +written against the previous API, but may change their meaning. In practice, +even though the user didn't change their code, the CloudFormation template +that gets synthesized is now different. + +**Not all template changes are breaking changes!** Consider a user that has +created a Stack using the previous version of the library, has updated their +version of the CDK library and is now deploying an update. A behavior change +is breaking if: + +* The update cannot be applied at all +* The update can be applied but causes service interruption or data loss. + +Data loss happens when the [Logical +ID](https://docs.aws.amazon.com/cdk/latest/guide/identifiers.html#identifiers_logical_ids) +of a stateful resource changes, or one of the [resource properties that requires +replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html) +is modified. In both of these cases, CloudFormation will delete the +resource, and if it was a stateful resource like a database the data in it is now gone. + +If a change applies cleanly and does not cause any service interruption, it +is not breaking. Nevertheless, it might still be wise to avoid those kinds of +changes as users are understandably wary of unexpected template changes, will +scrutinize them heavily, and we don't want to cause unnecessary panic and churn +in our use base. + +Determining whether or not behavioral changes are breaking requires expertise +and judgement on the part of the library owner, and testing. + +#### Dealing with breaking behavior changes + +Most of the time, behavioral changes will arise because we want to change the +default value or default behavior of some property (i.e., we want to change the +interpretation of what it means if the value is missing). + +If the new behavior is going to be breaking, the user must opt in to it, either by: + +* Adding a new API element (class, property, method, ...) to have users + explicitly opt in to the new behavior at the source code level (potentially + `@deprecate`ing the old API element); or +* Use the [feature flag](#feature-flags) mechanism to have the user opt in to the new + behavior without changing the source code. + +Of these two, the first one is preferred if possible (as feature flags have +non-local effects which can cause unintended effects). + ## Tools The CDK is a big project, and at the moment, all of the CDK modules are mastered in a single monolithic repository diff --git a/DESIGN_GUIDELINES.md b/DESIGN_GUIDELINES.md index e6cebe9f434c6..0a8566ab9d90c 100644 --- a/DESIGN_GUIDELINES.md +++ b/DESIGN_GUIDELINES.md @@ -8,13 +8,77 @@ The purpose of this document is to provide guidelines for designing the APIs in the AWS Construct Library in order to ensure a consistent and integrated experience across the entire AWS surface area. +* [Preface](#preface) +* [What's Included](#what-s-included) +* [API Design](#api-design) + * [Modules](#modules) + * [Construct Class](#construct-class) + * [Construct Interface](#construct-interface) + * [Owned vs. Unowned Constructs](#owned-vs-unowned-constructs) + * [Abstract Base](#abstract-base) + * [Props](#props) + * [Types](#types) + * [Defaults](#defaults) + * [Flat](#flat) + * [Concise](#concise) + * [Naming](#naming) + * [Property Documentation](#property-documentation) + * [Enums](#enums) + * [Unions](#unions) + * [Attributes](#attributes) + * [Configuration](#configuration) + * [Prefer Additions](#prefer-additions) + * [Dropped Mutations](#dropped-mutations) + * [Factories](#factories) + * [Imports](#imports) + * [“from” Methods](#-from--methods) + * [From-attributes](#from-attributes) + * [Roles](#roles) + * [Resource Policies](#resource-policies) + * [VPC](#vpc) + * [Grants](#grants) + * [Metrics](#metrics) + * [Events](#events) + * [Connections](#connections) + * [Integrations](#integrations) + * [State](#state) + * [Physical Names - TODO](#physical-names---todo) + * [Tags](#tags) + * [Secrets](#secrets) +* [Project Structure](#project-structure) + * [Code Organization](#code-organization) +* [Implementation](#implementation) + * [General Principles](#general-principles) + * [Construct IDs](#construct-ids) + * [Errors](#errors) + * [Avoid Errors If Possible](#avoid-errors-if-possible) + * [Error reporting mechanism](#error-reporting-mechanism) + * [Throwing exceptions](#throwing-exceptions) + * [Never Catch Exceptions](#never-catch-exceptions) + * [Attaching (lazy) Validators](#attaching--lazy--validators) + * [Attaching Errors/Warnings](#attaching-errors-warnings) + * [Error messages](#error-messages) + * [Tokens](#tokens) +* [Documentation](#documentation) + * [Inline Documentation](#inline-documentation) + * [Readme](#readme) +* [Testing](#testing) + * [Unit tests](#unit-tests) + * [Integration tests](#integration-tests) + * [Versioning](#versioning) +* [Naming & Style](#naming---style) + * [Naming Conventions](#naming-conventions) + * [Coding Style](#coding-style) + +## Preface + As much as possible, the guidelines in this document are enforced using the [**awslint** tool](https://www.npmjs.com/package/awslint) which reflects on the APIs and verifies that the APIs adhere to the guidelines. When a guideline is backed by a linter rule, the rule name will be referenced like this: _[awslint:resource-class-is-construct]_. -For the purpose of this document we will use "Foo" to denote the official name +For the purpose of this document, we will use "Foo" to denote the official name of the resource as defined in the AWS CloudFormation resource specification (i.e. "Bucket", "Queue", "Topic", etc). This notation allows deriving names from the official name. For example, `FooProps` would be `BucketProps`, `TopicProps`, @@ -56,6 +120,73 @@ allows the library to be used from all supported programming languages. jsii poses restrictions on language features that cannot be idiomatically represented in target languages. +## What's Included + +The AWS Construct Library, which is shipped as part of the AWS CDK constructs +representing AWS resources. + +The AWS Construct Library has multiple layers of constructs, beginning +with low-level constructs, which we call _CFN Resources_ (or L1, short for +"level 1") or CFN Resources (short for CloudFormation). These constructs +directly represent all resources available in AWS CloudFormation. CFN Resources +are periodically generated from the AWS CloudFormation Resource +Specification. They are named **Cfn**_Xyz_, where _Xyz_ is name of the +resource. For example, CfnBucket represents the AWS::S3::Bucket AWS +CloudFormation resource. When you use Cfn resources, you must explicitly +configure all resource properties, which requires a complete understanding of +the details of the underlying AWS CloudFormation resource model. + +The next level of constructs, L2, also represent AWS resources, but with a +higher-level, intent-based API. They provide similar functionality, but provide +the defaults, boilerplate, and glue logic you'd be writing yourself with a CFN +Resource construct. L2 constructs offer convenient defaults and reduce the need +to know all the details about the AWS resources they represent, while providing +convenience methods that make it simpler to work with the resource. For example, +the `s3.Bucket` class represents an Amazon S3 bucket with additional properties +and methods, such as `bucket.addLifeCycleRule()`, which adds a lifecycle rule to +the bucket. + +Examples of behaviors that an L2 commonly include: + +- Strongly-typed modeling of the underlying L1 properties +- Methods for integrating other AWS resources (e.g., adding an event notification to + an S3 bucket). +- Modeling of permissions and resource policies +- Modeling of metrics + +In addition to the above, some L2s may introduce more complex and +helpful functionality, either part of the original L2 itself, or as part of a +separate construct. The most common form of these L2s are integration constructs +that model interactions between different services (e.g., SNS publishing to SQS, +CodePipeline actions that trigger Lambda functions). + +The next level of abstraction present within the CDK are what we designate as +"L2.5s": a step above the L2s in terms of abstraction, but not quite at the +level of complete patterns or applications. These constructs still largely +focus on a single logical resource -- in constrast to "patterns" which combine +multiple resources -- but are customized for a specific common usage scenario of +an L2. Examples of L2.5s in the CDK are `aws-apigateway.LambdaRestApi`, +`aws-lambda-nodejs.NodeJsFunction`, `aws-rds.ServerlessCluster` and `eks.FargateCluster`. + +L2.5 constructs will be considered for inclusion in the CDK if they... + +- cover a common usage scenario that can be used by a significant portion of + the community; +- provide significant ease of use over the base L2 (via usage-specific defaults + convenience methods or improved strong-typing); +- simplify or enable another L2 within the CDK + +The CDK also currently includes some even higher-level constructs, which we call +patterns. These constructs often involve multiple kinds of resources and are +designed to help you complete common tasks in AWS or represent entire +applications. For example, the +`aws-ecs-patterns.ApplicationLoadBalancedFargateService` construct represents an +architecture that includes an AWS Fargate container cluster employing an +Application Load Balancer (ALB). These patterns are typically difficult to +design to be one-size-fits-all and are best suited to be published as separate +libraries, rather than included directly in the CDK. The patterns that currently +exist in the CDK will be removed in the next CDK major version (CDKv2). + ## API Design ### Modules @@ -98,8 +229,8 @@ or abstractions. However, you will notice that some sections explicitly call out guidelines that apply only to AWS resources (and in many cases enforced/implemented by the **Resource** base class). -AWS services are modeled around the concept of *resources*. Service normally -expose through their APIs one or more resources, which can be provisioned +AWS services are modeled around the concept of *resources*. Services normally +expose one or more resources through their APIs, which can be provisioned through the APIs control plane or through AWS CloudFormation. Every resource available in the AWS platform will have a corresponding resource @@ -333,14 +464,31 @@ from harnessing the full power of the resource, and customizing its behavior. alignment. The **@default** documentation tag must be included on all optional properties -of interfaces. Since there are cases where the default behavior is not a -specific value but rather depends on circumstances/context, the default -documentation tag must always begin with a “**-**" and then include a -description of the default behavior _[awslint:props-default-doc]_. +of interfaces. + +In cases where the default behavior can be described by a value (typically the +case for booleans and enums, sometimes for strings and numbers), the value immediately +follows the **@default** tag and should be a valid JavaScript value (as in: +`@default false`, or `@default "stringValue"`). + +In the majority of cases, the default behavior is not a specific value but +rather depends on circumstances/context. The default documentation tag must +begin with a “**-**" and then include a description of the default behavior +_[awslint:props-default-doc]_. This is specially true if the property +is a complex value or a reference to an object: don't write `@default +undefined`, describe the behavior that happens if the property is not +supplied. -For example: +Describe the default value or default behavior, even if it's not CDK that +controls the default. For example, if an absent value does not get rendered +into the template and it's ultimately the AWS *service* that determines the +default behavior, we still describe it in our documentation. + +Examples: ```ts +// ✅ DO - uses a '-' and describes the behavior + /** * External KMS key to use for bucket encryption. * @@ -350,6 +498,32 @@ For example: encryptionKey?: kms.IEncryptionKey; ``` +```ts +/** + * External KMS key to use for bucket encryption. + * + * @default undefined + * ❌ DO NOT - that the value is 'undefined' by default is implied. However, + * what will the *behavior* be if the value is left out? + */ +encryptionKey?: kms.IEncryptionKey; +``` + +```ts +/** + * Minimum capacity of the AutoScaling resource + * + * @default - no minimum capacity + * ❌ DO NOT - there most certainly is. It's probably 0 or 1. + * + * // OR + * @default - the minimum capacity is the default minimum capacity + * ❌ DO NOT - this is circular and useless to the reader. + * Describe what will actually happen. + */ +minCapacity?: number; +``` + #### Flat Do not introduce artificial nesting for props. It hinders discoverability and @@ -397,12 +571,12 @@ For example, prefer “readCapacity” versus “readCapacityUnits”. We prefer the terminology used by the official AWS service documentation over new terminology, even if you think it's not ideal. It helps users diagnose issues and map the mental model of the construct to the service APIs, -documentation and examples. For example don't be tempted to change SQS's +documentation and examples. For example, don't be tempted to change SQS's **dataKeyReusePeriod** with **keyRotation** because it will be hard for people to diagnose problems. They won't be able to just search for “sqs dataKeyReuse” and find topics on it. -> We can relax this guidelines when this is about generic terms (like +> We can relax this guideline when this is about generic terms (like `httpStatus` instead of `statusCode`). The important semantics to preserve are for *service features*: I wouldn't want to rename "lambda layers" to "lambda dependencies" just because it makes more sense because then users won't be @@ -697,8 +871,8 @@ _[awslint:from-signature]_: #### “from” Methods Resource constructs should export static “from” methods for importing unowned -resources given one more of its physical attributes such as ARN, name, etc. All -constructs should have at least one "fromXxx" method _[awslint:from-method]_: +resources given one or more of its physical attributes such as ARN, name, etc. All +constructs should have at least one `fromXxx` method _[awslint:from-method]_: ```ts static fromFooArn(scope: Construct, id: string, bucketArn: string): IFoo; @@ -870,7 +1044,7 @@ vpcSubnetSelection?: ec2.SubnetSelection; ### Grants -Grants are one of the most powerful concept in the AWS Construct Library. They +Grants are one of the most powerful concepts in the AWS Construct Library. They offer a higher level, intent-based, API for managing IAM permissions for AWS resources. @@ -974,7 +1148,7 @@ class Function extends Resource implements IFunction { ### Events -Many AWS resource emit events to the CloudWatch event bus. Such resources should +Many AWS resources emit events to the CloudWatch event bus. Such resources should have a set of “onXxx” methods available on their construct interface _[awslint:events-in-interface]_. @@ -1216,19 +1390,6 @@ for (const az of availabilityZones) { ### Errors -#### Input Validation - -Prefer to validate input as early as it is passed into your code (ctor, methods, -etc) and bail out by throwing an **Error** (no need to create subclasses of -Error since all errors in the CDK are unrecoverable): - -* All lowercase sentences (usually they are printed after “Error: \”) -* Include a descriptive message -* Include the value provided -* Include the expected/allowed values -* No need to include information that can be obtained from the stack trace -* No need to add a period at the end of error messages - #### Avoid Errors If Possible Always prefer to do the right thing for the user instead of raising an @@ -1237,18 +1398,79 @@ example, VPC has **enableDnsHostnames** and **enableDnsSupport**. DNS hostnames *require* DNS support, so only fail if the user enabled DNS hostnames but explicitly disabled DNS support. Otherwise, auto-enable DNS support for them. +#### Error reporting mechanism + +There are three mechanism you can use to report errors: + +* Eagerly throw an exception (fails synthesis) +* Attach a (lazy) validator to a construct (fails synthesis) +* Attach errors to a construct (succeeds synthesis, fails deployment) + +Between these, the first two fail synthesis, while the latter doesn't. Failing synthesis +means that no Cloud Assembly will be produced. + +The distinction becomes apparent when you consider multiple stacks in the same Cloud +Assembly: + +* If synthesis fails due to an error in *one* stack (either by throwing an exception + or by failing validation), the other stack can also not be deployed. +* In contrast, if you attach an error to a construct in one stack, that stack cannot + be deployed but the other one still can. + +Choose one of the first two methods if the failure is caused by a misuse of the API, +which the user should be alerted to and fix as quickly as possible. Choose attaching +an error to a construct if the failure is due to environmental factors outside the +direct use of the API surface (for example, lack of context provider lookup values). + +#### Throwing exceptions + +This should be the preferred error reporting method. + +Validate input as early as it is passed into your code (ctor, methods, +etc) and bail out by throwing an `Error`. No need to create subclasses of +Error since all errors in the CDK are unrecoverable. + +When validating inputs, don't forget to account for the fact that these +values may be `Token`s and not available for inspection at synthesis time. + +Example: + +```ts +if (!Token.isUnresolved(props.minCapacity) && props.minCapacity < 1) { + throw new Error(`'minCapacity' should be at least 1, got '${props.minCapacity}'`); +} +``` + #### Never Catch Exceptions -All CDK errors are unrecoverable. If a method wishes to signal a recoverable +All CDK errors are unrecoverable. If a method wishes to signal a recoverable error, this should be modeled in a return value and not through exceptions. -#### Post Validation +#### Attaching (lazy) Validators + +In the rare case where the integrity of your construct can only be checked +after the app has completed its initialization, call the +**this.node.addValidation()** method to add a validation object. This will +generally only be necessary if you want to produce an error when a certain +interaction with your construct did *not* happen (for example, a property +that should have been configured over the lifetime of the construct, wasn't): + +Always prefer early input validation over post-validation, as the necessity +of these should be rare. + +Example: -In the rare case where the integrity of your construct can only be checked right -before synthesis, override the **Construct.validate()** method and return -meaningful errors. Always prefer early input validation over post-validation. +```ts +this.node.addValidation({ + // 'validate' should return a string[] list of errors + validate: () => this.rules.length === 0 + ? ['At least one Rule must be added. Call \'addRule()\' to add Rules.'] + : [] + } +}); +``` -#### Attached Errors/Warnings +#### Attaching Errors/Warnings You can also “attach” an error or a warning to a construct via the **Annotations** class. These methods (e.g., `Annotations.of(construct).addWarning`) @@ -1256,7 +1478,46 @@ will attach CDK metadata to your construct, which will be displayed to the user by the toolchain when the stack is deployed. Errors will not allow deployment and warnings will only be displayed in -highlight (unless **--strict** mode is used). +highlight (unless `--strict` mode is used). + +```ts +if (!Token.isUnresolved(subnetIds) && subnetIds.length < 2) { + Annotations.of(this).addError(`Need at least 2 subnet ids, got: ${JSON.stringify(subnetIds)}`); +} +``` + +#### Error messages + +Think about error messages from the point of view of the end user of the CDK. +This is not necessarily someone who knows about the internals of your +construct library, so try to phrase the message in a way that would make +sense to them. + +For example, if a value the user supplied gets handed off between a number of +functions before finally being validated, phrase the message in terms of the +API the user interacted with, not in terms of the internal APIs. + +A good error message should include the following components: + +* What went wrong, in a way that makes sense to a top-level user +* An example of the incorrect value provided (if applicable) +* An example of the expected/allowed values (if applicable) +* The message should explain the (most likely) cause and change the user can + make to rectify the situation + +The message should be all lowercase and not end in a period, or contain +information that can be obtained from the stack trace. + +```ts +// ✅ DO - show the value you got and be specific about what the user should do +`supply at least one of minCapacity or maxCapacity, got ${JSON.stringify(action)}` + +// ❌ DO NOT - this tells the user nothing about what's wrong or what they should do +`required values are missing` + +// ❌ DO NOT - this error only makes sense if you know the implementation +`'undefined' is not a number` +``` ### Tokens diff --git a/allowed-breaking-changes.txt b/allowed-breaking-changes.txt index 964bb4d5c7712..2ca2ca5b6067f 100644 --- a/allowed-breaking-changes.txt +++ b/allowed-breaking-changes.txt @@ -56,10 +56,3 @@ incompatible-argument:@aws-cdk/aws-ecs.TaskDefinition.addVolume # We made properties optional and it's really fine but our differ doesn't think so. weakened:@aws-cdk/cloud-assembly-schema.DockerImageSource weakened:@aws-cdk/cloud-assembly-schema.FileSource - -# https://github.com/aws/aws-cdk/pull/13145 -removed:@aws-cdk/core.AssetStaging.isArchive -removed:@aws-cdk/core.AssetStaging.packaging -removed:@aws-cdk/core.BundlingOutput -removed:@aws-cdk/core.BundlingOptions.outputType - diff --git a/package.json b/package.json index a986d658240f5..84eeb77a0016c 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,9 @@ "fs-extra": "^9.1.0", "graceful-fs": "^4.2.6", "jest-junit": "^12.0.0", - "jsii-diff": "^1.21.0", - "jsii-pacmak": "^1.21.0", - "jsii-rosetta": "^1.21.0", + "jsii-diff": "^1.23.0", + "jsii-pacmak": "^1.23.0", + "jsii-rosetta": "^1.23.0", "lerna": "^3.22.1", "standard-version": "^9.1.1", "typescript": "~3.9.9" diff --git a/packages/@aws-cdk/assert/package.json b/packages/@aws-cdk/assert/package.json index 1416fd7f4673c..17a2d01fae5c9 100644 --- a/packages/@aws-cdk/assert/package.json +++ b/packages/@aws-cdk/assert/package.json @@ -25,7 +25,7 @@ "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "pkglint": "0.0.0", - "ts-jest": "^26.5.1" + "ts-jest": "^26.5.2" }, "dependencies": { "@aws-cdk/cloud-assembly-schema": "0.0.0", diff --git a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json index 1e220d75e96a9..d0f82b65a940e 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json +++ b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json @@ -29,7 +29,7 @@ "devDependencies": { "aws-sdk": "^2.596.0", "aws-sdk-mock": "^5.1.0", - "eslint": "^7.20.0", + "eslint": "^7.21.0", "eslint-config-standard": "^14.1.1", "eslint-plugin-import": "^2.22.1", "eslint-plugin-node": "^11.1.0", @@ -37,7 +37,7 @@ "eslint-plugin-standard": "^4.1.0", "jest": "^26.6.3", "lambda-tester": "^3.6.0", - "nock": "^13.0.7", - "ts-jest": "^26.5.1" + "nock": "^13.0.9", + "ts-jest": "^26.5.2" } } diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.ts index 7f5e9d49a95db..bdd4ae31af241 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.ts @@ -23,7 +23,7 @@ class TestStack extends Stack { const serviceToken = CustomResourceProvider.getOrCreate(this, resourceType, { codeDirectory: `${__dirname}/core-custom-resource-provider-fixture`, - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, description: 'veni vidi vici', }); diff --git a/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts b/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts index 33b0e070d144e..b12a56fe67e80 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts @@ -51,8 +51,6 @@ export class EdgeFunction extends Resource implements lambda.IVersion { public readonly role?: iam.IRole; public readonly version: string; - // functionStack needed for `addAlias`. - private readonly functionStack: Stack; private readonly _edgeFunction: lambda.Function; constructor(scope: Construct, id: string, props: EdgeFunctionProps) { @@ -60,11 +58,10 @@ export class EdgeFunction extends Resource implements lambda.IVersion { // Create a simple Function if we're already in us-east-1; otherwise create a cross-region stack. const regionIsUsEast1 = !Token.isUnresolved(this.stack.region) && this.stack.region === 'us-east-1'; - const { functionStack, edgeFunction, edgeArn } = regionIsUsEast1 + const { edgeFunction, edgeArn } = regionIsUsEast1 ? this.createInRegionFunction(props) : this.createCrossRegionFunction(id, props); - this.functionStack = functionStack; this.edgeArn = edgeArn; this.functionArn = edgeArn; @@ -89,7 +86,7 @@ export class EdgeFunction extends Resource implements lambda.IVersion { } public addAlias(aliasName: string, options: lambda.AliasOptions = {}): lambda.Alias { - return new lambda.Alias(this.functionStack, `Alias${aliasName}`, { + return new lambda.Alias(this._edgeFunction, `Alias${aliasName}`, { aliasName, version: this._edgeFunction.currentVersion, ...options, @@ -146,7 +143,7 @@ export class EdgeFunction extends Resource implements lambda.IVersion { const edgeFunction = new lambda.Function(this, 'Fn', props); addEdgeLambdaToRoleTrustStatement(edgeFunction.role!); - return { edgeFunction, edgeArn: edgeFunction.currentVersion.edgeArn, functionStack: this.stack }; + return { edgeFunction, edgeArn: edgeFunction.currentVersion.edgeArn }; } /** Create a support stack and function in us-east-1, and a SSM reader in-region */ @@ -166,7 +163,7 @@ export class EdgeFunction extends Resource implements lambda.IVersion { const edgeArn = this.createCrossRegionArnReader(parameterNamePrefix, parameterName, edgeFunction); - return { edgeFunction, edgeArn, functionStack }; + return { edgeFunction, edgeArn }; } private createCrossRegionArnReader(parameterNamePrefix: string, parameterName: string, edgeFunction: lambda.Function): string { @@ -183,7 +180,7 @@ export class EdgeFunction extends Resource implements lambda.IVersion { const resourceType = 'Custom::CrossRegionStringParameterReader'; const serviceToken = CustomResourceProvider.getOrCreate(this, resourceType, { codeDirectory: path.join(__dirname, 'edge-function'), - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, policyStatements: [{ Effect: 'Allow', Resource: parameterArnPrefix, @@ -233,7 +230,6 @@ export class EdgeFunction extends Resource implements lambda.IVersion { interface FunctionConfig { readonly edgeFunction: lambda.Function; readonly edgeArn: string; - readonly functionStack: Stack; } function addEdgeLambdaToRoleTrustStatement(role: iam.IRole) { diff --git a/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts b/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts index 7a743056dfcb7..55b0c2f4aeaac 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts @@ -217,6 +217,17 @@ test('addAlias() creates alias in function stack', () => { }); }); +test('mutliple aliases with the same name can be added to the same stack', () => { + const fn1 = new cloudfront.experimental.EdgeFunction(stack, 'MyFn1', defaultEdgeFunctionProps()); + const fn2 = new cloudfront.experimental.EdgeFunction(stack, 'MyFn2', defaultEdgeFunctionProps()); + fn1.addAlias('live'); + fn2.addAlias('live'); + + const fnStack = getFnStack(); + expect(fnStack).toCountResources('AWS::Lambda::Function', 2); + expect(fnStack).toCountResources('AWS::Lambda::Alias', 2); +}); + test('addPermission() creates permissions in function stack', () => { const fn = new cloudfront.experimental.EdgeFunction(stack, 'MyFn', defaultEdgeFunctionProps()); diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.expected.json b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.expected.json index 412498ec96a3f..c251765b20980 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.expected.json +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.expected.json @@ -159,36 +159,9 @@ "FunctionArn" ] } - } - ], - "TargetOriginId": "integdistributionlambdacrossregionDistOrigin167A054D5", - "ViewerProtocolPolicy": "allow-all" - }, - "Enabled": true, - "HttpVersion": "http2", - "IPV6Enabled": true, - "Origins": [ - { - "CustomOriginConfig": { - "OriginProtocolPolicy": "https-only" }, - "DomainName": "www.example.com", - "Id": "integdistributionlambdacrossregionDistOrigin167A054D5" - } - ] - } - } - }, - "Dist286EC08DF": { - "Type": "AWS::CloudFront::Distribution", - "Properties": { - "DistributionConfig": { - "DefaultCacheBehavior": { - "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad", - "Compress": true, - "LambdaFunctionAssociations": [ { - "EventType": "origin-request", + "EventType": "origin-response", "LambdaFunctionARN": { "Fn::GetAtt": [ "Lambda2ArnReader5ACFBE1F", @@ -197,7 +170,7 @@ } } ], - "TargetOriginId": "integdistributionlambdacrossregionDist2Origin14F08376D", + "TargetOriginId": "integdistributionlambdacrossregionDistOrigin167A054D5", "ViewerProtocolPolicy": "allow-all" }, "Enabled": true, @@ -208,8 +181,8 @@ "CustomOriginConfig": { "OriginProtocolPolicy": "https-only" }, - "DomainName": "www.example2.com", - "Id": "integdistributionlambdacrossregionDist2Origin14F08376D" + "DomainName": "www.example.com", + "Id": "integdistributionlambdacrossregionDistOrigin167A054D5" } ] } @@ -277,13 +250,13 @@ "Code": { "ZipFile": "foo" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "LambdaServiceRoleA8ED4D3B", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -307,6 +280,21 @@ }, "Name": "EdgeFunctionArnLambda" } + }, + "LambdaAliaslive79C8A712": { + "Type": "AWS::Lambda::Alias", + "Properties": { + "FunctionName": { + "Ref": "LambdaD247545B" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "LambdaCurrentVersionDF706F6A97fb843e9bd06fcd2bb15eeace80e13e", + "Version" + ] + }, + "Name": "live" + } } } }, @@ -356,13 +344,13 @@ "Code": { "ZipFile": "foo" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "Lambda2ServiceRole31A072E1", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -386,6 +374,21 @@ }, "Name": "EdgeFunctionArnLambda2" } + }, + "Lambda2Aliaslive77F6085F": { + "Type": "AWS::Lambda::Alias", + "Properties": { + "FunctionName": { + "Ref": "Lambda217CFB423" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "Lambda2CurrentVersion72012B74b9eef8becb98501bc795baca3c6169c4", + "Version" + ] + }, + "Name": "live" + } } } } diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.ts b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.ts index df2c26aecfd26..b1aed3d79da21 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.ts @@ -22,25 +22,23 @@ const lambdaFunction2 = new cloudfront.experimental.EdgeFunction(stack, 'Lambda2 stackId: `edge-lambda-stack-${region}-2`, }); +lambdaFunction.addAlias('live'); +lambdaFunction2.addAlias('live'); + new cloudfront.Distribution(stack, 'Dist', { defaultBehavior: { origin: new TestOrigin('www.example.com'), cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, - edgeLambdas: [{ - functionVersion: lambdaFunction.currentVersion, - eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, - }], - }, -}); - -new cloudfront.Distribution(stack, 'Dist2', { - defaultBehavior: { - origin: new TestOrigin('www.example2.com'), - cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, - edgeLambdas: [{ - functionVersion: lambdaFunction2.currentVersion, - eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, - }], + edgeLambdas: [ + { + functionVersion: lambdaFunction.currentVersion, + eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, + }, + { + functionVersion: lambdaFunction2.currentVersion, + eventType: cloudfront.LambdaEdgeEventType.ORIGIN_RESPONSE, + }, + ], }, }); diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 4a9a8f5b250cb..d3f90ff026b36 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -418,6 +418,7 @@ The following third-party identity providers are currently supported in the CDK - [Login With Amazon](https://developer.amazon.com/apps-and-games/login-with-amazon) - [Facebook Login](https://developers.facebook.com/docs/facebook-login/) - [Google Login](https://developers.google.com/identity/sign-in/web/sign-in) +- [Sign In With Apple](https://developer.apple.com/sign-in-with-apple/get-started/) The following code configures a user pool to federate with the third party provider, 'Login with Amazon'. The identity provider needs to be configured with a set of credentials that the Cognito backend can use to federate with the diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts index ea5693f45d1c4..866c11015ecfd 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts @@ -157,6 +157,12 @@ export class OAuthScope { * Identity providers supported by the UserPoolClient */ export class UserPoolClientIdentityProvider { + /** + * Allow users to sign in using 'Sign In With Apple'. + * A `UserPoolIdentityProviderApple` must be attached to the user pool. + */ + public static readonly APPLE = new UserPoolClientIdentityProvider('SignInWithApple'); + /** * Allow users to sign in using 'Facebook Login'. * A `UserPoolIdentityProviderFacebook` must be attached to the user pool. diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/apple.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/apple.ts new file mode 100644 index 0000000000000..c1fbd6d4ca9fa --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/apple.ts @@ -0,0 +1,63 @@ +import { Construct } from 'constructs'; +import { CfnUserPoolIdentityProvider } from '../cognito.generated'; +import { UserPoolIdentityProviderProps } from './base'; +import { UserPoolIdentityProviderBase } from './private/user-pool-idp-base'; + +/** + * Properties to initialize UserPoolAppleIdentityProvider + */ +export interface UserPoolIdentityProviderAppleProps extends UserPoolIdentityProviderProps { + /** + * The client id recognized by Apple APIs. + * @see https://developer.apple.com/documentation/sign_in_with_apple/clientconfigi/3230948-clientid + */ + readonly clientId: string; + /** + * The teamId for Apple APIs to authenticate the client. + */ + readonly teamId: string; + /** + * The keyId (of the same key, which content has to be later supplied as `privateKey`) for Apple APIs to authenticate the client. + */ + readonly keyId: string; + /** + * The privateKey content for Apple APIs to authenticate the client. + */ + readonly privateKey: string; + /** + * The list of apple permissions to obtain for getting access to the apple profile + * @see https://developer.apple.com/documentation/sign_in_with_apple/clientconfigi/3230955-scope + * @default [ name ] + */ + readonly scopes?: string[]; +} + +/** + * Represents a identity provider that integrates with 'Apple' + * @resource AWS::Cognito::UserPoolIdentityProvider + */ +export class UserPoolIdentityProviderApple extends UserPoolIdentityProviderBase { + public readonly providerName: string; + + constructor(scope: Construct, id: string, props: UserPoolIdentityProviderAppleProps) { + super(scope, id, props); + + const scopes = props.scopes ?? ['name']; + + const resource = new CfnUserPoolIdentityProvider(this, 'Resource', { + userPoolId: props.userPool.userPoolId, + providerName: 'SignInWithApple', // must be 'SignInWithApple' when the type is 'SignInWithApple' + providerType: 'SignInWithApple', + providerDetails: { + client_id: props.clientId, + team_id: props.teamId, + key_id: props.keyId, + private_key: props.privateKey, + authorize_scopes: scopes.join(' '), + }, + attributeMapping: super.configureAttributeMapping(), + }); + + this.providerName = super.getResourceNameAttribute(resource.ref); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts index be155fca69a6d..08278947b9e04 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts @@ -4,6 +4,15 @@ import { IUserPool } from '../user-pool'; * An attribute available from a third party identity provider. */ export class ProviderAttribute { + /** The email attribute provided by Apple */ + public static readonly APPLE_EMAIL = new ProviderAttribute('email'); + /** The name attribute provided by Apple */ + public static readonly APPLE_NAME = new ProviderAttribute('name'); + /** The first name attribute provided by Apple */ + public static readonly APPLE_FIRST_NAME = new ProviderAttribute('firstName'); + /** The last name attribute provided by Apple */ + public static readonly APPLE_LAST_NAME = new ProviderAttribute('lastName'); + /** The user id attribute provided by Amazon */ public static readonly AMAZON_USER_ID = new ProviderAttribute('user_id'); /** The email attribute provided by Amazon */ diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts index dbc63a9854f37..321ee0ecad5d9 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts @@ -1,4 +1,5 @@ export * from './base'; +export * from './apple'; export * from './amazon'; export * from './facebook'; export * from './google'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/package.json b/packages/@aws-cdk/aws-cognito/package.json index c9bae0f953907..3a981167956a3 100644 --- a/packages/@aws-cdk/aws-cognito/package.json +++ b/packages/@aws-cdk/aws-cognito/package.json @@ -110,7 +110,8 @@ "props-physical-name:@aws-cdk/aws-cognito.UserPoolDomainProps", "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderFacebookProps", "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderAmazonProps", - "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderGoogleProps" + "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderGoogleProps", + "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderAppleProps" ] }, "stability": "stable", diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.apple.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.apple.expected.json new file mode 100644 index 0000000000000..64baf88ef6807 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.apple.expected.json @@ -0,0 +1,118 @@ +{ + "Resources": { + "pool056F3F7E": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsVerificationMessage": "The verification code to your new account is {####}", + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "poolclient2623294C": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "pool056F3F7E" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + { + "Ref": "apple9B5408AC" + }, + "COGNITO" + ] + } + }, + "pooldomain430FA744": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "nija-test-pool", + "UserPoolId": { + "Ref": "pool056F3F7E" + } + } + }, + "apple9B5408AC": { + "Type": "AWS::Cognito::UserPoolIdentityProvider", + "Properties": { + "ProviderName": "SignInWithApple", + "ProviderType": "SignInWithApple", + "UserPoolId": { + "Ref": "pool056F3F7E" + }, + "AttributeMapping": { + "family_name": "lastName", + "given_name": "firstName" + }, + "ProviderDetails": { + "client_id": "com.amzn.cdk", + "team_id": "CDKTEAMCDK", + "key_id": "CDKKEYCDK1", + "private_key": "PRIV_KEY_CDK", + "authorize_scopes": "email name" + } + } + } + }, + "Outputs": { + "SignInLink": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "pooldomain430FA744" + }, + ".auth.", + { + "Ref": "AWS::Region" + }, + ".amazoncognito.com/login?client_id=", + { + "Ref": "poolclient2623294C" + }, + "&response_type=code&redirect_uri=https://example.com" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.apple.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.apple.ts new file mode 100644 index 0000000000000..fb8e15f26e308 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.apple.ts @@ -0,0 +1,41 @@ +import { App, CfnOutput, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderApple } from '../lib'; + +/* + * Stack verification steps + * * Visit the URL provided by stack output 'SignInLink' in a browser, and verify the 'Sign In With Apple' link shows up. + * * If you plug in valid 'Sign In With Apple' credentials, the federated log in should work. + */ +const app = new App(); +const stack = new Stack(app, 'integ-user-pool-idp-apple'); + +const userpool = new UserPool(stack, 'pool', { + removalPolicy: RemovalPolicy.DESTROY, +}); + +new UserPoolIdentityProviderApple(stack, 'apple', { + userPool: userpool, + clientId: 'com.amzn.cdk', + teamId: 'CDKTEAMCDK', + keyId: 'CDKKEYCDK1', + privateKey: 'PRIV_KEY_CDK', + scopes: ['email', 'name'], + attributeMapping: { + familyName: ProviderAttribute.APPLE_LAST_NAME, + givenName: ProviderAttribute.APPLE_FIRST_NAME, + }, +}); + +const client = userpool.addClient('client'); + +const domain = userpool.addDomain('domain', { + cognitoDomain: { + domainPrefix: 'nija-test-pool', + }, +}); + +new CfnOutput(stack, 'SignInLink', { + value: domain.signInUrl(client, { + redirectUri: 'https://example.com', + }), +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts index 3a056cd02dda7..02420721df344 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts @@ -487,13 +487,14 @@ describe('User Pool Client', () => { UserPoolClientIdentityProvider.FACEBOOK, UserPoolClientIdentityProvider.AMAZON, UserPoolClientIdentityProvider.GOOGLE, + UserPoolClientIdentityProvider.APPLE, ], }); // THEN expect(stack).toHaveResource('AWS::Cognito::UserPoolClient', { ClientName: 'AllEnabled', - SupportedIdentityProviders: ['COGNITO', 'Facebook', 'LoginWithAmazon', 'Google'], + SupportedIdentityProviders: ['COGNITO', 'Facebook', 'LoginWithAmazon', 'Google', 'SignInWithApple'], }); }); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/apple.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/apple.ts new file mode 100644 index 0000000000000..5f4180bce5682 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/apple.ts @@ -0,0 +1,113 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderApple } from '../../lib'; + +describe('UserPoolIdentityProvider', () => { + describe('apple', () => { + test('defaults', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderApple(stack, 'userpoolidp', { + userPool: pool, + clientId: 'com.amzn.cdk', + teamId: 'CDKTEAMCDK', + keyId: 'CDKKEYCDK1', + privateKey: 'PRIV_KEY_CDK', + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'SignInWithApple', + ProviderType: 'SignInWithApple', + ProviderDetails: { + client_id: 'com.amzn.cdk', + team_id: 'CDKTEAMCDK', + key_id: 'CDKKEYCDK1', + private_key: 'PRIV_KEY_CDK', + authorize_scopes: 'name', + }, + }); + }); + + test('scopes', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderApple(stack, 'userpoolidp', { + userPool: pool, + clientId: 'com.amzn.cdk', + teamId: 'CDKTEAMCDK', + keyId: 'CDKKEYCDK1', + privateKey: 'PRIV_KEY_CDK', + scopes: ['scope1', 'scope2'], + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'SignInWithApple', + ProviderType: 'SignInWithApple', + ProviderDetails: { + client_id: 'com.amzn.cdk', + team_id: 'CDKTEAMCDK', + key_id: 'CDKKEYCDK1', + private_key: 'PRIV_KEY_CDK', + authorize_scopes: 'scope1 scope2', + }, + }); + }); + + test('registered with user pool', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + const provider = new UserPoolIdentityProviderApple(stack, 'userpoolidp', { + userPool: pool, + clientId: 'com.amzn.cdk', + teamId: 'CDKTEAMCDK', + keyId: 'CDKKEYCDK1', + privateKey: 'PRIV_KEY_CDK', + }); + + // THEN + expect(pool.identityProviders).toContain(provider); + }); + + test('attribute mapping', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderApple(stack, 'userpoolidp', { + userPool: pool, + clientId: 'com.amzn.cdk', + teamId: 'CDKTEAMCDK', + keyId: 'CDKKEYCDK1', + privateKey: 'PRIV_KEY_CDK', + attributeMapping: { + familyName: ProviderAttribute.APPLE_LAST_NAME, + givenName: ProviderAttribute.APPLE_FIRST_NAME, + custom: { + customAttr1: ProviderAttribute.APPLE_EMAIL, + customAttr2: ProviderAttribute.other('sub'), + }, + }, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + AttributeMapping: { + family_name: 'firstName', + given_name: 'lastName', + customAttr1: 'email', + customAttr2: 'sub', + }, + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-docdb/README.md b/packages/@aws-cdk/aws-docdb/README.md index 826033de4c134..530942578a090 100644 --- a/packages/@aws-cdk/aws-docdb/README.md +++ b/packages/@aws-cdk/aws-docdb/README.md @@ -30,7 +30,7 @@ your instances will be launched privately or publicly: ```ts const cluster = new DatabaseCluster(this, 'Database', { masterUser: { - username: 'admin' + username: 'myuser' // NOTE: 'admin' is reserved by DocumentDB }, instanceProps: { instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.LARGE), diff --git a/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json b/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json index 8d1e45ac66b83..001e9b369ea83 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json +++ b/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json @@ -29,7 +29,7 @@ "devDependencies": { "aws-sdk": "^2.596.0", "aws-sdk-mock": "^5.1.0", - "eslint": "^7.20.0", + "eslint": "^7.21.0", "eslint-config-standard": "^14.1.1", "eslint-plugin-import": "^2.22.1", "eslint-plugin-node": "^11.1.0", @@ -37,6 +37,6 @@ "eslint-plugin-standard": "^4.1.0", "jest": "^26.6.3", "lambda-tester": "^3.6.0", - "nock": "^13.0.7" + "nock": "^13.0.9" } } diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index 6ca3616c33ac8..5737a58a67816 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -81,7 +81,7 @@ "jest": "^26.6.3", "pkglint": "0.0.0", "sinon": "^9.2.4", - "ts-jest": "^26.5.1" + "ts-jest": "^26.5.2" }, "dependencies": { "@aws-cdk/aws-applicationautoscaling": "0.0.0", diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index 3b902e5cb8c75..49fff4b5c4f63 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -275,7 +275,7 @@ DatabaseSubnet3 |`ISOLATED`|`10.0.6.32/28`|#3|Only routes within the VPC ### Accessing the Internet Gateway -If you need access to the internet gateway, you can get it's ID like so: +If you need access to the internet gateway, you can get its ID like so: ```ts const igwId = vpc.internetGatewayId; diff --git a/packages/@aws-cdk/aws-ec2/lib/cfn-init-elements.ts b/packages/@aws-cdk/aws-ec2/lib/cfn-init-elements.ts index a190c17d7aa20..646d6b2dcbfa4 100644 --- a/packages/@aws-cdk/aws-ec2/lib/cfn-init-elements.ts +++ b/packages/@aws-cdk/aws-ec2/lib/cfn-init-elements.ts @@ -327,6 +327,9 @@ export abstract class InitFile extends InitElement { * Use a literal string as the file content */ public static fromString(fileName: string, content: string, options: InitFileOptions = {}): InitFile { + if (!content) { + throw new Error(`InitFile ${fileName}: cannot create empty file. Please supply at least one character of content.`); + } return new class extends InitFile { protected _doBind(bindOptions: InitBindOptions) { return { diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts index 197946716f969..2250bdd37c6c3 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts @@ -287,6 +287,7 @@ export class InterfaceVpcEndpointAwsService implements IInterfaceVpcEndpointServ public static readonly KMS = new InterfaceVpcEndpointAwsService('kms'); public static readonly CLOUDWATCH_LOGS = new InterfaceVpcEndpointAwsService('logs'); public static readonly CLOUDWATCH = new InterfaceVpcEndpointAwsService('monitoring'); + public static readonly RDS = new InterfaceVpcEndpointAwsService('rds'); public static readonly SAGEMAKER_API = new InterfaceVpcEndpointAwsService('sagemaker.api'); public static readonly SAGEMAKER_RUNTIME = new InterfaceVpcEndpointAwsService('sagemaker.runtime'); public static readonly SAGEMAKER_RUNTIME_FIPS = new InterfaceVpcEndpointAwsService('sagemaker.runtime-fips'); diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index 624577f547b62..801040d4c7385 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -233,6 +233,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.KINESIS_STREAMS", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.KINESIS_FIREHOSE", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.KMS", + "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.RDS", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.SAGEMAKER_API", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.SAGEMAKER_NOTEBOOK", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.SAGEMAKER_RUNTIME", diff --git a/packages/@aws-cdk/aws-ec2/test/cfn-init-element.test.ts b/packages/@aws-cdk/aws-ec2/test/cfn-init-element.test.ts index e794021c46b8f..75896912f3661 100644 --- a/packages/@aws-cdk/aws-ec2/test/cfn-init-element.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/cfn-init-element.test.ts @@ -206,6 +206,12 @@ describe('InitFile', () => { }); }); + test('empty content string throws error', () => { + expect(() => { + ec2.InitFile.fromString('/tmp/foo', ''); + }).toThrow('InitFile /tmp/foo: cannot create empty file. Please supply at least one character of content.'); + }); + test('symlink throws an error if mode is set incorrectly', () => { expect(() => { ec2.InitFile.symlink('/tmp/foo', '/tmp/bar', { diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index 83c05ba1ed308..3787903dedc3e 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -39,6 +39,15 @@ export interface IRepository extends IResource { */ repositoryUriForTag(tag?: string): string; + /** + * Returns the URI of the repository for a certain tag. Can be used in `docker push/pull`. + * + * ACCOUNT.dkr.ecr.REGION.amazonaws.com/REPOSITORY[@DIGEST] + * + * @param digest Image digest to use (tools usually default to the image with the "latest" tag if omitted) + */ + repositoryUriForDigest(digest?: string): string; + /** * Add a policy statement to the repository's resource policy */ @@ -136,8 +145,29 @@ export abstract class RepositoryBase extends Resource implements IRepository { */ public repositoryUriForTag(tag?: string): string { const tagSuffix = tag ? `:${tag}` : ''; + return this.repositoryUriWithSuffix(tagSuffix); + } + + /** + * Returns the URL of the repository. Can be used in `docker push/pull`. + * + * ACCOUNT.dkr.ecr.REGION.amazonaws.com/REPOSITORY[@DIGEST] + * + * @param digest Optional image digest + */ + public repositoryUriForDigest(digest?: string): string { + const digestSuffix = digest ? `@${digest}` : ''; + return this.repositoryUriWithSuffix(digestSuffix); + } + + /** + * Returns the repository URI, with an appended suffix, if provided. + * @param suffix An image tag or an image digest. + * @private + */ + private repositoryUriWithSuffix(suffix?: string): string { const parts = this.stack.parseArn(this.repositoryArn); - return `${parts.account}.dkr.ecr.${parts.region}.${this.stack.urlSuffix}/${this.repositoryName}${tagSuffix}`; + return `${parts.account}.dkr.ecr.${parts.region}.${this.stack.urlSuffix}/${this.repositoryName}${suffix}`; } /** diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 438f63e14e009..09a5ad13c14cd 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -67,7 +67,7 @@ one to run tasks on AWS Fargate. Here are the main differences: - **Amazon EC2**: instances are under your control. Complete control of task to host - allocation. Required to specify at least a memory reseration or limit for + allocation. Required to specify at least a memory reservation or limit for every container. Can use Host, Bridge and AwsVpc networking modes. Can attach Classic Load Balancer. Can share volumes between container and host. - **AWS Fargate**: tasks run on AWS-managed instances, AWS manages task to host @@ -175,7 +175,7 @@ cluster.addCapacity('AsgSpot', { When the `ecs.AddCapacityOptions` that you provide has a non-zero `taskDrainTime` (the default) then an SNS topic and Lambda are created to ensure that the cluster's instances have been properly drained of tasks before terminating. The SNS Topic is sent the instance-terminating lifecycle event from the AutoScalingGroup, and the Lambda acts on that event. If you wish to engage [server-side encryption](https://docs.aws.amazon.com/sns/latest/dg/sns-data-encryption.html) for this SNS Topic -then you may do so by providing a KMS key for the `topicEncryptionKey` propery of `ecs.AddCapacityOptions`. +then you may do so by providing a KMS key for the `topicEncryptionKey` property of `ecs.AddCapacityOptions`. ```ts // Given @@ -190,7 +190,7 @@ cluster.addCapacity('ASGEncryptedSNS', { ## Task definitions -A task Definition describes what a single copy of a **task** should look like. +A task definition describes what a single copy of a **task** should look like. A task definition has one or more containers; typically, it has one main container (the *default container* is the first one that's added to the task definition, and it is marked *essential*) and optionally @@ -237,23 +237,35 @@ const container = ec2TaskDefinition.addContainer("WebContainer", { You can specify container properties when you add them to the task definition, or with various methods, e.g.: +To add a port mapping when adding a container to the task definition, specify the `portMappings` option: + +```ts +taskDefinition.addContainer("WebContainer", { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + memoryLimitMiB: 1024, + portMappings: [{ containerPort: 3000 }] +}); +``` + +To add port mappings directly to a container definition, call `addPortMappings()`: + ```ts container.addPortMappings({ containerPort: 3000 -}) +}); ``` To add data volumes to a task definition, call `addVolume()`: ```ts -const volume = ecs.Volume("Volume", { +const volume = { // Use an Elastic FileSystem name: "mydatavolume", efsVolumeConfiguration: ecs.EfsVolumeConfiguration({ fileSystemId: "EFS" // ... other options here ... }) -}); +}; const container = fargateTaskDefinition.addVolume("mydatavolume"); ``` diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 983489743482f..9911a49a039cf 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -288,6 +288,12 @@ export interface ContainerDefinitionOptions { * @default - No GPUs assigned. */ readonly gpuCount?: number; + + /** + * The port mappings to add to the container definition. + * @default - No ports are mapped. + */ + readonly portMappings?: PortMapping[]; } /** @@ -433,6 +439,10 @@ export class ContainerDefinition extends CoreConstruct { } props.taskDefinition._linkContainer(this); + + if (props.portMappings) { + this.addPortMappings(...props.portMappings); + } } /** diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts index 18c6df350fb4e..4cf4de8a83292 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -229,9 +229,9 @@ export class Ec2Service extends BaseService implements IEc2Service { this.addPlacementConstraints(...props.placementConstraints || []); this.addPlacementStrategies(...props.placementStrategies || []); - if (!this.taskDefinition.defaultContainer) { - throw new Error('A TaskDefinition must have at least one essential container'); - } + this.node.addValidation({ + validate: () => !this.taskDefinition.defaultContainer ? ['A TaskDefinition must have at least one essential container'] : [], + }); } /** @@ -249,7 +249,7 @@ export class Ec2Service extends BaseService implements IEc2Service { } /** - * Adds one or more placement contstraints to use for tasks in the service. For more information, see + * Adds one or more placement constraints to use for tasks in the service. For more information, see * [Amazon ECS Task Placement Constraints](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-placement-constraints.html). */ public addPlacementConstraints(...constraints: PlacementConstraint[]) { diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index 1db94fc5286e0..793fb633e83d0 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -172,9 +172,9 @@ export class FargateService extends BaseService implements IFargateService { this.configureAwsVpcNetworkingWithSecurityGroups(props.cluster.vpc, props.assignPublicIp, props.vpcSubnets, securityGroups); - if (!props.taskDefinition.defaultContainer) { - throw new Error('A TaskDefinition must have at least one essential container'); - } + this.node.addValidation({ + validate: () => !this.taskDefinition.defaultContainer ? ['A TaskDefinition must have at least one essential container'] : [], + }); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/images/ecr.ts b/packages/@aws-cdk/aws-ecs/lib/images/ecr.ts index e8dc339bf9e82..b9786f13e2816 100644 --- a/packages/@aws-cdk/aws-ecs/lib/images/ecr.ts +++ b/packages/@aws-cdk/aws-ecs/lib/images/ecr.ts @@ -22,10 +22,14 @@ export class EcrImage extends ContainerImage { /** * Constructs a new instance of the EcrImage class. */ - constructor(private readonly repository: ecr.IRepository, private readonly tag: string) { + constructor(private readonly repository: ecr.IRepository, private readonly tagOrDigest: string) { super(); - this.imageName = this.repository.repositoryUriForTag(this.tag); + if (tagOrDigest?.startsWith('sha256:')) { + this.imageName = this.repository.repositoryUriForDigest(this.tagOrDigest); + } else { + this.imageName = this.repository.repositoryUriForTag(this.tagOrDigest); + } } public bind(_scope: CoreConstruct, containerDefinition: ContainerDefinition): ContainerImageConfig { diff --git a/packages/@aws-cdk/aws-ecs/test/container-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/container-definition.test.ts index 0e42a0aecc381..81e9f274160b7 100644 --- a/packages/@aws-cdk/aws-ecs/test/container-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/container-definition.test.ts @@ -703,6 +703,55 @@ describe('container definition', () => { }); + test('can add port mappings to the container definition by props', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + // WHEN + taskDefinition.addContainer('cont', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryLimitMiB: 1024, + portMappings: [{ containerPort: 80 }], + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + PortMappings: [{ ContainerPort: 80 }], + }, + ], + }); + }); + + test('can add port mappings using props and addPortMappings and both are included', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + // WHEN + const containerDefinition = taskDefinition.addContainer('cont', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryLimitMiB: 1024, + portMappings: [{ containerPort: 80 }], + }); + + containerDefinition.addPortMappings({ containerPort: 443 }); + + // THEN + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + PortMappings: [ + { ContainerPort: 80 }, + { ContainerPort: 443 }, + ], + }, + ], + }); + }); + describe('Environment Files', () => { describe('with EC2 task definitions', () => { test('can add asset environment file to the container definition', () => { diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts index 2279245aebaa4..d88c5ecafd1d7 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elb from '@aws-cdk/aws-elasticloadbalancing'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; @@ -541,14 +541,48 @@ nodeunitShim({ const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + // Errors on validation, not on construction. + new ecs.Ec2Service(stack, 'Ec2Service', { + cluster, + taskDefinition, + }); + // THEN test.throws(() => { - new ecs.Ec2Service(stack, 'Ec2Service', { - cluster, - taskDefinition, - }); + expect(stack); + }, /one essential container/); + + test.done(); + }, + + 'allows adding the default container after creating the service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + new ecs.Ec2Service(stack, 'FargateService', { + cluster, + taskDefinition, }); + // Add the container *after* creating the service + taskDefinition.addContainer('main', { + image: ecs.ContainerImage.fromRegistry('somecontainer'), + memoryReservationMiB: 10, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Name: 'main', + }, + ], + })); + test.done(); }, diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-task-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-task-definition.test.ts index 16128a96c6ed1..4f713b06d2d71 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-task-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-task-definition.test.ts @@ -531,6 +531,146 @@ describe('ec2 task definition', () => { }); + test('correctly sets containers from ECR repository using an image tag', () => { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, 'myECRImage'), 'myTag'), + memoryLimitMiB: 512, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 4, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.dkr.ecr.', + { + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/', + { + Ref: 'myECRImage7DEAE474', + }, + ':myTag', + ], + ], + }, + Name: 'web', + }], + }); + }); + + test('correctly sets containers from ECR repository using an image digest', () => { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, 'myECRImage'), 'sha256:94afd1f2e64d908bc90dbca0035a5b567EXAMPLE'), + memoryLimitMiB: 512, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 4, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.dkr.ecr.', + { + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/', + { + Ref: 'myECRImage7DEAE474', + }, + '@sha256:94afd1f2e64d908bc90dbca0035a5b567EXAMPLE', + ], + ], + }, + Name: 'web', + }], + }); + }); + test('correctly sets containers from ECR repository using default props', () => { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts index acddf713d6d0a..c7aa1fc633a1d 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts @@ -482,14 +482,46 @@ nodeunitShim({ const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + // Errors on validation, not on construction. + new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); + // THEN test.throws(() => { - new ecs.FargateService(stack, 'FargateService', { - cluster, - taskDefinition, - }); + expect(stack); + }, /one essential container/); + + test.done(); + }, + + 'allows adding the default container after creating the service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + + new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, }); + // Add the container *after* creating the service + taskDefinition.addContainer('main', { + image: ecs.ContainerImage.fromRegistry('somecontainer'), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Name: 'main', + }, + ], + })); + test.done(); }, diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts index d333fd9df89e2..e2d40e4ef52f6 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts @@ -15,13 +15,12 @@ const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { cpu: 512, }); -const container = taskDefinition.addContainer('web', { +taskDefinition.addContainer('web', { image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), -}); - -container.addPortMappings({ - containerPort: 80, - protocol: ecs.Protocol.TCP, + portMappings: [{ + containerPort: 80, + protocol: ecs.Protocol.TCP, + }], }); const service = new ecs.FargateService(stack, 'Service', { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index 1844314e1f560..b72151f81f2f8 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -263,10 +263,14 @@ export class ApplicationListener extends BaseListener implements IApplicationLis this.certificateArns.push(first.certificateArn); } - if (additionalCerts.length > 0) { - new ApplicationListenerCertificate(this, id, { + // Only one certificate can be specified per resource, even though + // `certificates` is of type Array + for (let i = 0; i < additionalCerts.length; i++) { + // ids should look like: `id`, `id2`, `id3` (for backwards-compatibility) + const certId = (i > 0) ? `${id}${i + 1}` : id; + new ApplicationListenerCertificate(this, certId, { listener: this, - certificates: additionalCerts, + certificates: [additionalCerts[i]], }); } } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts index 3e7b639cb1a8f..b6e379a17e463 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts @@ -143,6 +143,43 @@ describe('tests', () => { }); }); + test('HTTPS listener can add more than two certificates', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + + // WHEN + const listener = lb.addListener('Listener', { + port: 443, + defaultTargetGroups: [ + new elbv2.ApplicationTargetGroup(stack, 'Group', { vpc, port: 80 }), + ], + certificates: [ + elbv2.ListenerCertificate.fromArn('cert1'), + elbv2.ListenerCertificate.fromArn('cert2'), + elbv2.ListenerCertificate.fromArn('cert3'), + ], + }); + + expect(listener.node.tryFindChild('DefaultCertificates')).toBeDefined(); + expect(listener.node.tryFindChild('DefaultCertificates2')).toBeDefined(); + expect(listener.node.tryFindChild('DefaultCertificates3')).not.toBeDefined(); + + // THEN + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Certificates: [{ CertificateArn: 'cert1' }], + }); + + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { + Certificates: [{ CertificateArn: 'cert2' }], + }); + + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { + Certificates: [{ CertificateArn: 'cert3' }], + }); + }); + test('Can configure targetType on TargetGroups', () => { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-events-targets/README.md b/packages/@aws-cdk/aws-events-targets/README.md index 787bfcc433d30..cd843ef130a3a 100644 --- a/packages/@aws-cdk/aws-events-targets/README.md +++ b/packages/@aws-cdk/aws-events-targets/README.md @@ -27,11 +27,45 @@ Currently supported are: * Put a record to a Kinesis stream * Log an event into a LogGroup * Put a record to a Kinesis Data Firehose stream +* Put an event on an EventBridge bus See the README of the `@aws-cdk/aws-events` library for more information on EventBridge. -## LogGroup +## Invoke a Lambda function + +Use the `LambdaFunction` target to invoke a lambda function. + +The code snippet below creates an event rule with a Lambda function as a target +triggered for every events from `aws.ec2` source. You can optionally attach a +[dead letter queue](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html). + +```ts +import * as lambda from "@aws-cdk/aws-lambda"; +import * as events from "@aws-cdk/aws-events"; +import * as sqs from "@aws-cdk/aws-sqs"; +import * as targets from "@aws-cdk/aws-events-targets"; + +const fn = new lambda.Function(this, 'MyFunc', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.fromInline(`exports.handler = ${handler.toString()}`), +}); + +const rule = new events.Rule(this, 'rule', { + eventPattern: { + source: ["aws.ec2"], + }, +}); + +const queue = new sqs.Queue(this, 'Queue'); + +rule.addTarget(new targets.LambdaFunction(fn, { + deadLetterQueue: queue, // Optional: add a dead letter queue +})); +``` + +## Log an event into a LogGroup Use the `LogGroup` target to log your events in a CloudWatch LogGroup. diff --git a/packages/@aws-cdk/aws-events-targets/lib/event-bus.ts b/packages/@aws-cdk/aws-events-targets/lib/event-bus.ts new file mode 100644 index 0000000000000..1d07261a8eace --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/lib/event-bus.ts @@ -0,0 +1,45 @@ +import * as events from '@aws-cdk/aws-events'; +import * as iam from '@aws-cdk/aws-iam'; +import { singletonEventRole } from './util'; + +/** + * Configuration properties of an Event Bus event + */ +export interface EventBusProps { + /** + * Role to be used to publish the event + * + * @default a new role is created. + */ + readonly role?: iam.IRole; +} + +/** + * Notify an existing Event Bus of an event + */ +export class EventBus implements events.IRuleTarget { + private readonly role?: iam.IRole; + + constructor(private readonly eventBus: events.IEventBus, props: EventBusProps = {}) { + this.role = props.role; + } + + bind(rule: events.IRule, id?: string): events.RuleTargetConfig { + if (this.role) { + this.role.addToPrincipalPolicy(this.putEventStatement()); + } + const role = this.role ?? singletonEventRole(rule, [this.putEventStatement()]); + return { + id: id ?? '', + arn: this.eventBus.eventBusArn, + role, + }; + } + + private putEventStatement() { + return new iam.PolicyStatement({ + actions: ['events:PutEvents'], + resources: [this.eventBus.eventBusArn], + }); + } +} diff --git a/packages/@aws-cdk/aws-events-targets/lib/index.ts b/packages/@aws-cdk/aws-events-targets/lib/index.ts index bef8ce2463ffa..155791c195d1e 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/index.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/index.ts @@ -7,6 +7,7 @@ export * from './aws-api'; export * from './lambda'; export * from './ecs-task-properties'; export * from './ecs-task'; +export * from './event-bus'; export * from './state-machine'; export * from './kinesis-stream'; export * from './log-group'; diff --git a/packages/@aws-cdk/aws-events-targets/lib/lambda.ts b/packages/@aws-cdk/aws-events-targets/lib/lambda.ts index 780cd6d57162c..44315579a1300 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/lambda.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/lambda.ts @@ -1,6 +1,7 @@ import * as events from '@aws-cdk/aws-events'; import * as lambda from '@aws-cdk/aws-lambda'; -import { addLambdaPermission } from './util'; +import * as sqs from '@aws-cdk/aws-sqs'; +import { addLambdaPermission, addToDeadLetterQueueResourcePolicy } from './util'; /** * Customize the Lambda Event Target @@ -14,6 +15,18 @@ export interface LambdaFunctionProps { * @default the entire EventBridge event */ readonly event?: events.RuleTargetInput; + + /** + * The SQS queue to be used as deadLetterQueue. + * Check out the [considerations for using a dead-letter queue](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html#dlq-considerations). + * + * The events not successfully delivered are automatically retried for a specified period of time, + * depending on the retry policy of the target. + * If an event is not delivered before all retry attempts are exhausted, it will be sent to the dead letter queue. + * + * @default - no dead-letter queue + */ + readonly deadLetterQueue?: sqs.IQueue; } /** @@ -32,9 +45,14 @@ export class LambdaFunction implements events.IRuleTarget { // Allow handler to be called from rule addLambdaPermission(rule, this.handler); + if (this.props.deadLetterQueue) { + addToDeadLetterQueueResourcePolicy(rule, this.props.deadLetterQueue); + } + return { id: '', arn: this.handler.functionArn, + deadLetterConfig: this.props.deadLetterQueue ? { arn: this.props.deadLetterQueue?.queueArn } : undefined, input: this.props.event, targetResource: this.handler, }; diff --git a/packages/@aws-cdk/aws-events-targets/lib/util.ts b/packages/@aws-cdk/aws-events-targets/lib/util.ts index 1026d1ae35a1a..069b04a8c5131 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/util.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/util.ts @@ -1,7 +1,8 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; -import { ConstructNode, IConstruct, Names } from '@aws-cdk/core'; +import * as sqs from '@aws-cdk/aws-sqs'; +import { Annotations, ConstructNode, IConstruct, Names, Token, TokenComparison } from '@aws-cdk/core'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main // eslint-disable-next-line no-duplicate-imports, import/order @@ -50,3 +51,45 @@ export function addLambdaPermission(rule: events.IRule, handler: lambda.IFunctio }); } } + +/** + * Allow a rule to send events with failed invocation to an Amazon SQS queue. + */ +export function addToDeadLetterQueueResourcePolicy(rule: events.IRule, queue: sqs.IQueue) { + if (!sameEnvDimension(rule.env.region, queue.env.region)) { + throw new Error(`Cannot assign Dead Letter Queue in region ${queue.env.region} to the rule ${Names.nodeUniqueId(rule.node)} in region ${rule.env.region}. Both the queue and the rule must be in the same region.`); + } + + // Skip Resource Policy creation if the Queue is not in the same account. + // There is no way to add a target onto an imported rule, so we can assume we will run the following code only + // in the account where the rule is created. + if (sameEnvDimension(rule.env.account, queue.env.account)) { + const policyStatementId = `AllowEventRule${Names.nodeUniqueId(rule.node)}`; + + queue.addToResourcePolicy(new iam.PolicyStatement({ + sid: policyStatementId, + principals: [new iam.ServicePrincipal('events.amazonaws.com')], + effect: iam.Effect.ALLOW, + actions: ['sqs:SendMessage'], + resources: [queue.queueArn], + conditions: { + ArnEquals: { + 'aws:SourceArn': rule.ruleArn, + }, + }, + })); + } else { + Annotations.of(rule).addWarning(`Cannot add a resource policy to your dead letter queue associated with rule ${rule.ruleName} because the queue is in a different account. You must add the resource policy manually to the dead letter queue in account ${queue.env.account}.`); + } +} + + +/** + * Whether two string probably contain the same environment dimension (region or account) + * + * Used to compare either accounts or regions, and also returns true if both + * are unresolved (in which case both are expted to be "current region" or "current account"). + */ +function sameEnvDimension(dim1: string, dim2: string) { + return [TokenComparison.SAME, TokenComparison.BOTH_UNRESOLVED].includes(Token.compareStrings(dim1, dim2)); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/event-bus/event-rule-target.test.ts b/packages/@aws-cdk/aws-events-targets/test/event-bus/event-rule-target.test.ts new file mode 100644 index 0000000000000..4c39a907210d7 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/event-bus/event-rule-target.test.ts @@ -0,0 +1,93 @@ +import '@aws-cdk/assert/jest'; +import * as events from '@aws-cdk/aws-events'; +import * as iam from '@aws-cdk/aws-iam'; +import { Stack } from '@aws-cdk/core'; +import * as targets from '../../lib'; + +test('Use EventBus as an event rule target', () => { + const stack = new Stack(); + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + + rule.addTarget(new targets.EventBus(events.EventBus.fromEventBusArn( + stack, + 'External', + 'arn:aws:events:us-east-1:111111111111:default', + ), + )); + + expect(stack).toHaveResource('AWS::Events::Rule', { + Targets: [ + { + Arn: 'arn:aws:events:us-east-1:111111111111:default', + Id: 'Target0', + RoleArn: { + 'Fn::GetAtt': [ + 'RuleEventsRoleC51A4248', + 'Arn', + ], + }, + }, + ], + }); + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Effect: 'Allow', + Action: 'events:PutEvents', + Resource: 'arn:aws:events:us-east-1:111111111111:default', + }], + Version: '2012-10-17', + }, + Roles: [{ + Ref: 'RuleEventsRoleC51A4248', + }], + }); +}); + +test('with supplied role', () => { + const stack = new Stack(); + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('events.amazonaws.com'), + roleName: 'GivenRole', + }); + + rule.addTarget(new targets.EventBus( + events.EventBus.fromEventBusArn( + stack, + 'External', + 'arn:aws:events:us-east-1:123456789012:default', + ), + { role }, + )); + + expect(stack).toHaveResource('AWS::Events::Rule', { + Targets: [{ + Arn: 'arn:aws:events:us-east-1:123456789012:default', + Id: 'Target0', + RoleArn: { + 'Fn::GetAtt': [ + 'Role1ABCC5F0', + 'Arn', + ], + }, + }], + }); + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Effect: 'Allow', + Action: 'events:PutEvents', + Resource: 'arn:aws:events:us-east-1:123456789012:default', + }], + Version: '2012-10-17', + }, + Roles: [{ + Ref: 'Role1ABCC5F0', + }], + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/event-bus/integ.event-bus.expected.json b/packages/@aws-cdk/aws-events-targets/test/event-bus/integ.event-bus.expected.json new file mode 100644 index 0000000000000..632ddf1767598 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/event-bus/integ.event-bus.expected.json @@ -0,0 +1,83 @@ +{ + "Resources": { + "Rule4C995B7F": { + "Type": "AWS::Events::Rule", + "Properties": { + "ScheduleExpression": "rate(1 minute)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::Join": [ + "", + [ + "arn:aws:events:", + { + "Ref": "AWS::Region" + }, + ":999999999999:event-bus/test-bus" + ] + ] + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "RuleEventsRoleC51A4248", + "Arn" + ] + } + } + ] + } + }, + "RuleEventsRoleC51A4248": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "RuleEventsRoleDefaultPolicy0510525D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "events:PutEvents", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:events:", + { + "Ref": "AWS::Region" + }, + ":999999999999:event-bus/test-bus" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RuleEventsRoleDefaultPolicy0510525D", + "Roles": [ + { + "Ref": "RuleEventsRoleC51A4248" + } + ] + } + } + } +} diff --git a/packages/@aws-cdk/aws-events-targets/test/event-bus/integ.event-bus.ts b/packages/@aws-cdk/aws-events-targets/test/event-bus/integ.event-bus.ts new file mode 100644 index 0000000000000..c0ec2ea421b85 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/event-bus/integ.event-bus.ts @@ -0,0 +1,26 @@ +/// !cdk-integ pragma:ignore-assets +import * as events from '@aws-cdk/aws-events'; +import * as cdk from '@aws-cdk/core'; +import * as targets from '../../lib'; + +const app = new cdk.App(); + +class EventSourceStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const rule = new events.Rule(this, 'Rule', { + schedule: events.Schedule.expression('rate(1 minute)'), + }); + rule.addTarget(new targets.EventBus( + events.EventBus.fromEventBusArn( + this, + 'External', + `arn:aws:events:${this.region}:999999999999:event-bus/test-bus`, + ), + )); + } +} + +new EventSourceStack(app, 'event-source-stack'); +app.synth(); diff --git a/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.expected.json b/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.expected.json index c12d92ef34810..7df2a4becf021 100644 --- a/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.expected.json @@ -123,6 +123,95 @@ ] } } + }, + "Timer30894E3BB": { + "Type": "AWS::Events::Rule", + "Properties": { + "ScheduleExpression": "rate(2 minutes)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "MyFunc8A243A2C", + "Arn" + ] + }, + "DeadLetterConfig": { + "Arn": { + "Fn::GetAtt": [ + "Queue4A7E3555", + "Arn" + ] + } + }, + "Id": "Target0" + } + ] + } + }, + "Timer3AllowEventRulelambdaeventsMyFunc910E580F79317F73": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyFunc8A243A2C", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "Timer30894E3BB", + "Arn" + ] + } + } + }, + "Queue4A7E3555": { + "Type": "AWS::SQS::Queue", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "QueuePolicy25439813": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Fn::GetAtt": [ + "Timer30894E3BB", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "Queue4A7E3555", + "Arn" + ] + }, + "Sid": "AllowEventRulelambdaeventsTimer3107B9373" + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "Queue4A7E3555" + } + ] + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.ts b/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.ts index 17d17a291e244..c37c632d803ae 100644 --- a/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.ts +++ b/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.ts @@ -1,5 +1,6 @@ import * as events from '@aws-cdk/aws-events'; import * as lambda from '@aws-cdk/aws-lambda'; +import * as sqs from '@aws-cdk/aws-sqs'; import * as cdk from '@aws-cdk/core'; import * as targets from '../../lib'; @@ -23,6 +24,17 @@ const timer2 = new events.Rule(stack, 'Timer2', { }); timer2.addTarget(new targets.LambdaFunction(fn)); + +const timer3 = new events.Rule(stack, 'Timer3', { + schedule: events.Schedule.rate(cdk.Duration.minutes(2)), +}); + +const queue = new sqs.Queue(stack, 'Queue'); + +timer3.addTarget(new targets.LambdaFunction(fn, { + deadLetterQueue: queue, +})); + app.synth(); /* eslint-disable no-console */ diff --git a/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts b/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts index 29be8973fe3c3..c5fd260715293 100644 --- a/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts @@ -1,6 +1,7 @@ import '@aws-cdk/assert/jest'; import * as events from '@aws-cdk/aws-events'; import * as lambda from '@aws-cdk/aws-lambda'; +import * as sqs from '@aws-cdk/aws-sqs'; import * as cdk from '@aws-cdk/core'; import * as constructs from 'constructs'; import * as targets from '../../lib'; @@ -147,6 +148,183 @@ test('lambda handler and cloudwatch event across stacks', () => { expect(eventStack).toCountResources('AWS::Lambda::Permission', 1); }); +test('use a Dead Letter Queue for the rule target', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + + const fn = new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'bar', + runtime: lambda.Runtime.PYTHON_2_7, + }); + + const queue = new sqs.Queue(stack, 'Queue'); + + new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + targets: [new targets.LambdaFunction(fn, { + deadLetterQueue: queue, + })], + }); + + expect(() => app.synth()).not.toThrow(); + + // the Permission resource should be in the event stack + expect(stack).toHaveResource('AWS::Events::Rule', { + Targets: [ + { + Arn: { + 'Fn::GetAtt': [ + 'MyLambdaCCE802FB', + 'Arn', + ], + }, + DeadLetterConfig: { + Arn: { + 'Fn::GetAtt': [ + 'Queue4A7E3555', + 'Arn', + ], + }, + }, + Id: 'Target0', + }, + ], + }); + + expect(stack).toHaveResource('AWS::SQS::QueuePolicy', { + PolicyDocument: { + Statement: [ + { + Action: 'sqs:SendMessage', + Condition: { + ArnEquals: { + 'aws:SourceArn': { + 'Fn::GetAtt': [ + 'Rule4C995B7F', + 'Arn', + ], + }, + }, + }, + Effect: 'Allow', + Principal: { + Service: 'events.amazonaws.com', + }, + Resource: { + 'Fn::GetAtt': [ + 'Queue4A7E3555', + 'Arn', + ], + }, + Sid: 'AllowEventRuleStackRuleF6E31DD0', + }, + ], + Version: '2012-10-17', + }, + Queues: [ + { + Ref: 'Queue4A7E3555', + }, + ], + }); +}); + +test('throw an error when using a Dead Letter Queue for the rule target in a different region', () => { + // GIVEN + const app = new cdk.App(); + const stack1 = new cdk.Stack(app, 'Stack1', { + env: { + region: 'eu-west-1', + }, + }); + const stack2 = new cdk.Stack(app, 'Stack2', { + env: { + region: 'eu-west-2', + }, + }); + + const fn = new lambda.Function(stack1, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'bar', + runtime: lambda.Runtime.PYTHON_2_7, + }); + + const queue = new sqs.Queue(stack2, 'Queue'); + + let rule = new events.Rule(stack1, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + }); + + + expect(() => { + rule.addTarget(new targets.LambdaFunction(fn, { + deadLetterQueue: queue, + })); + }).toThrow(/Cannot assign Dead Letter Queue in region eu-west-2 to the rule Stack1Rule92BA1111 in region eu-west-1. Both the queue and the rule must be in the same region./); +}); + +test('must display a warning when using a Dead Letter Queue from another account', () => { + // GIVEN + const app = new cdk.App(); + const stack1 = new cdk.Stack(app, 'Stack1', { + env: { + region: 'eu-west-1', + account: '111111111111', + }, + }); + + const stack2 = new cdk.Stack(app, 'Stack2', { + env: { + region: 'eu-west-1', + account: '222222222222', + }, + }); + + const fn = new lambda.Function(stack1, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'bar', + runtime: lambda.Runtime.PYTHON_2_7, + }); + + const queue = sqs.Queue.fromQueueArn(stack2, 'Queue', 'arn:aws:sqs:eu-west-1:444455556666:queue1'); + + new events.Rule(stack1, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + targets: [new targets.LambdaFunction(fn, { + deadLetterQueue: queue, + })], + }); + + expect(() => app.synth()).not.toThrow(); + + // the Permission resource should be in the event stack + expect(stack1).toHaveResource('AWS::Events::Rule', { + ScheduleExpression: 'rate(1 minute)', + State: 'ENABLED', + Targets: [ + { + Arn: { + 'Fn::GetAtt': [ + 'MyLambdaCCE802FB', + 'Arn', + ], + }, + DeadLetterConfig: { + Arn: 'arn:aws:sqs:eu-west-1:444455556666:queue1', + }, + Id: 'Target0', + }, + ], + }); + + expect(stack1).not.toHaveResource('AWS::SQS::QueuePolicy'); + + let rule = stack1.node.children.find(child => child instanceof events.Rule); + expect(rule?.node.metadata[0].data).toMatch(/Cannot add a resource policy to your dead letter queue associated with rule .* because the queue is in a different account\. You must add the resource policy manually to the dead letter queue in account 222222222222\./); +}); + function newTestLambda(scope: constructs.Construct, suffix = '') { return new lambda.Function(scope, `MyLambda${suffix}`, { code: new lambda.InlineCode('foo'), diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index 969965021f75d..2c71054b60828 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -295,6 +295,7 @@ export class Rule extends Resource implements IRule { kinesisParameters: targetProps.kinesisParameters, runCommandParameters: targetProps.runCommandParameters, batchParameters: targetProps.batchParameters, + deadLetterConfig: targetProps.deadLetterConfig, sqsParameters: targetProps.sqsParameters, input: inputProps && inputProps.input, inputPath: inputProps && inputProps.inputPath, diff --git a/packages/@aws-cdk/aws-events/lib/target.ts b/packages/@aws-cdk/aws-events/lib/target.ts index 319cf3d4f14da..da2ac9a1c7fb1 100644 --- a/packages/@aws-cdk/aws-events/lib/target.ts +++ b/packages/@aws-cdk/aws-events/lib/target.ts @@ -47,6 +47,12 @@ export interface RuleTargetConfig { */ readonly batchParameters?: CfnRule.BatchParametersProperty; + /** + * Contains information about a dead-letter queue configuration. + * @default no dead-letter queue set + */ + readonly deadLetterConfig?: CfnRule.DeadLetterConfigProperty; + /** * The Amazon ECS task definition and task count to use, if the event target * is an Amazon ECS task. diff --git a/packages/@aws-cdk/aws-iam/lib/oidc-provider.ts b/packages/@aws-cdk/aws-iam/lib/oidc-provider.ts index 91c221ec55652..ec70c6d152cbe 100644 --- a/packages/@aws-cdk/aws-iam/lib/oidc-provider.ts +++ b/packages/@aws-cdk/aws-iam/lib/oidc-provider.ts @@ -155,7 +155,7 @@ export class OpenIdConnectProvider extends Resource implements IOpenIdConnectPro private getOrCreateProvider() { return CustomResourceProvider.getOrCreate(this, RESOURCE_TYPE, { codeDirectory: path.join(__dirname, 'oidc-provider'), - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, policyStatements: [ { Effect: 'Allow', diff --git a/packages/@aws-cdk/aws-lambda-destinations/README.md b/packages/@aws-cdk/aws-lambda-destinations/README.md index 8459820e9686e..404b0b3157adb 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/README.md +++ b/packages/@aws-cdk/aws-lambda-destinations/README.md @@ -21,7 +21,7 @@ The following destinations are supported * SNS topic * EventBridge event bus -Example with a SNS topic for sucessful invocations: +Example with a SNS topic for successful invocations: ```ts import * as lambda from '@aws-cdk/aws-lambda'; diff --git a/packages/@aws-cdk/aws-lambda-nodejs/README.md b/packages/@aws-cdk/aws-lambda-nodejs/README.md index f35062120fb9d..d11602c5e656e 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/README.md +++ b/packages/@aws-cdk/aws-lambda-nodejs/README.md @@ -148,7 +148,7 @@ new lambda.NodejsFunction(this, 'my-handler', { }, logLevel: LogLevel.SILENT, // defaults to LogLevel.WARNING keepNames: true, // defaults to false - tsconfig: 'custom-tsconfig.json' // use custom-tsconfig.json instead of default, + tsconfig: 'custom-tsconfig.json' // use custom-tsconfig.json instead of default, metafile: true, // include meta file, defaults to false banner : '/* comments */', // by default no comments are passed footer : '/* comments */', // by default no comments are passed @@ -220,7 +220,7 @@ Use `bundling.dockerImage` to use a custom Docker bundling image: ```ts new lambda.NodejsFunction(this, 'my-handler', { bundling: { - dockerImage: cdk.BundlingDockerImage.fromAsset('/path/to/Dockerfile'), + dockerImage: cdk.DockerImage.fromBuild('/path/to/Dockerfile'), }, }); ``` diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts index 5051fc9012ada..536ca1ea7646a 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts @@ -140,10 +140,10 @@ export class Bundling implements cdk.BundlingOptions { const esbuildCommand: string = [ npx, 'esbuild', - '--bundle', pathJoin(inputDir, this.relativeEntryPath).replace(/(\s+)/g, '\\$1'), + '--bundle', `"${pathJoin(inputDir, this.relativeEntryPath)}"`, `--target=${this.props.target ?? toTarget(this.props.runtime)}`, '--platform=node', - `--outfile=${pathJoin(outputDir, 'index.js')}`, + `--outfile="${pathJoin(outputDir, 'index.js')}"`, ...this.props.minify ? ['--minify'] : [], ...this.props.sourceMap ? ['--sourcemap'] : [], ...this.externals.map(external => `--external:${external}`), diff --git a/packages/@aws-cdk/aws-lambda-nodejs/package.json b/packages/@aws-cdk/aws-lambda-nodejs/package.json index c2e2f658febac..7ae4a7822c113 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/package.json +++ b/packages/@aws-cdk/aws-lambda-nodejs/package.json @@ -67,7 +67,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "delay": "5.0.0", - "esbuild": "^0.8.50", + "esbuild": "^0.8.54", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts index 2f96c10ff78ed..e6c32c496b2ff 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts @@ -20,7 +20,7 @@ beforeEach(() => { getEsBuildVersionMock.mockReturnValue('0.8.8'); fromAssetMock.mockReturnValue({ image: 'built-image', - cp: () => {}, + cp: () => 'dest-path', run: () => {}, toJSON: () => 'built-image', }); @@ -53,7 +53,7 @@ test('esbuild bundling in Docker', () => { }, command: [ 'bash', '-c', - 'npx esbuild --bundle /asset-input/lib/handler.ts --target=node12 --platform=node --outfile=/asset-output/index.js --external:aws-sdk --loader:.png=dataurl', + 'npx esbuild --bundle "/asset-input/lib/handler.ts" --target=node12 --platform=node --outfile="/asset-output/index.js" --external:aws-sdk --loader:.png=dataurl', ], workingDirectory: '/', }), @@ -74,7 +74,7 @@ test('esbuild bundling with handler named index.ts', () => { bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'npx esbuild --bundle /asset-input/lib/index.ts --target=node12 --platform=node --outfile=/asset-output/index.js --external:aws-sdk', + 'npx esbuild --bundle "/asset-input/lib/index.ts" --target=node12 --platform=node --outfile="/asset-output/index.js" --external:aws-sdk', ], }), }); @@ -94,7 +94,7 @@ test('esbuild bundling with tsx handler', () => { bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'npx esbuild --bundle /asset-input/lib/handler.tsx --target=node12 --platform=node --outfile=/asset-output/index.js --external:aws-sdk', + 'npx esbuild --bundle "/asset-input/lib/handler.tsx" --target=node12 --platform=node --outfile="/asset-output/index.js" --external:aws-sdk', ], }), }); @@ -139,7 +139,7 @@ test('esbuild bundling with externals and dependencies', () => { command: [ 'bash', '-c', [ - 'npx esbuild --bundle /asset-input/test/bundling.test.js --target=node12 --platform=node --outfile=/asset-output/index.js --external:abc --external:delay', + 'npx esbuild --bundle "/asset-input/test/bundling.test.js" --target=node12 --platform=node --outfile="/asset-output/index.js" --external:abc --external:delay', `echo \'{\"dependencies\":{\"delay\":\"${delayVersion}\"}}\' > /asset-output/package.json`, 'cp /asset-input/package-lock.json /asset-output/package-lock.json', 'cd /asset-output', @@ -181,8 +181,8 @@ test('esbuild bundling with esbuild options', () => { command: [ 'bash', '-c', [ - 'npx esbuild --bundle /asset-input/lib/handler.ts', - '--target=es2020 --platform=node --outfile=/asset-output/index.js', + 'npx esbuild --bundle "/asset-input/lib/handler.ts"', + '--target=es2020 --platform=node --outfile="/asset-output/index.js"', '--minify --sourcemap --external:aws-sdk --loader:.png=dataurl', '--define:DEBUG=true --define:process.env.KEY="VALUE"', '--log-level=silent --keep-names --tsconfig=/asset-input/lib/custom-tsconfig.ts', @@ -334,23 +334,3 @@ test('with command hooks', () => { }), }); }); - -test('escapes spaces in path', () => { - Bundling.bundle({ - entry: '/project/lib/my cool lambda/handler.ts', - depsLockFilePath, - runtime: Runtime.NODEJS_12_X, - forceDockerBundling: true, - }); - - // Correctly bundles with esbuild - expect(Code.fromAsset).toHaveBeenCalledWith(path.dirname(depsLockFilePath), { - assetHashType: AssetHashType.OUTPUT, - bundling: expect.objectContaining({ - command: [ - 'bash', '-c', - expect.stringContaining('lib/my\\ cool\\ lambda/handler.ts'), - ], - }), - }); -}); diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index 569569f526736..ae21d6ea9216e 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -36,6 +36,9 @@ runtime code. * `lambda.Code.fromAsset(path)` - specify a directory or a .zip file in the local filesystem which will be zipped and uploaded to S3 before deployment. See also [bundling asset code](#bundling-asset-code). + * `lambda.Code.fromDockerBuild(path, options)` - use the result of a Docker + build as code. The runtime code is expected to be located at `/asset` in the + image and will be zipped and uploaded to S3 as an asset. The following example shows how to define a Python function and deploy the code from the local directory `my-lambda-handler` to it: @@ -450,7 +453,7 @@ new lambda.Function(this, 'Function', { bundling: { image: lambda.Runtime.PYTHON_3_6.bundlingDockerImage, command: [ - 'bash', '-c', + 'bash', '-c', 'pip install -r requirements.txt -t /asset-output && cp -au . /asset-output' ], }, @@ -462,8 +465,8 @@ new lambda.Function(this, 'Function', { 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: +Use `cdk.DockerImage.fromRegistry(image)` to use an existing image or +`cdk.DockerImage.fromBuild(path)` to build a specific image: ```ts import * as cdk from '@aws-cdk/core'; @@ -471,7 +474,7 @@ import * as cdk from '@aws-cdk/core'; new lambda.Function(this, 'Function', { code: lambda.Code.fromAsset('/path/to/handler', { bundling: { - image: cdk.BundlingDockerImage.fromAsset('/path/to/dir/with/DockerFile', { + image: cdk.DockerImage.fromBuild('/path/to/dir/with/DockerFile', { buildArgs: { ARG1: 'value1', }, diff --git a/packages/@aws-cdk/aws-lambda/lib/code.ts b/packages/@aws-cdk/aws-lambda/lib/code.ts index 29cd3d02ae4de..b4f41b2804257 100644 --- a/packages/@aws-cdk/aws-lambda/lib/code.ts +++ b/packages/@aws-cdk/aws-lambda/lib/code.ts @@ -57,6 +57,22 @@ export abstract class Code { return new AssetCode(path, options); } + /** + * Loads the function code from an asset created by a Docker build. + * + * By defaut, the asset is expected to be located at `/asset` in the + * image. + * + * @param path The path to the directory containing the Docker file + * @param options Docker build options + */ + public static fromDockerBuild(path: string, options: DockerBuildAssetOptions = {}): AssetCode { + const assetPath = cdk.DockerImage + .fromBuild(path, options) + .cp(options.imagePath ?? '/asset', options.outputPath); + return new AssetCode(assetPath); + } + /** * DEPRECATED * @deprecated use `fromAsset` @@ -488,3 +504,24 @@ export class AssetImageCode extends Code { }; } } + +/** + * Options when creating an asset from a Docker build. + */ +export interface DockerBuildAssetOptions extends cdk.DockerBuildOptions { + /** + * The path in the Docker image where the asset is located after the build + * operation. + * + * @default /asset + */ + readonly imagePath?: string; + + /** + * The path on the local filesystem where the asset will be copied + * using `docker cp`. + * + * @default - a unique temporary directory in the system temp directory + */ + readonly outputPath?: string; +} diff --git a/packages/@aws-cdk/aws-lambda/test/code.test.ts b/packages/@aws-cdk/aws-lambda/test/code.test.ts index abdbc0e080daa..91de07a17c5a6 100644 --- a/packages/@aws-cdk/aws-lambda/test/code.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/code.test.ts @@ -329,6 +329,29 @@ describe('code', () => { }); }); }); + + describe('lambda.Code.fromDockerBuild', () => { + test('can use the result of a Docker build as an asset', () => { + // given + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true); + + // when + new lambda.Function(stack, 'Fn', { + code: lambda.Code.fromDockerBuild(path.join(__dirname, 'docker-build-lambda')), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_12_X, + }); + + // then + expect(stack).toHaveResource('AWS::Lambda::Function', { + Metadata: { + [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.38cd320fa97b348accac88e48d9cede4923f7cab270ce794c95a665be83681a8', + [cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'Code', + }, + }, ResourcePart.CompleteDefinition); + }); + }); }); function defineFunction(code: lambda.Code, runtime: lambda.Runtime = lambda.Runtime.NODEJS_10_X) { diff --git a/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/Dockerfile b/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/Dockerfile new file mode 100644 index 0000000000000..4643fde141850 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/Dockerfile @@ -0,0 +1,3 @@ +FROM public.ecr.aws/amazonlinux/amazonlinux:latest + +COPY index.js /asset diff --git a/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/index.ts b/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/index.ts new file mode 100644 index 0000000000000..cc867895b4efc --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/index.ts @@ -0,0 +1,5 @@ +/* eslint-disable no-console */ +export async function handler(event: any) { + console.log('Event: %j', event); + return event; +} diff --git a/packages/@aws-cdk/aws-logs/package.json b/packages/@aws-cdk/aws-logs/package.json index f5a004e27bb1f..c1b3eef36c73b 100644 --- a/packages/@aws-cdk/aws-logs/package.json +++ b/packages/@aws-cdk/aws-logs/package.json @@ -77,7 +77,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nock": "^13.0.7", + "nock": "^13.0.9", "nodeunit": "^0.11.3", "pkglint": "0.0.0", "sinon": "^9.2.4" diff --git a/packages/@aws-cdk/aws-neptune/README.md b/packages/@aws-cdk/aws-neptune/README.md index 6b2eddde67362..fc542acf1b3da 100644 --- a/packages/@aws-cdk/aws-neptune/README.md +++ b/packages/@aws-cdk/aws-neptune/README.md @@ -9,10 +9,101 @@ > > [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + --- -```ts +Amazon Neptune is a fast, reliable, fully managed graph database service that makes it easy to build and run applications that work with highly connected datasets. The core of Neptune is a purpose-built, high-performance graph database engine. This engine is optimized for storing billions of relationships and querying the graph with milliseconds latency. Neptune supports the popular graph query languages Apache TinkerPop Gremlin and W3C’s SPARQL, enabling you to build queries that efficiently navigate highly connected datasets. + +The `@aws-cdk/aws-neptune` package contains primitives for setting up Neptune database clusters and instances. + +```ts nofixture import * as neptune from '@aws-cdk/aws-neptune'; ``` + +## Starting a Neptune Database + +To set up a Neptune database, define a `DatabaseCluster`. You must always launch a database in a VPC. + +```ts +const cluster = new neptune.DatabaseCluster(this, 'Database', { + vpc, + instanceType: neptune.InstanceType.R5_LARGE +}); +``` + +By default only writer instance is provisioned with this construct. + +## Connecting + +To control who can access the cluster, use the `.connections` attribute. Neptune databases have a default port, so +you don't need to specify the port: + +```ts fixture=with-cluster +cluster.connections.allowDefaultPortFromAnyIpv4('Open to the world'); +``` + +The endpoints to access your database cluster will be available as the `.clusterEndpoint` and `.clusterReadEndpoint` +attributes: + +```ts fixture=with-cluster +const writeAddress = cluster.clusterEndpoint.socketAddress; // "HOSTNAME:PORT" +``` + +## Customizing parameters + +Neptune allows configuring database behavior by supplying custom parameter groups. For more details, refer to the +following link: + +```ts +const clusterParams = new neptune.ClusterParameterGroup(this, 'ClusterParams', { + description: 'Cluster parameter group', + parameters: { + neptune_enable_audit_log: '1' + }, +}); + +const dbParams = new neptune.ParameterGroup(this, 'DbParams', { + description: 'Db parameter group', + parameters: { + neptune_query_timeout: '120000' + }, +}); + +const cluster = new neptune.DatabaseCluster(this, 'Database', { + vpc, + instanceType: neptune.InstanceType.R5_LARGE, + clusterParameterGroup: clusterParams, + parameterGroup: dbParams, +}); +``` + +## Adding replicas + +`DatabaseCluster` allows launching replicas along with the writer instance. This can be specified using the `instanceCount` +attribute. + +```ts +const cluster = new neptune.DatabaseCluster(this, 'Database', { + vpc, + instanceType: neptune.InstanceType.R5_LARGE, + instances: 2 +}); +``` + +Additionally it is also possible to add replicas using `DatabaseInstance` for an existing cluster. + +```ts fixture=with-cluster +const replica1 = new neptune.DatabaseInstance(this, 'Instance', { + cluster, + instanceType: neptune.InstanceType.R5_LARGE +}); +``` diff --git a/packages/@aws-cdk/aws-neptune/lib/cluster.ts b/packages/@aws-cdk/aws-neptune/lib/cluster.ts new file mode 100644 index 0000000000000..4adbe2ce8ea04 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/lib/cluster.ts @@ -0,0 +1,457 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; +import { Duration, IResource, RemovalPolicy, Resource, Token } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { Endpoint } from './endpoint'; +import { InstanceType } from './instance'; +import { CfnDBCluster, CfnDBInstance } from './neptune.generated'; +import { IClusterParameterGroup, IParameterGroup } from './parameter-group'; +import { ISubnetGroup, SubnetGroup } from './subnet-group'; + +/** + * Possible Instances Types to use in Neptune cluster + * used for defining {@link DatabaseClusterProps.engineVersion}. + */ +export class EngineVersion { + /** + * Neptune engine version 1.0.1.0 + */ + public static readonly V1_0_1_0 = new EngineVersion('1.0.1.0'); + /** + * Neptune engine version 1.0.1.1 + */ + public static readonly V1_0_1_1 = new EngineVersion('1.0.1.1'); + /** + * Neptune engine version 1.0.1.2 + */ + public static readonly V1_0_1_2 = new EngineVersion('1.0.1.2'); + /** + * Neptune engine version 1.0.2.1 + */ + public static readonly V1_0_2_1 = new EngineVersion('1.0.2.1'); + /** + * Neptune engine version 1.0.2.2 + */ + public static readonly V1_0_2_2 = new EngineVersion('1.0.2.2'); + /** + * Neptune engine version 1.0.3.0 + */ + public static readonly V1_0_3_0 = new EngineVersion('1.0.3.0'); + /** + * Neptune engine version 1.0.4.0 + */ + public static readonly V1_0_4_0 = new EngineVersion('1.0.4.0'); + /** + * Neptune engine version 1.0.4.1 + */ + public static readonly V1_0_4_1 = new EngineVersion('1.0.4.1'); + + /** + * Constructor for specifying a custom engine version + * @param version the engine version of Neptune + */ + public constructor(public readonly version: string) {} +} + +/** + * Properties for a new database cluster + */ +export interface DatabaseClusterProps { + /** + * What version of the database to start + * + * @default - The default engine version. + */ + readonly engineVersion?: EngineVersion; + + /** + * The port the Neptune cluster will listen on + * + * @default - The default engine port + */ + readonly port?: number; + + /** + * How many days to retain the backup + * + * @default - cdk.Duration.days(1) + */ + readonly backupRetention?: Duration; + + /** + * A daily time range in 24-hours UTC format in which backups preferably execute. + * + * Must be at least 30 minutes long. + * + * Example: '01:00-02:00' + * + * @default - a 30-minute window selected at random from an 8-hour block of + * time for each AWS Region. To see the time blocks available, see + */ + readonly preferredBackupWindow?: string; + + /** + * The KMS key for storage encryption. + * + * @default - default master key. + */ + readonly kmsKey?: kms.IKey; + + /** + * Whether to enable storage encryption + * + * @default true + */ + readonly storageEncrypted?: boolean; + + /** + * Number of Neptune compute instances + * + * @default 1 + */ + readonly instances?: number; + + /** + * An optional identifier for the cluster + * + * @default - A name is automatically generated. + */ + readonly dbClusterName?: string; + + /** + * Base identifier for instances + * + * Every replica is named by appending the replica number to this string, 1-based. + * + * @default - `dbClusterName` is used with the word "Instance" appended. If `dbClusterName` is not provided, the + * identifier is automatically generated. + */ + readonly instanceIdentifierBase?: string; + + /** + * What type of instance to start for the replicas + */ + readonly instanceType: InstanceType; + + /** + * A list of AWS Identity and Access Management (IAM) role that can be used by the cluster to access other AWS services. + * + * @default - No role is attached to the cluster. + */ + readonly associatedRoles?: iam.IRole[]; + + /** + * Indicates whether the DB cluster should have deletion protection enabled. + * + * @default - true if ``removalPolicy`` is RETAIN, false otherwise + */ + readonly deletionProtection?: boolean; + + /** + * A weekly time range in which maintenance should preferably execute. + * + * Must be at least 30 minutes long. + * + * Example: 'tue:04:17-tue:04:47' + * + * @default - 30-minute window selected at random from an 8-hour block of time for + * each AWS Region, occurring on a random day of the week. + */ + readonly preferredMaintenanceWindow?: string; + + /** + * Additional parameters to pass to the database engine + * + * @default - No parameter group. + */ + readonly clusterParameterGroup?: IClusterParameterGroup; + + /** + * The DB parameter group to associate with the instance. + * + * @default no parameter group + */ + readonly parameterGroup?: IParameterGroup; + + /** + * Existing subnet group for the cluster. + * + * @default - a new subnet group will be created. + */ + readonly subnetGroup?: ISubnetGroup; + + /** + * What subnets to run the Neptune instances in. + * + * Must be at least 2 subnets in two different AZs. + */ + readonly vpc: ec2.IVpc; + + /** + * Where to place the instances within the VPC + * + * @default private subnets + */ + readonly vpcSubnets?: ec2.SubnetSelection; + + /** + * Security group. + * + * @default a new security group is created. + */ + readonly securityGroups?: ec2.ISecurityGroup[]; + + /** + * The removal policy to apply when the cluster and its instances are removed + * or replaced during a stack update, or when the stack is deleted. This + * removal policy also applies to the implicit security group created for the + * cluster if one is not supplied as a parameter. + * + * @default - Retain cluster. + */ + readonly removalPolicy?: RemovalPolicy +} + +/** + * Create a clustered database with a given number of instances. + */ +export interface IDatabaseCluster extends IResource, ec2.IConnectable { + /** + * Identifier of the cluster + */ + readonly clusterIdentifier: string; + + /** + * The endpoint to use for read/write operations + * @attribute Endpoint,Port + */ + readonly clusterEndpoint: Endpoint; + + /** + * Endpoint to use for load-balanced read-only operations. + * @attribute ReadEndpoint + */ + readonly clusterReadEndpoint: Endpoint; +} + +/** + * Properties that describe an existing cluster instance + */ +export interface DatabaseClusterAttributes { + /** + * The database port + */ + readonly port: number; + + /** + * The security group of the database cluster + */ + readonly securityGroup: ec2.ISecurityGroup; + + /** + * Identifier for the cluster + */ + readonly clusterIdentifier: string; + + /** + * Cluster endpoint address + */ + readonly clusterEndpointAddress: string; + + /** + * Reader endpoint address + */ + readonly readerEndpointAddress: string; +} + +/** + * Create a clustered database with a given number of instances. + * + * @resource AWS::Neptune::DBCluster + */ +export class DatabaseCluster extends Resource implements IDatabaseCluster { + + /** + * The default number of instances in the Neptune cluster if none are + * specified + */ + public static readonly DEFAULT_NUM_INSTANCES = 1; + + /** + * Import an existing DatabaseCluster from properties + */ + public static fromDatabaseClusterAttributes(scope: Construct, id: string, attrs: DatabaseClusterAttributes): IDatabaseCluster { + class Import extends Resource implements IDatabaseCluster { + public readonly defaultPort = ec2.Port.tcp(attrs.port); + public readonly connections = new ec2.Connections({ + securityGroups: [attrs.securityGroup], + defaultPort: this.defaultPort, + }); + public readonly clusterIdentifier = attrs.clusterIdentifier; + public readonly clusterEndpoint = new Endpoint(attrs.clusterEndpointAddress, attrs.port); + public readonly clusterReadEndpoint = new Endpoint(attrs.readerEndpointAddress, attrs.port); + } + + return new Import(scope, id); + } + + /** + * Identifier of the cluster + */ + public readonly clusterIdentifier: string; + + /** + * The endpoint to use for read/write operations + */ + public readonly clusterEndpoint: Endpoint; + + /** + * Endpoint to use for load-balanced read-only operations. + */ + public readonly clusterReadEndpoint: Endpoint; + + /** + * The resource id for the cluster; for example: cluster-ABCD1234EFGH5678IJKL90MNOP. The cluster ID uniquely + * identifies the cluster and is used in things like IAM authentication policies. + * @attribute ClusterResourceId + */ + public readonly clusterResourceIdentifier: string; + + /** + * The connections object to implement IConectable + */ + public readonly connections: ec2.Connections; + + /** + * The VPC where the DB subnet group is created. + */ + public readonly vpc: ec2.IVpc; + + /** + * The subnets used by the DB subnet group. + */ + public readonly vpcSubnets: ec2.SubnetSelection; + + /** + * Subnet group used by the DB + */ + public readonly subnetGroup: ISubnetGroup; + + /** + * Identifiers of the instance + */ + public readonly instanceIdentifiers: string[] = []; + + /** + * Endpoints which address each individual instance. + */ + public readonly instanceEndpoints: Endpoint[] = []; + + constructor(scope: Construct, id: string, props: DatabaseClusterProps) { + super(scope, id); + + this.vpc = props.vpc; + this.vpcSubnets = props.vpcSubnets ?? { subnetType: ec2.SubnetType.PRIVATE }; + + // Determine the subnet(s) to deploy the Neptune cluster to + const { subnetIds, internetConnectivityEstablished } = this.vpc.selectSubnets(this.vpcSubnets); + + // Cannot test whether the subnets are in different AZs, but at least we can test the amount. + if (subnetIds.length < 2) { + throw new Error(`Cluster requires at least 2 subnets, got ${subnetIds.length}`); + } + + this.subnetGroup = props.subnetGroup ?? new SubnetGroup(this, 'Subnets', { + description: `Subnets for ${id} database`, + vpc: this.vpc, + vpcSubnets: this.vpcSubnets, + removalPolicy: props.removalPolicy === RemovalPolicy.RETAIN ? props.removalPolicy : undefined, + }); + + const securityGroups = props.securityGroups ?? [ + new ec2.SecurityGroup(this, 'SecurityGroup', { + description: 'Neptune security group', + vpc: this.vpc, + }), + ]; + + // Default to encrypted storage + const storageEncrypted = props.storageEncrypted ?? true; + + if (props.kmsKey && !storageEncrypted) { + throw new Error('KMS key supplied but storageEncrypted is false'); + } + + const deletionProtection = props.deletionProtection ?? (props.removalPolicy === RemovalPolicy.RETAIN ? true : undefined); + + // Create the Neptune cluster + const cluster = new CfnDBCluster(this, 'Resource', { + // Basic + engineVersion: props.engineVersion?.version, + dbClusterIdentifier: props.dbClusterName, + dbSubnetGroupName: this.subnetGroup.subnetGroupName, + port: props.port, + vpcSecurityGroupIds: securityGroups.map(sg => sg.securityGroupId), + dbClusterParameterGroupName: props.clusterParameterGroup?.clusterParameterGroupName, + deletionProtection: deletionProtection, + associatedRoles: props.associatedRoles ? props.associatedRoles.map(role => ({ roleArn: role.roleArn })) : undefined, + // Backup + backupRetentionPeriod: props.backupRetention?.toDays(), + preferredBackupWindow: props.preferredBackupWindow, + preferredMaintenanceWindow: props.preferredMaintenanceWindow, + // Encryption + kmsKeyId: props.kmsKey?.keyArn, + storageEncrypted, + }); + + cluster.applyRemovalPolicy(props.removalPolicy, { + applyToUpdateReplacePolicy: true, + }); + + this.clusterIdentifier = cluster.ref; + this.clusterResourceIdentifier = cluster.attrClusterResourceId; + + const port = Token.asNumber(cluster.attrPort); + this.clusterEndpoint = new Endpoint(cluster.attrEndpoint, port); + this.clusterReadEndpoint = new Endpoint(cluster.attrReadEndpoint, port); + + // Create the instances + const instanceCount = props.instances ?? DatabaseCluster.DEFAULT_NUM_INSTANCES; + if (instanceCount < 1) { + throw new Error('At least one instance is required'); + } + + for (let i = 0; i < instanceCount; i++) { + const instanceIndex = i + 1; + + const instanceIdentifier = props.instanceIdentifierBase != null ? `${props.instanceIdentifierBase}${instanceIndex}` + : props.dbClusterName != null ? `${props.dbClusterName}instance${instanceIndex}` : undefined; + + const instance = new CfnDBInstance(this, `Instance${instanceIndex}`, { + // Link to cluster + dbClusterIdentifier: cluster.ref, + dbInstanceIdentifier: instanceIdentifier, + // Instance properties + dbInstanceClass: props.instanceType, + dbParameterGroupName: props.parameterGroup?.parameterGroupName, + }); + + // We must have a dependency on the NAT gateway provider here to create + // things in the right order. + instance.node.addDependency(internetConnectivityEstablished); + + instance.applyRemovalPolicy(props.removalPolicy, { + applyToUpdateReplacePolicy: true, + }); + + this.instanceIdentifiers.push(instance.ref); + this.instanceEndpoints.push(new Endpoint(instance.attrEndpoint, port)); + } + + this.connections = new ec2.Connections({ + defaultPort: ec2.Port.tcp(port), + securityGroups: securityGroups, + }); + } +} diff --git a/packages/@aws-cdk/aws-neptune/lib/endpoint.ts b/packages/@aws-cdk/aws-neptune/lib/endpoint.ts new file mode 100644 index 0000000000000..fc92942416b7d --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/lib/endpoint.ts @@ -0,0 +1,31 @@ +import { Token } from '@aws-cdk/core'; + +/** + * Connection endpoint of a neptune cluster or instance + * + * Consists of a combination of hostname and port. + */ +export class Endpoint { + /** + * The hostname of the endpoint + */ + public readonly hostname: string; + + /** + * The port of the endpoint + */ + public readonly port: number; + + /** + * The combination of "HOSTNAME:PORT" for this endpoint + */ + public readonly socketAddress: string; + + constructor(address: string, port: number) { + this.hostname = address; + this.port = port; + + const portDesc = Token.isUnresolved(port) ? Token.asString(port) : port; + this.socketAddress = `${address}:${portDesc}`; + } +} diff --git a/packages/@aws-cdk/aws-neptune/lib/index.ts b/packages/@aws-cdk/aws-neptune/lib/index.ts index 67cdd432ee7d2..35257958b20aa 100644 --- a/packages/@aws-cdk/aws-neptune/lib/index.ts +++ b/packages/@aws-cdk/aws-neptune/lib/index.ts @@ -1,2 +1,8 @@ +export * from './cluster'; +export * from './instance'; +export * from './endpoint'; +export * from './parameter-group'; +export * from './subnet-group'; + // AWS::Neptune CloudFormation Resources: export * from './neptune.generated'; diff --git a/packages/@aws-cdk/aws-neptune/lib/instance.ts b/packages/@aws-cdk/aws-neptune/lib/instance.ts new file mode 100644 index 0000000000000..8459c710577c8 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/lib/instance.ts @@ -0,0 +1,234 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { IDatabaseCluster } from './cluster'; +import { Endpoint } from './endpoint'; +import { CfnDBInstance } from './neptune.generated'; +import { IParameterGroup } from './parameter-group'; + +/** + * Possible Instances Types to use in Neptune cluster + * used for defining {@link DatabaseInstanceProps.instanceType}. + */ +export enum InstanceType { + /** + * db.r5.large + */ + R5_LARGE = 'db.r5.large', + /** + * db.r5.xlarge + */ + R5_XLARGE = 'db.r5.xlarge', + /** + * db.r5.2xlarge + */ + R5_2XLARGE = 'db.r5.2xlarge', + /** + * db.r5.4xlarge + */ + R5_4XLARGE = 'db.r5.4xlarge', + /** + * db.r5.8xlarge + */ + R5_8XLARGE = 'db.r5.8xlarge', + /** + * db.r5.12xlarge + */ + R5_12XLARGE = 'db.r5.12xlarge', + /** + * db.r5.24xlarge + */ + R5_24XLARGE = 'db.r5.24xlarge', + /** + * db.r4.large + */ + R4_LARGE = 'db.r4.large', + /** + * db.r4.xlarge + */ + R4_XLARGE = 'db.r4.xlarge', + /** + * db.r4.2xlarge + */ + R4_2XLARGE = 'db.r4.2xlarge', + /** + * db.r4.4xlarge + */ + R4_4XLARGE = 'db.r4.4xlarge', + /** + * db.r4.8xlarge + */ + R4_8XLARGE = 'db.r4.8xlarge', + /** + * db.t3.medium + */ + T3_MEDIUM = 'db.t3.medium' +} + +/** + * A database instance + */ +export interface IDatabaseInstance extends cdk.IResource { + /** + * The instance identifier. + */ + readonly instanceIdentifier: string; + + /** + * The instance endpoint. + */ + readonly instanceEndpoint: Endpoint; + + /** + * The instance endpoint address. + * + * @attribute Endpoint + */ + readonly dbInstanceEndpointAddress: string; + + /** + * The instance endpoint port. + * + * @attribute Port + */ + readonly dbInstanceEndpointPort: string; +} + +/** + * Properties that describe an existing instance + */ +export interface DatabaseInstanceAttributes { + /** + * The instance identifier. + */ + readonly instanceIdentifier: string; + + /** + * The endpoint address. + */ + readonly instanceEndpointAddress: string; + + /** + * The database port. + */ + readonly port: number; +} + +/** + * Construction properties for a DatabaseInstanceNew + */ +export interface DatabaseInstanceProps { + /** + * The Neptune database cluster the instance should launch into. + */ + readonly cluster: IDatabaseCluster; + + /** + * What type of instance to start for the replicas + */ + readonly instanceType: InstanceType; + + /** + * The name of the Availability Zone where the DB instance will be located. + * + * @default - no preference + */ + readonly availabilityZone?: string; + + /** + * A name for the DB instance. If you specify a name, AWS CloudFormation + * converts it to lowercase. + * + * @default - a CloudFormation generated name + */ + readonly dbInstanceName?: string; + + /** + * The DB parameter group to associate with the instance. + * + * @default no parameter group + */ + readonly parameterGroup?: IParameterGroup; + + /** + * The CloudFormation policy to apply when the instance is removed from the + * stack or replaced during an update. + * + * @default RemovalPolicy.Retain + */ + readonly removalPolicy?: cdk.RemovalPolicy +} + +/** + * A database instance + * + * @resource AWS::Neptune::DBInstance + */ +export class DatabaseInstance extends cdk.Resource implements IDatabaseInstance { + + /** + * Import an existing database instance. + */ + public static fromDatabaseInstanceAttributes(scope: Construct, id: string, attrs: DatabaseInstanceAttributes): IDatabaseInstance { + class Import extends cdk.Resource implements IDatabaseInstance { + public readonly defaultPort = ec2.Port.tcp(attrs.port); + public readonly instanceIdentifier = attrs.instanceIdentifier; + public readonly dbInstanceEndpointAddress = attrs.instanceEndpointAddress; + public readonly dbInstanceEndpointPort = attrs.port.toString(); + public readonly instanceEndpoint = new Endpoint(attrs.instanceEndpointAddress, attrs.port); + } + + return new Import(scope, id); + } + + + /** + * The instance's database cluster + */ + public readonly cluster: IDatabaseCluster; + + /** + * @inheritdoc + */ + public readonly instanceIdentifier: string; + + /** + * @inheritdoc + */ + public readonly instanceEndpoint: Endpoint; + + /** + * @inheritdoc + */ + public readonly dbInstanceEndpointAddress: string; + + /** + * @inheritdoc + */ + public readonly dbInstanceEndpointPort: string; + + constructor(scope: Construct, id: string, props: DatabaseInstanceProps) { + super(scope, id); + + const instance = new CfnDBInstance(this, 'Resource', { + dbClusterIdentifier: props.cluster.clusterIdentifier, + dbInstanceClass: props.instanceType, + availabilityZone: props.availabilityZone, + dbInstanceIdentifier: props.dbInstanceName, + dbParameterGroupName: props.parameterGroup?.parameterGroupName, + }); + + this.cluster = props.cluster; + this.instanceIdentifier = instance.ref; + this.dbInstanceEndpointAddress = instance.attrEndpoint; + this.dbInstanceEndpointPort = instance.attrPort; + + // create a number token that represents the port of the instance + const portAttribute = cdk.Token.asNumber(instance.attrPort); + this.instanceEndpoint = new Endpoint(instance.attrEndpoint, portAttribute); + + instance.applyRemovalPolicy(props.removalPolicy, { + applyToUpdateReplacePolicy: true, + }); + } +} diff --git a/packages/@aws-cdk/aws-neptune/lib/parameter-group.ts b/packages/@aws-cdk/aws-neptune/lib/parameter-group.ts new file mode 100644 index 0000000000000..3cfacf061f19c --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/lib/parameter-group.ts @@ -0,0 +1,135 @@ +import { IResource, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnDBClusterParameterGroup } from './neptune.generated'; + +/** + * Properties for a parameter group + */ +interface ParameterGroupPropsBase { + /** + * Description for this parameter group + * + * @default a CDK generated description + */ + readonly description?: string; + + /** + * The parameters in this parameter group + */ + readonly parameters: { [key: string]: string }; +} + +/** + * Marker class for cluster parameter group + */ +export interface ClusterParameterGroupProps extends ParameterGroupPropsBase { + /** + * The name of the parameter group + * + * @default A CDK generated name for the parameter group + */ + readonly clusterParameterGroupName?: string; +} + +/** + * Marker class for cluster parameter group + */ +export interface ParameterGroupProps extends ParameterGroupPropsBase { + /** + * The name of the parameter group + * + * @default A CDK generated name for the parameter group + */ + readonly parameterGroupName?: string; +} + +/** + * A parameter group + */ +export interface IClusterParameterGroup extends IResource { + /** + * The name of this parameter group + */ + readonly clusterParameterGroupName: string; +} + + +/** + * A cluster parameter group + * + * @resource AWS::Neptune::DBClusterParameterGroup + */ +export class ClusterParameterGroup extends Resource implements IClusterParameterGroup { + /** + * Imports a parameter group + */ + public static fromClusterParameterGroupName(scope: Construct, id: string, clusterParameterGroupName: string): IClusterParameterGroup { + class Import extends Resource implements IClusterParameterGroup { + public readonly clusterParameterGroupName = clusterParameterGroupName; + } + return new Import(scope, id); + } + + /** + * The name of the parameter group + */ + public readonly clusterParameterGroupName: string; + + constructor(scope: Construct, id: string, props: ClusterParameterGroupProps) { + super(scope, id); + + const resource = new CfnDBClusterParameterGroup(this, 'Resource', { + name: props.clusterParameterGroupName, + description: props.description || 'Cluster parameter group for neptune db cluster', + family: 'neptune1', + parameters: props.parameters, + }); + + this.clusterParameterGroupName = resource.ref; + } +} + +/** + * A parameter group + */ +export interface IParameterGroup extends IResource { + /** + * The name of this parameter group + */ + readonly parameterGroupName: string; +} + +/** + * DB parameter group + * + * @resource AWS::Neptune::DBParameterGroup + */ +export class ParameterGroup extends Resource implements IParameterGroup { + /** + * Imports a parameter group + */ + public static fromParameterGroupName(scope: Construct, id: string, parameterGroupName: string): IParameterGroup { + class Import extends Resource implements IParameterGroup { + public readonly parameterGroupName = parameterGroupName; + } + return new Import(scope, id); + } + + /** + * The name of the parameter group + */ + public readonly parameterGroupName: string; + + constructor(scope: Construct, id: string, props: ParameterGroupProps) { + super(scope, id); + + const resource = new CfnDBClusterParameterGroup(this, 'Resource', { + name: props.parameterGroupName, + description: props.description || 'Instance parameter group for neptune db instances', + family: 'neptune1', + parameters: props.parameters, + }); + + this.parameterGroupName = resource.ref; + } +} diff --git a/packages/@aws-cdk/aws-neptune/lib/subnet-group.ts b/packages/@aws-cdk/aws-neptune/lib/subnet-group.ts new file mode 100644 index 0000000000000..383435b7a0b38 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/lib/subnet-group.ts @@ -0,0 +1,91 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import { IResource, RemovalPolicy, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnDBSubnetGroup } from './neptune.generated'; + +/** + * Interface for a subnet group. + */ +export interface ISubnetGroup extends IResource { + /** + * The name of the subnet group. + * @attribute + */ + readonly subnetGroupName: string; +} + +/** + * Properties for creating a SubnetGroup. + */ +export interface SubnetGroupProps { + /** + * Description of the subnet group. + * + * @default - a name is generated + */ + readonly description?: string; + + /** + * The VPC to place the subnet group in. + */ + readonly vpc: ec2.IVpc; + + /** + * The name of the subnet group. + * + * @default - a name is generated + */ + readonly subnetGroupName?: string; + + /** + * Which subnets within the VPC to associate with this group. + * + * @default - private subnets + */ + readonly vpcSubnets?: ec2.SubnetSelection; + + /** + * The removal policy to apply when the subnet group are removed + * from the stack or replaced during an update. + * + * @default RemovalPolicy.DESTROY + */ + readonly removalPolicy?: RemovalPolicy +} + +/** + * Class for creating a RDS DB subnet group + * + * @resource AWS::Neptune::DBSubnetGroup + */ +export class SubnetGroup extends Resource implements ISubnetGroup { + + /** + * Imports an existing subnet group by name. + */ + public static fromSubnetGroupName(scope: Construct, id: string, subnetGroupName: string): ISubnetGroup { + return new class extends Resource implements ISubnetGroup { + public readonly subnetGroupName = subnetGroupName; + }(scope, id); + } + + public readonly subnetGroupName: string; + + constructor(scope: Construct, id: string, props: SubnetGroupProps) { + super(scope, id); + + const { subnetIds } = props.vpc.selectSubnets(props.vpcSubnets ?? { subnetType: ec2.SubnetType.PRIVATE }); + + const subnetGroup = new CfnDBSubnetGroup(this, 'Resource', { + dbSubnetGroupDescription: props.description || 'Subnet group for Neptune', + dbSubnetGroupName: props.subnetGroupName, + subnetIds, + }); + + if (props.removalPolicy) { + subnetGroup.applyRemovalPolicy(props.removalPolicy); + } + + this.subnetGroupName = subnetGroup.ref; + } +} diff --git a/packages/@aws-cdk/aws-neptune/package.json b/packages/@aws-cdk/aws-neptune/package.json index f4ae26436389b..5afe6d91322a2 100644 --- a/packages/@aws-cdk/aws-neptune/package.json +++ b/packages/@aws-cdk/aws-neptune/package.json @@ -74,22 +74,36 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.2.0" }, "peerDependencies": { + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.2.0" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, + "awslint": { + "exclude": [ + "props-physical-name:@aws-cdk/aws-neptune.ParameterGroupProps", + "props-physical-name:@aws-cdk/aws-neptune.ClusterParameterGroupProps", + "props-physical-name:@aws-cdk/aws-neptune.SubnetGroupProps" + ] + }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", "awscdkio": { "announce": false }, diff --git a/packages/@aws-cdk/aws-neptune/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-neptune/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..2e687290371fa --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/rosetta/default.ts-fixture @@ -0,0 +1,14 @@ +import { Duration, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as neptune from '@aws-cdk/aws-neptune'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const vpc = new ec2.Vpc(this, 'VPC', { maxAzs: 2 }); + + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-neptune/rosetta/with-cluster.ts-fixture b/packages/@aws-cdk/aws-neptune/rosetta/with-cluster.ts-fixture new file mode 100644 index 0000000000000..c638d8b4d04fa --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/rosetta/with-cluster.ts-fixture @@ -0,0 +1,19 @@ +import { Duration, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as neptune from '@aws-cdk/aws-neptune'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const vpc = new ec2.Vpc(this, 'VPC', { maxAzs: 2 }); + + const cluster = new neptune.DatabaseCluster(this, 'Database', { + vpc, + instanceType: neptune.InstanceType.R5_LARGE, + }); + + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts new file mode 100644 index 0000000000000..d2c5ff4b6c1ef --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts @@ -0,0 +1,439 @@ +import { expect as expectCDK, haveResource, ResourcePart } from '@aws-cdk/assert'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; +import * as cdk from '@aws-cdk/core'; + +import { ClusterParameterGroup, DatabaseCluster, EngineVersion, InstanceType } from '../lib'; + +describe('DatabaseCluster', () => { + + test('check that instantiation works', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + Properties: { + DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, + VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], + StorageEncrypted: true, + }, + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, ResourcePart.CompleteDefinition)); + + expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, ResourcePart.CompleteDefinition)); + + expectCDK(stack).to(haveResource('AWS::Neptune::DBSubnetGroup', { + SubnetIds: [ + { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, + { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, + { Ref: 'VPCPrivateSubnet3Subnet3EDCD457' }, + ], + })); + }); + + test('can create a cluster with a single instance', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + instances: 1, + vpc, + instanceType: InstanceType.R5_LARGE, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, + VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], + })); + }); + + test('errors when less than one instance is specified', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + expect(() => { + new DatabaseCluster(stack, 'Database', { + instances: 0, + vpc, + instanceType: InstanceType.R5_LARGE, + }); + }).toThrowError('At least one instance is required'); + }); + + test('errors when only one subnet is specified', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC', { + maxAzs: 1, + }); + + // WHEN + expect(() => { + new DatabaseCluster(stack, 'Database', { + instances: 1, + vpc, + vpcSubnets: { + subnetType: ec2.SubnetType.PRIVATE, + }, + instanceType: InstanceType.R5_LARGE, + }); + }).toThrowError('Cluster requires at least 2 subnets, got 1'); + }); + + test('can create a cluster with custom engine version', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + engineVersion: EngineVersion.V1_0_4_1, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + EngineVersion: '1.0.4.1', + DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, + VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], + })); + }); + + test('can create a cluster with imported vpc and security group', () => { + // GIVEN + const stack = testStack(); + const vpc = ec2.Vpc.fromLookup(stack, 'VPC', { + vpcId: 'VPC12345', + }); + const sg = ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'SecurityGroupId12345'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + instances: 1, + vpc, + securityGroups: [sg], + instanceType: InstanceType.R5_LARGE, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, + VpcSecurityGroupIds: ['SecurityGroupId12345'], + })); + }); + + test('cluster with parameter group', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const group = new ClusterParameterGroup(stack, 'Params', { + description: 'bye', + parameters: { + param: 'value', + }, + }); + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + clusterParameterGroup: group, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + DBClusterParameterGroupName: { Ref: 'ParamsA8366201' }, + })); + }); + + test('cluster with associated role', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('rds.amazonaws.com'), + }); + role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonS3ReadOnlyAccess')); + + new DatabaseCluster(stack, 'Database', { + vpc, + associatedRoles: [role], + instanceType: InstanceType.R5_LARGE, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + AssociatedRoles: [ + { + RoleArn: { + 'Fn::GetAtt': [ + 'Role1ABCC5F0', + 'Arn', + ], + }, + }, + ], + })); + }); + + test('cluster with imported parameter group', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const group = ClusterParameterGroup.fromClusterParameterGroupName(stack, 'Params', 'ParamGroupName'); + + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + clusterParameterGroup: group, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + DBClusterParameterGroupName: 'ParamGroupName', + })); + }); + + test('create an encrypted cluster with custom KMS key', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + kmsKey: new kms.Key(stack, 'Key'), + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + KmsKeyId: { + 'Fn::GetAtt': [ + 'Key961B73FD', + 'Arn', + ], + }, + StorageEncrypted: true, + })); + }); + + test('creating a cluster defaults to using encryption', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + StorageEncrypted: true, + })); + }); + + test('supplying a KMS key with storageEncryption false throws an error', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + function action() { + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + kmsKey: new kms.Key(stack, 'Key'), + storageEncrypted: false, + }); + } + + // THEN + expect(action).toThrow(); + }); + + test('cluster exposes different read and write endpoints', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const cluster = new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + }); + + // THEN + expect(stack.resolve(cluster.clusterEndpoint)).not.toBe(stack.resolve(cluster.clusterReadEndpoint)); + }); + + test('instance identifier used when present', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const instanceIdentifierBase = 'instanceidentifierbase-'; + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + instanceIdentifierBase, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + DBInstanceIdentifier: `${instanceIdentifierBase}1`, + })); + }); + + test('cluster identifier used', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const clusterIdentifier = 'clusteridentifier-'; + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + dbClusterName: clusterIdentifier, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + DBInstanceIdentifier: `${clusterIdentifier}instance1`, + })); + }); + + test('imported cluster has supplied attributes', () => { + // GIVEN + const stack = testStack(); + + // WHEN + const cluster = DatabaseCluster.fromDatabaseClusterAttributes(stack, 'Database', { + clusterEndpointAddress: 'addr', + clusterIdentifier: 'identifier', + port: 3306, + readerEndpointAddress: 'reader-address', + securityGroup: ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789', { + allowAllOutbound: false, + }), + }); + + // THEN + expect(cluster.clusterEndpoint.hostname).toEqual('addr'); + expect(cluster.clusterEndpoint.port).toEqual(3306); + expect(cluster.clusterIdentifier).toEqual('identifier'); + expect(cluster.clusterReadEndpoint.hostname).toEqual('reader-address'); + }); + + test('imported cluster with imported security group honors allowAllOutbound', () => { + // GIVEN + const stack = testStack(); + + const cluster = DatabaseCluster.fromDatabaseClusterAttributes(stack, 'Database', { + clusterEndpointAddress: 'addr', + clusterIdentifier: 'identifier', + port: 3306, + readerEndpointAddress: 'reader-address', + securityGroup: ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789', { + allowAllOutbound: false, + }), + }); + + // WHEN + cluster.connections.allowToAnyIpv4(ec2.Port.tcp(443)); + + // THEN + expectCDK(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + GroupId: 'sg-123456789', + })); + }); + + test('backup retention period respected', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + backupRetention: cdk.Duration.days(20), + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + BackupRetentionPeriod: 20, + })); + }); + + test('backup maintenance window respected', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + backupRetention: cdk.Duration.days(20), + preferredBackupWindow: '07:34-08:04', + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + BackupRetentionPeriod: 20, + PreferredBackupWindow: '07:34-08:04', + })); + }); + + test('regular maintenance window respected', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + preferredMaintenanceWindow: '07:34-08:04', + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + PreferredMaintenanceWindow: '07:34-08:04', + })); + }); +}); + +function testStack() { + const stack = new cdk.Stack(undefined, undefined, { env: { account: '12345', region: 'us-test-1' } }); + stack.node.setContext('availability-zones:12345:us-test-1', ['us-test-1a', 'us-test-1b']); + return stack; +} diff --git a/packages/@aws-cdk/aws-neptune/test/endpoint.test.ts b/packages/@aws-cdk/aws-neptune/test/endpoint.test.ts new file mode 100644 index 0000000000000..cd5bd17bd3af2 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/endpoint.test.ts @@ -0,0 +1,15 @@ +import { Endpoint } from '../lib'; + +describe('Endpoint', () => { + test('accepts valid port string numbers', () => { + // GIVEN + for (const port of [1, 50, 65535]) { + // WHEN + const endpoint = new Endpoint('127.0.0.1', port); + + // THEN + expect(endpoint.port).toBe(port); + } + }); + +}); diff --git a/packages/@aws-cdk/aws-neptune/test/instance.test.ts b/packages/@aws-cdk/aws-neptune/test/instance.test.ts new file mode 100644 index 0000000000000..4dcfc75e243ac --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/instance.test.ts @@ -0,0 +1,131 @@ +import { expect as expectCDK, haveOutput, haveResource, ResourcePart } from '@aws-cdk/assert'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import * as constructs from 'constructs'; + +import { DatabaseCluster, DatabaseInstance, InstanceType, ParameterGroup } from '../lib'; + +describe('DatabaseInstance', () => { + test('check that instantiation works', () => { + // GIVEN + const stack = testStack(); + + // WHEN + new DatabaseInstance(stack, 'Instance', { + cluster: stack.cluster, + instanceType: InstanceType.R5_LARGE, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + Properties: { + DBClusterIdentifier: { Ref: 'DatabaseB269D8BB' }, + DBInstanceClass: 'db.r5.large', + }, + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, ResourcePart.CompleteDefinition)); + }); + + test('check that the endpoint works', () => { + // GIVEN + const stack = testStack(); + const instance = new DatabaseInstance(stack, 'Instance', { + cluster: stack.cluster, + instanceType: InstanceType.R5_LARGE, + }); + const exportName = 'DbInstanceEndpoint'; + + // WHEN + new cdk.CfnOutput(stack, exportName, { + exportName, + value: instance.instanceEndpoint.socketAddress, + }); + + // THEN + expectCDK(stack).to(haveOutput({ + exportName, + outputValue: { + 'Fn::Join': [ + '', + [ + { 'Fn::GetAtt': ['InstanceC1063A87', 'Endpoint'] }, + ':', + { 'Fn::GetAtt': ['InstanceC1063A87', 'Port'] }, + ], + ], + }, + })); + }); + + test('check importing works as expected', () => { + // GIVEN + const stack = testStack(); + const endpointExportName = 'DbInstanceEndpoint'; + const instanceEndpointAddress = '127.0.0.1'; + const instanceIdentifier = 'InstanceID'; + const port = 8888; + + // WHEN + const instance = DatabaseInstance.fromDatabaseInstanceAttributes(stack, 'Instance', { + instanceEndpointAddress, + instanceIdentifier, + port, + }); + new cdk.CfnOutput(stack, 'EndpointOutput', { + exportName: endpointExportName, + value: instance.instanceEndpoint.socketAddress, + }); + + // THEN + expectCDK(stack).to(haveOutput({ + exportName: endpointExportName, + outputValue: `${instanceEndpointAddress}:${port}`, + })); + }); + + test('instance with parameter group', () => { + // GIVEN + const stack = testStack(); + + // WHEN + const group = new ParameterGroup(stack, 'Params', { + description: 'bye', + parameters: { + param: 'value', + }, + }); + new DatabaseInstance(stack, 'Instance', { + cluster: stack.cluster, + instanceType: InstanceType.R5_LARGE, + parameterGroup: group, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + DBParameterGroupName: { Ref: 'ParamsA8366201' }, + })); + }); +}); + +class TestStack extends cdk.Stack { + public readonly vpc: ec2.Vpc; + public readonly cluster: DatabaseCluster; + + constructor(scope?: constructs.Construct, id?: string, props: cdk.StackProps = {}) { + super(scope, id, props); + + this.node.setContext('availability-zones:12345:us-test-1', ['us-test-1a', 'us-test-1b']); + + this.vpc = new ec2.Vpc(this, 'VPC'); + this.cluster = new DatabaseCluster(this, 'Database', { + instanceType: InstanceType.R5_LARGE, + vpc: this.vpc, + }); + } +} + +function testStack() { + const stack = new TestStack(undefined, undefined, { env: { account: '12345', region: 'us-test-1' } }); + return stack; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-neptune/test/integ.cluster.expected.json b/packages/@aws-cdk/aws-neptune/test/integ.cluster.expected.json new file mode 100644 index 0000000000000..823f7af2a5b45 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/integ.cluster.expected.json @@ -0,0 +1,504 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableAssociation0B0896DC": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "VPCPublicSubnet1DefaultRoute91CEF279": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet1EIP6AD938E8": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet2Subnet74179F39": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTableAssociation5A808732": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "VPCPublicSubnet2DefaultRouteB7481BBA": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet2EIP4947BC00": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableAssociation347902D1": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "VPCPrivateSubnet1DefaultRouteAE1D6490": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "VPCPrivateSubnet2SubnetCFCDAA7A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTableAssociation0C73D413": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + } + }, + "VPCPrivateSubnet2DefaultRouteF4F5CFD2": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet2NATGateway3C070193" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "ParamsA8366201": { + "Type": "AWS::Neptune::DBClusterParameterGroup", + "Properties": { + "Description": "A nice parameter group", + "Family": "neptune1", + "Parameters": { + "neptune_enable_audit_log": "1", + "neptune_query_timeout": "100000" + } + } + }, + "DbSecurity381C2C15": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "DatabaseSubnets3C9252C9": { + "Type": "AWS::Neptune::DBSubnetGroup", + "Properties": { + "DBSubnetGroupDescription": "Subnets for Database database", + "SubnetIds": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + ] + } + }, + "DatabaseSecurityGroup5C91FDCB": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Neptune security group", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "DatabaseSecurityGroupfrom00000IndirectPortF24F2E03": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "CidrIp": "0.0.0.0/0", + "Description": "Open to the world", + "FromPort": { + "Fn::GetAtt": [ + "DatabaseB269D8BB", + "Port" + ] + }, + "GroupId": { + "Fn::GetAtt": [ + "DatabaseSecurityGroup5C91FDCB", + "GroupId" + ] + }, + "ToPort": { + "Fn::GetAtt": [ + "DatabaseB269D8BB", + "Port" + ] + } + } + }, + "DatabaseB269D8BB": { + "Type": "AWS::Neptune::DBCluster", + "Properties": { + "DBClusterParameterGroupName": { + "Ref": "ParamsA8366201" + }, + "DBSubnetGroupName": { + "Ref": "DatabaseSubnets3C9252C9" + }, + "KmsKeyId": { + "Fn::GetAtt": [ + "DbSecurity381C2C15", + "Arn" + ] + }, + "StorageEncrypted": true, + "VpcSecurityGroupIds": [ + { + "Fn::GetAtt": [ + "DatabaseSecurityGroup5C91FDCB", + "GroupId" + ] + } + ] + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "DatabaseInstance1844F58FD": { + "Type": "AWS::Neptune::DBInstance", + "Properties": { + "DBInstanceClass": "db.r5.large", + "DBClusterIdentifier": { + "Ref": "DatabaseB269D8BB" + } + }, + "DependsOn": [ + "VPCPrivateSubnet1DefaultRouteAE1D6490", + "VPCPrivateSubnet2DefaultRouteF4F5CFD2" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts b/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts new file mode 100644 index 0000000000000..b62c0d054a624 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts @@ -0,0 +1,48 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as kms from '@aws-cdk/aws-kms'; +import * as cdk from '@aws-cdk/core'; +import * as constructs from 'constructs'; +import { DatabaseCluster, InstanceType } from '../lib'; +import { ClusterParameterGroup } from '../lib/parameter-group'; + +/* + * Stack verification steps: + * * aws docdb describe-db-clusters --db-cluster-identifier + */ + +class TestStack extends cdk.Stack { + constructor(scope: constructs.Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const vpc = new ec2.Vpc(this, 'VPC', { maxAzs: 2 }); + + const params = new ClusterParameterGroup(this, 'Params', { + description: 'A nice parameter group', + parameters: { + neptune_enable_audit_log: '1', + neptune_query_timeout: '100000', + }, + }); + + const kmsKey = new kms.Key(this, 'DbSecurity', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + const cluster = new DatabaseCluster(this, 'Database', { + vpc, + vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE }, + instanceType: InstanceType.R5_LARGE, + clusterParameterGroup: params, + kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + cluster.connections.allowDefaultPortFromAnyIpv4('Open to the world'); + } +} + +const app = new cdk.App(); + +new TestStack(app, 'aws-cdk-neptune-integ'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-neptune/test/neptune.test.ts b/packages/@aws-cdk/aws-neptune/test/neptune.test.ts deleted file mode 100644 index e394ef336bfb4..0000000000000 --- a/packages/@aws-cdk/aws-neptune/test/neptune.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '@aws-cdk/assert/jest'; -import {} from '../lib'; - -test('No tests are specified for this package', () => { - expect(true).toBe(true); -}); diff --git a/packages/@aws-cdk/aws-neptune/test/parameter-group.test.ts b/packages/@aws-cdk/aws-neptune/test/parameter-group.test.ts new file mode 100644 index 0000000000000..6ba6ff6562d76 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/parameter-group.test.ts @@ -0,0 +1,50 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import { ClusterParameterGroup, ParameterGroup } from '../lib'; + +describe('ClusterParameterGroup', () => { + + test('create a cluster parameter group', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new ClusterParameterGroup(stack, 'Params', { + description: 'desc', + parameters: { + key: 'value', + }, + }); + + // THEN + expect(stack).to(haveResource('AWS::Neptune::DBClusterParameterGroup', { + Description: 'desc', + Parameters: { + key: 'value', + }, + })); + + }); + + test('create a instance/db parameter group', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new ParameterGroup(stack, 'Params', { + description: 'desc', + parameters: { + key: 'value', + }, + }); + + // THEN + expect(stack).to(haveResource('AWS::Neptune::DBClusterParameterGroup', { + Description: 'desc', + Parameters: { + key: 'value', + }, + })); + + }); +}); diff --git a/packages/@aws-cdk/aws-neptune/test/subnet-group.test.ts b/packages/@aws-cdk/aws-neptune/test/subnet-group.test.ts new file mode 100644 index 0000000000000..e6c75013716c8 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/subnet-group.test.ts @@ -0,0 +1,83 @@ +import '@aws-cdk/assert/jest'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import { Stack } from '@aws-cdk/core'; +import { SubnetGroup } from '../lib'; + +let stack: Stack; +let vpc: ec2.IVpc; + +beforeEach(() => { + stack = new Stack(); + vpc = new ec2.Vpc(stack, 'VPC'); +}); + +test('creates a subnet group from minimal properties', () => { + new SubnetGroup(stack, 'Group', { + description: 'MyGroup', + vpc, + }); + + expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + DBSubnetGroupDescription: 'MyGroup', + SubnetIds: [ + { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, + { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, + ], + }); +}); + +test('creates a subnet group from all properties', () => { + new SubnetGroup(stack, 'Group', { + description: 'My Shared Group', + subnetGroupName: 'SharedGroup', + vpc, + vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE }, + }); + + expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + DBSubnetGroupDescription: 'My Shared Group', + DBSubnetGroupName: 'SharedGroup', + SubnetIds: [ + { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, + { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, + ], + }); +}); + +describe('subnet selection', () => { + test('defaults to private subnets', () => { + new SubnetGroup(stack, 'Group', { + description: 'MyGroup', + vpc, + }); + + expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + DBSubnetGroupDescription: 'MyGroup', + SubnetIds: [ + { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, + { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, + ], + }); + }); + + test('can specify subnet type', () => { + new SubnetGroup(stack, 'Group', { + description: 'MyGroup', + vpc, + vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, + }); + + expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + DBSubnetGroupDescription: 'MyGroup', + SubnetIds: [ + { Ref: 'VPCPublicSubnet1SubnetB4246D30' }, + { Ref: 'VPCPublicSubnet2Subnet74179F39' }, + ], + }); + }); +}); + +test('import group by name', () => { + const subnetGroup = SubnetGroup.fromSubnetGroupName(stack, 'Group', 'my-subnet-group'); + expect(subnetGroup.subnetGroupName).toBe('my-subnet-group'); +}); diff --git a/packages/@aws-cdk/aws-route53/lib/record-set.ts b/packages/@aws-cdk/aws-route53/lib/record-set.ts index 577af5c1a3a57..a86195f1d0055 100644 --- a/packages/@aws-cdk/aws-route53/lib/record-set.ts +++ b/packages/@aws-cdk/aws-route53/lib/record-set.ts @@ -604,7 +604,7 @@ export class CrossAccountZoneDelegationRecord extends CoreConstruct { const serviceToken = CustomResourceProvider.getOrCreate(this, CROSS_ACCOUNT_ZONE_DELEGATION_RESOURCE_TYPE, { codeDirectory: path.join(__dirname, 'cross-account-zone-delegation-handler'), - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, policyStatements: [{ Effect: 'Allow', Action: 'sts:AssumeRole', Resource: props.delegationRole.roleArn }], }); diff --git a/packages/@aws-cdk/aws-s3-assets/README.md b/packages/@aws-cdk/aws-s3-assets/README.md index aab4c46d9c44d..7a751410a2b22 100644 --- a/packages/@aws-cdk/aws-s3-assets/README.md +++ b/packages/@aws-cdk/aws-s3-assets/README.md @@ -88,8 +88,8 @@ The following example uses custom asset bundling to convert a markdown file to h [Example of using asset bundling](./test/integ.assets.bundling.lit.ts). -The bundling docker image (`image`) can either come from a registry (`BundlingDockerImage.fromRegistry`) -or it can be built from a `Dockerfile` located inside your project (`BundlingDockerImage.fromAsset`). +The bundling docker image (`image`) can either come from a registry (`DockerImage.fromRegistry`) +or it can be built from a `Dockerfile` located inside your project (`DockerImage.fromBuild`). You can set the `CDK_DOCKER` environment variable in order to provide a custom docker program to execute. This may sometime be needed when building in @@ -114,7 +114,7 @@ new assets.Asset(this, 'BundledAsset', { }, }, // Docker bundling fallback - image: BundlingDockerImage.fromRegistry('alpine'), + image: DockerImage.fromRegistry('alpine'), entrypoint: ['/bin/sh', '-c'], command: ['bundle'], }, @@ -124,6 +124,27 @@ new assets.Asset(this, 'BundledAsset', { Although optional, it's recommended to provide a local bundling method which can greatly improve performance. +If the bundling output contains a single archive file (zip or jar) it will be +uploaded to S3 as-is and will not be zipped. Otherwise the contents of the +output directory will be zipped and the zip file will be uploaded to S3. This +is the default behavior for `bundling.outputType` (`BundlingOutput.AUTO_DISCOVER`). + +Use `BundlingOutput.NOT_ARCHIVED` if the bundling output must always be zipped: + +```ts +const asset = new assets.Asset(this, 'BundledAsset', { + path: '/path/to/asset', + bundling: { + image: DockerImage.fromRegistry('alpine'), + command: ['command-that-produces-an-archive.sh'], + outputType: BundlingOutput.NOT_ARCHIVED, // Bundling output will be zipped even though it produces a single archive file. + }, +}); +``` + +Use `BundlingOutput.ARCHIVED` if the bundling output contains a single archive file and +you don't want it to be zippped. + ## CloudFormation Resource Metadata > NOTE: This section is relevant for authors of AWS Resource Constructs. diff --git a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts index 938778d1381f4..510834a61c634 100644 --- a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts +++ b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts @@ -1,4 +1,3 @@ -import * as fs from 'fs'; import * as path from 'path'; import * as assets from '@aws-cdk/assets'; import * as iam from '@aws-cdk/aws-iam'; @@ -13,8 +12,6 @@ import { toSymlinkFollow } from './compat'; // eslint-disable-next-line no-duplicate-imports, import/order import { Construct as CoreConstruct } from '@aws-cdk/core'; -const ARCHIVE_EXTENSIONS = ['.zip', '.jar']; - export interface AssetOptions extends assets.CopyOptions, cdk.AssetOptions { /** * A list of principals that should be able to read this asset from S3. @@ -139,17 +136,12 @@ export class Asset extends CoreConstruct implements cdk.IAsset { this.assetPath = staging.relativeStagedPath(stack); - const packaging = determinePackaging(staging.sourcePath); - - this.isFile = packaging === cdk.FileAssetPackaging.FILE; + this.isFile = staging.packaging === cdk.FileAssetPackaging.FILE; - // sets isZipArchive based on the type of packaging and file extension - this.isZipArchive = packaging === cdk.FileAssetPackaging.ZIP_DIRECTORY - ? true - : ARCHIVE_EXTENSIONS.some(ext => staging.sourcePath.toLowerCase().endsWith(ext)); + this.isZipArchive = staging.isArchive; const location = stack.synthesizer.addFileAsset({ - packaging, + packaging: staging.packaging, sourceHash: this.sourceHash, fileName: this.assetPath, }); @@ -210,19 +202,3 @@ export class Asset extends CoreConstruct implements cdk.IAsset { this.bucket.grantRead(grantee); } } - -function determinePackaging(assetPath: string): cdk.FileAssetPackaging { - if (!fs.existsSync(assetPath)) { - throw new Error(`Cannot find asset at ${assetPath}`); - } - - if (fs.statSync(assetPath).isDirectory()) { - return cdk.FileAssetPackaging.ZIP_DIRECTORY; - } - - if (fs.statSync(assetPath).isFile()) { - return cdk.FileAssetPackaging.FILE; - } - - throw new Error(`Asset ${assetPath} is expected to be either a directory or a regular file`); -} diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.ts b/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.ts index 83243212409d7..8052dd340d888 100644 --- a/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.ts +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.ts @@ -17,7 +17,7 @@ class TestStack extends Stack { // Put objects in the bucket to ensure auto delete works as expected const serviceToken = CustomResourceProvider.getOrCreate(this, PUT_OBJECTS_RESOURCE_TYPE, { codeDirectory: path.join(__dirname, 'put-objects-handler'), - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, policyStatements: [{ Effect: 'Allow', Action: 's3:PutObject', diff --git a/packages/@aws-cdk/aws-sam/package.json b/packages/@aws-cdk/aws-sam/package.json index 6e76bd0098faf..6643799114182 100644 --- a/packages/@aws-cdk/aws-sam/package.json +++ b/packages/@aws-cdk/aws-sam/package.json @@ -78,7 +78,7 @@ "cfn2ts": "0.0.0", "jest": "^26.6.3", "pkglint": "0.0.0", - "ts-jest": "^26.5.1" + "ts-jest": "^26.5.2" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-secretsmanager/test/integ.secret-name-parsed.ts b/packages/@aws-cdk/aws-secretsmanager/test/integ.secret-name-parsed.ts index 2d245cc6e3e01..801077c5c8494 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/integ.secret-name-parsed.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/integ.secret-name-parsed.ts @@ -21,7 +21,7 @@ class SecretsManagerStack extends cdk.Stack { const resourceType = 'Custom::IntegVerificationSecretNameMatches'; const serviceToken = cdk.CustomResourceProvider.getOrCreate(this, resourceType, { codeDirectory: path.join(__dirname, 'integ.secret-name-parsed.handler'), - runtime: cdk.CustomResourceProviderRuntime.NODEJS_12, + runtime: cdk.CustomResourceProviderRuntime.NODEJS_12_X, policyStatements: [{ Effect: 'Allow', Resource: secrets.map(s => s.secretArn), diff --git a/packages/@aws-cdk/aws-ses-actions/test/integ.actions.expected.json b/packages/@aws-cdk/aws-ses-actions/test/integ.actions.expected.json index b58b770bcd426..4379649e02d69 100644 --- a/packages/@aws-cdk/aws-ses-actions/test/integ.actions.expected.json +++ b/packages/@aws-cdk/aws-ses-actions/test/integ.actions.expected.json @@ -40,13 +40,13 @@ "Code": { "ZipFile": "exports.handler = async (event) => event;" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "FunctionServiceRole675BB04A", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -341,16 +341,51 @@ "Type": "AWS::Lambda::Function", "Properties": { "Code": { - "ZipFile": "exports.handler = function dropSpamCode(event, _, callback) {\n console.log('Spam filter');\n const sesNotification = event.Records[0].ses;\n console.log('SES Notification:\\n', JSON.stringify(sesNotification, null, 2));\n // Check if any spam check failed\n if (sesNotification.receipt.spfVerdict.status === 'FAIL'\n || sesNotification.receipt.dkimVerdict.status === 'FAIL'\n || sesNotification.receipt.spamVerdict.status === 'FAIL'\n || sesNotification.receipt.virusVerdict.status === 'FAIL') {\n console.log('Dropping spam');\n // Stop processing rule set, dropping message\n callback(null, { disposition: 'STOP_RULE_SET' });\n }\n else {\n callback(null, null);\n }\n}" + "S3Bucket": { + "Ref": "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3Bucket6AFCBA5F" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3VersionKey02BA9086" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3VersionKey02BA9086" + } + ] + } + ] + } + ] + ] + } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4", "Arn" ] }, - "Runtime": "nodejs10.x" + "Handler": "index.handler", + "Runtime": "nodejs14.x" }, "DependsOn": [ "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4" @@ -372,5 +407,19 @@ } } } + }, + "Parameters": { + "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3Bucket6AFCBA5F": { + "Type": "String", + "Description": "S3 bucket for asset \"96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34\"" + }, + "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3VersionKey02BA9086": { + "Type": "String", + "Description": "S3 key for asset version \"96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34\"" + }, + "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34ArtifactHash6BE57680": { + "Type": "String", + "Description": "Artifact hash for asset \"96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34\"" + } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ses/lib/drop-spam-handler/index.ts b/packages/@aws-cdk/aws-ses/lib/drop-spam-handler/index.ts new file mode 100644 index 0000000000000..76a639acdf50e --- /dev/null +++ b/packages/@aws-cdk/aws-ses/lib/drop-spam-handler/index.ts @@ -0,0 +1,22 @@ +/* eslint-disable no-console */ + +// Adapted from https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-lambda-example-functions.html +export async function handler(event: AWSLambda.SESEvent): Promise<{ disposition: string } | null> { + console.log('Spam filter'); + + const sesNotification = event.Records[0].ses; + console.log('SES Notification: %j', sesNotification); + + // Check if any spam check failed + if (sesNotification.receipt.spfVerdict.status === 'FAIL' + || sesNotification.receipt.dkimVerdict.status === 'FAIL' + || sesNotification.receipt.spamVerdict.status === 'FAIL' + || sesNotification.receipt.virusVerdict.status === 'FAIL') { + console.log('Dropping spam'); + + // Stop processing rule set, dropping message + return { disposition: 'STOP_RULE_SET' }; + } + + return null; +} diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts index 5b6a276929c8e..7f6f1fe914a6b 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts @@ -1,3 +1,4 @@ +import * as path from 'path'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import { Aws, IResource, Lazy, Resource } from '@aws-cdk/core'; @@ -175,9 +176,9 @@ export class DropSpamReceiptRule extends CoreConstruct { super(scope, id); const fn = new lambda.SingletonFunction(this, 'Function', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', - code: lambda.Code.fromInline(`exports.handler = ${dropSpamCode}`), + code: lambda.Code.fromAsset(path.join(__dirname, 'drop-spam-handler')), uuid: '224e77f9-a32e-4b4d-ac32-983477abba16', }); @@ -203,25 +204,3 @@ export class DropSpamReceiptRule extends CoreConstruct { }); } } - -// Adapted from https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-lambda-example-functions.html -/* eslint-disable no-console */ -function dropSpamCode(event: any, _: any, callback: any) { - console.log('Spam filter'); - - const sesNotification = event.Records[0].ses; - console.log('SES Notification:\n', JSON.stringify(sesNotification, null, 2)); - - // Check if any spam check failed - if (sesNotification.receipt.spfVerdict.status === 'FAIL' - || sesNotification.receipt.dkimVerdict.status === 'FAIL' - || sesNotification.receipt.spamVerdict.status === 'FAIL' - || sesNotification.receipt.virusVerdict.status === 'FAIL') { - console.log('Dropping spam'); - - // Stop processing rule set, dropping message - callback(null, { disposition: 'STOP_RULE_SET' }); - } else { - callback(null, null); - } -} diff --git a/packages/@aws-cdk/aws-ses/test/integ.receipt.expected.json b/packages/@aws-cdk/aws-ses/test/integ.receipt.expected.json index b4cdf79e60d44..2bb22f110b951 100644 --- a/packages/@aws-cdk/aws-ses/test/integ.receipt.expected.json +++ b/packages/@aws-cdk/aws-ses/test/integ.receipt.expected.json @@ -97,16 +97,51 @@ "Type": "AWS::Lambda::Function", "Properties": { "Code": { - "ZipFile": "exports.handler = function dropSpamCode(event, _, callback) {\n console.log('Spam filter');\n const sesNotification = event.Records[0].ses;\n console.log('SES Notification:\\n', JSON.stringify(sesNotification, null, 2));\n // Check if any spam check failed\n if (sesNotification.receipt.spfVerdict.status === 'FAIL'\n || sesNotification.receipt.dkimVerdict.status === 'FAIL'\n || sesNotification.receipt.spamVerdict.status === 'FAIL'\n || sesNotification.receipt.virusVerdict.status === 'FAIL') {\n console.log('Dropping spam');\n // Stop processing rule set, dropping message\n callback(null, { disposition: 'STOP_RULE_SET' });\n }\n else {\n callback(null, null);\n }\n}" + "S3Bucket": { + "Ref": "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3Bucket6AFCBA5F" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3VersionKey02BA9086" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3VersionKey02BA9086" + } + ] + } + ] + } + ] + ] + } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4", "Arn" ] }, - "Runtime": "nodejs10.x" + "Handler": "index.handler", + "Runtime": "nodejs14.x" }, "DependsOn": [ "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4" @@ -150,5 +185,19 @@ } } } + }, + "Parameters": { + "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3Bucket6AFCBA5F": { + "Type": "String", + "Description": "S3 bucket for asset \"96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34\"" + }, + "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3VersionKey02BA9086": { + "Type": "String", + "Description": "S3 key for asset version \"96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34\"" + }, + "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34ArtifactHash6BE57680": { + "Type": "String", + "Description": "Artifact hash for asset \"96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34\"" + } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 38305b270da69..d7c2d1498394d 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -189,7 +189,7 @@ const convertToSeconds = new tasks.EvaluateExpression(this, 'Convert to seconds' const createMessage = new tasks.EvaluateExpression(this, 'Create message', { // Note: this is a string inside a string. expression: '`Now waiting ${$.waitSeconds} seconds...`', - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_14_X, resultPath: '$.message', }); @@ -212,9 +212,8 @@ new sfn.StateMachine(this, 'StateMachine', { ``` The `EvaluateExpression` supports a `runtime` prop to specify the Lambda -runtime to use to evaluate the expression. Currently, the only runtime -supported is `lambda.Runtime.NODEJS_10_X`. - +runtime to use to evaluate the expression. Currently, only runtimes +of the Node.js family are supported. ## Athena 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-nodejs-handler/index.ts similarity index 100% rename from packages/@aws-cdk/aws-stepfunctions-tasks/lib/eval-nodejs10.x-handler/index.ts rename to packages/@aws-cdk/aws-stepfunctions-tasks/lib/eval-nodejs-handler/index.ts diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts index 45457a1c377c8..64c25d5e3dd3a 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts @@ -20,7 +20,7 @@ export interface EvaluateExpressionProps extends sfn.TaskStateBaseProps { /** * The runtime language to use to evaluate the expression. * - * @default lambda.Runtime.NODEJS_10_X + * @default lambda.Runtime.NODEJS_14_X */ readonly runtime?: lambda.Runtime; } @@ -58,7 +58,7 @@ export class EvaluateExpression extends sfn.TaskStateBase { constructor(scope: Construct, id: string, private readonly props: EvaluateExpressionProps) { super(scope, id, props); - this.evalFn = createEvalFn(this.props.runtime || lambda.Runtime.NODEJS_10_X, this); + this.evalFn = createEvalFn(this.props.runtime ?? lambda.Runtime.NODEJS_14_X, this); this.taskPolicies = [ new iam.PolicyStatement({ @@ -97,17 +97,18 @@ export class EvaluateExpression extends sfn.TaskStateBase { } function createEvalFn(runtime: lambda.Runtime, scope: Construct) { - const code = lambda.Code.asset(path.join(__dirname, `eval-${runtime.name}-handler`)); const lambdaPurpose = 'Eval'; switch (runtime) { + case lambda.Runtime.NODEJS_14_X: + case lambda.Runtime.NODEJS_12_X: case lambda.Runtime.NODEJS_10_X: return new lambda.SingletonFunction(scope, 'EvalFunction', { runtime, handler: 'index.handler', uuid: 'a0d2ce44-871b-4e74-87a1-f5e63d7c3bdc', lambdaPurpose, - code, + code: lambda.Code.fromAsset(path.join(__dirname, 'eval-nodejs-handler')), }); // TODO: implement other runtimes default: diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/update-endpoint.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/update-endpoint.ts index 4a4e068d816d0..4ff5bf4d19bb3 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/update-endpoint.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/update-endpoint.ts @@ -73,6 +73,13 @@ export class SageMakerUpdateEndpoint extends sfn.TaskStateBase { // SageMaker uses lowercase for resource name in the arn resourceName: sfn.JsonPath.isEncodedJsonPath(this.props.endpointName) ? '*' : `${this.props.endpointName.toLowerCase()}`, }), + stack.formatArn({ + service: 'sagemaker', + resource: 'endpoint-config', + // If the endpointConfig name comes from input, we cannot target the policy to a particular ARN prefix reliably. + // SageMaker uses lowercase for resource name in the arn + resourceName: sfn.JsonPath.isEncodedJsonPath(this.props.endpointConfigName) ? '*' : `${this.props.endpointConfigName.toLowerCase()}`, + }), ], }), ]; 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-nodejs-handler.test.ts similarity index 96% rename from packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs10.x-handler.test.ts rename to packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs-handler.test.ts index d42be11d7aaa4..8d69e9d3b8105 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs10.x-handler.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs-handler.test.ts @@ -1,5 +1,5 @@ import { Event } from '../lib'; -import { handler } from '../lib/eval-nodejs10.x-handler'; +import { handler } from '../lib/eval-nodejs-handler'; test('with numbers', async () => { // GIVEN diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/evaluate-expression.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/evaluate-expression.test.ts index 679e817dfbead..7a2aa196b3de2 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/evaluate-expression.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/evaluate-expression.test.ts @@ -34,7 +34,7 @@ test('Eval with Node.js', () => { }); expect(stack).toHaveResource('AWS::Lambda::Function', { - Runtime: 'nodejs10.x', + Runtime: 'nodejs14.x', }); }); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.evaluate-expression.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.evaluate-expression.expected.json index efdf3878e67e2..c48b04a826783 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.evaluate-expression.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.evaluate-expression.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0S3BucketA16CB30E" + "Ref": "AssetParametersbc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626bS3Bucket743A2950" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0S3VersionKey102DBBD9" + "Ref": "AssetParametersbc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626bS3VersionKey2DBCB833" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0S3VersionKey102DBBD9" + "Ref": "AssetParametersbc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626bS3VersionKey2DBCB833" } ] } @@ -72,14 +72,14 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "Evala0d2ce44871b4e7487a1f5e63d7c3bdcServiceRoleDC85DDD3", "Arn" ] }, - "Runtime": "nodejs10.x" + "Handler": "index.handler", + "Runtime": "nodejs14.x" }, "DependsOn": [ "Evala0d2ce44871b4e7487a1f5e63d7c3bdcServiceRoleDC85DDD3" @@ -185,17 +185,17 @@ } }, "Parameters": { - "AssetParameters640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0S3BucketA16CB30E": { + "AssetParametersbc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626bS3Bucket743A2950": { "Type": "String", - "Description": "S3 bucket for asset \"640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0\"" + "Description": "S3 bucket for asset \"bc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626b\"" }, - "AssetParameters640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0S3VersionKey102DBBD9": { + "AssetParametersbc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626bS3VersionKey2DBCB833": { "Type": "String", - "Description": "S3 key for asset version \"640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0\"" + "Description": "S3 key for asset version \"bc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626b\"" }, - "AssetParameters640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0ArtifactHash43D553D7": { + "AssetParametersbc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626bArtifactHashAD6C554B": { "Type": "String", - "Description": "Artifact hash for asset \"640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0\"" + "Description": "Artifact hash for asset \"bc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626b\"" } }, "Outputs": { @@ -205,4 +205,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.call-sagemaker.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.call-sagemaker.expected.json index 1e8f55ca57056..c163ebd11cf77 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.call-sagemaker.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.call-sagemaker.expected.json @@ -465,26 +465,48 @@ { "Action": "sagemaker:updateEndpoint", "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":sagemaker:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":endpoint/*" + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":sagemaker:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":endpoint/*" + ] ] - ] - } + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":sagemaker:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":endpoint-config/*" + ] + ] + } + ] } ], "Version": "2012-10-17" diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/update-endpoint.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/update-endpoint.test.ts index f628ddbe9ed14..edd90392970b5 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/update-endpoint.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/update-endpoint.test.ts @@ -1,4 +1,5 @@ import '@aws-cdk/assert/jest'; +import * as iam from '@aws-cdk/aws-iam'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as tasks from '../../lib'; @@ -80,3 +81,65 @@ test('Task throws if WAIT_FOR_TASK_TOKEN is supplied as service integration patt }); }).toThrow(/Unsupported service integration pattern. Supported Patterns: REQUEST_RESPONSE. Received: WAIT_FOR_TASK_TOKEN/i); }); + +test('PolicyStatement has sufficient permissions', () => { + // WHEN + const props = { + endpointName: 'MyEndpoint', + endpointConfigName: 'MyEndpointConfig', + }; + const task = new tasks.SageMakerUpdateEndpoint(stack, 'SagemakerEndpoint', props); + + const graph = new sfn.StateGraph(task, 'test'); + + // THEN + expect(graph.policyStatements).toEqual( + [ + new iam.PolicyStatement({ + actions: ['sagemaker:updateEndpoint'], + resources: [ + stack.formatArn({ + service: 'sagemaker', + resource: 'endpoint', + resourceName: props.endpointName.toLowerCase(), + }), + stack.formatArn({ + service: 'sagemaker', + resource: 'endpoint-config', + resourceName: props.endpointConfigName.toLowerCase(), + }), + ], + }), + ], + ); + + // WHEN + const props2 = { + endpointName: sfn.JsonPath.stringAt('$.Endpoint.Name'), + endpointConfigName: sfn.JsonPath.stringAt('$.Endpoint.Config'), + }; + const task2 = new tasks.SageMakerUpdateEndpoint(stack, 'SagemakerEndpoint2', props2); + + const graph2 = new sfn.StateGraph(task2, 'test'); + + // THEN + expect(graph2.policyStatements).toEqual( + [ + new iam.PolicyStatement({ + actions: ['sagemaker:updateEndpoint'], + resources: [ + stack.formatArn({ + service: 'sagemaker', + resource: 'endpoint', + resourceName: '*', + }), + stack.formatArn({ + service: 'sagemaker', + resource: 'endpoint-config', + resourceName: '*', + }), + ], + }), + ], + ); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/cloudformation-diff/package.json b/packages/@aws-cdk/cloudformation-diff/package.json index 2a1705ab876fd..d717c69b9db96 100644 --- a/packages/@aws-cdk/cloudformation-diff/package.json +++ b/packages/@aws-cdk/cloudformation-diff/package.json @@ -25,7 +25,7 @@ "colors": "^1.4.0", "diff": "^5.0.0", "fast-deep-equal": "^3.1.3", - "string-width": "^4.2.0", + "string-width": "^4.2.2", "table": "^6.0.7" }, "devDependencies": { @@ -36,7 +36,7 @@ "fast-check": "^2.13.0", "jest": "^26.6.3", "pkglint": "0.0.0", - "ts-jest": "^26.5.1" + "ts-jest": "^26.5.2" }, "repository": { "url": "https://github.com/aws/aws-cdk.git", diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index 457f11834e88a..ce9716626baee 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -367,7 +367,7 @@ "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", "pkglint": "0.0.0", - "ts-jest": "^26.5.1" + "ts-jest": "^26.5.2" }, "keywords": [ "aws", diff --git a/packages/@aws-cdk/core/README.md b/packages/@aws-cdk/core/README.md index 714e7139f0807..bb519f066af55 100644 --- a/packages/@aws-cdk/core/README.md +++ b/packages/@aws-cdk/core/README.md @@ -427,7 +427,7 @@ stack-unique identifier and returns the service token: ```ts const serviceToken = CustomResourceProvider.getOrCreate(this, 'Custom::MyCustomResourceType', { codeDirectory: `${__dirname}/my-handler`, - runtime: CustomResourceProviderRuntime.NODEJS_12, // currently the only supported runtime + runtime: CustomResourceProviderRuntime.NODEJS_12_X, description: "Lambda function created by the custom resource provider", }); @@ -522,7 +522,7 @@ export class Sum extends Construct { const resourceType = 'Custom::Sum'; const serviceToken = CustomResourceProvider.getOrCreate(this, resourceType, { codeDirectory: `${__dirname}/sum-handler`, - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, }); const resource = new CustomResource(this, 'Resource', { @@ -552,7 +552,7 @@ built-in singleton method: ```ts const provider = CustomResourceProvider.getOrCreateProvider(this, 'Custom::MyCustomResourceType', { codeDirectory: `${__dirname}/my-handler`, - runtime: CustomResourceProviderRuntime.NODEJS_12, // currently the only supported runtime + runtime: CustomResourceProviderRuntime.NODEJS_12_X, }); const roleArn = provider.roleArn; diff --git a/packages/@aws-cdk/core/lib/asset-staging.ts b/packages/@aws-cdk/core/lib/asset-staging.ts index 66c65e3d14864..6a34bd9b4b1ac 100644 --- a/packages/@aws-cdk/core/lib/asset-staging.ts +++ b/packages/@aws-cdk/core/lib/asset-staging.ts @@ -5,8 +5,8 @@ import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import * as fs from 'fs-extra'; import * as minimatch from 'minimatch'; -import { AssetHashType, AssetOptions } from './assets'; -import { BundlingOptions } from './bundling'; +import { AssetHashType, AssetOptions, FileAssetPackaging } from './assets'; +import { BundlingOptions, BundlingOutput } from './bundling'; import { FileSystem, FingerprintOptions } from './fs'; import { Names } from './names'; import { Cache } from './private/cache'; @@ -17,6 +17,8 @@ import { Stage } from './stage'; // eslint-disable-next-line import { Construct as CoreConstruct } from './construct-compat'; +const ARCHIVE_EXTENSIONS = ['.zip', '.jar']; + /** * A previously staged asset */ @@ -30,6 +32,16 @@ interface StagedAsset { * The hash we used previously */ readonly assetHash: string; + + /** + * The packaging of the asset + */ + readonly packaging: FileAssetPackaging, + + /** + * Whether this asset is an archive + */ + readonly isArchive: boolean; } /** @@ -124,6 +136,16 @@ export class AssetStaging extends CoreConstruct { */ public readonly assetHash: string; + /** + * How this asset should be packaged. + */ + public readonly packaging: FileAssetPackaging; + + /** + * Whether this asset is an archive (zip or jar). + */ + public readonly isArchive: boolean; + private readonly fingerprintOptions: FingerprintOptions; private readonly hashType: AssetHashType; @@ -138,12 +160,20 @@ export class AssetStaging extends CoreConstruct { private readonly cacheKey: string; + private readonly sourceStats: fs.Stats; + constructor(scope: Construct, id: string, props: AssetStagingProps) { super(scope, id); this.sourcePath = path.resolve(props.sourcePath); this.fingerprintOptions = props; + if (!fs.existsSync(this.sourcePath)) { + throw new Error(`Cannot find asset at ${this.sourcePath}`); + } + + this.sourceStats = fs.statSync(this.sourcePath); + const outdir = Stage.of(this)?.assetOutdir; if (!outdir) { throw new Error('unable to determine cloud assembly asset output directory. Assets must be defined indirectly within a "Stage" or an "App" scope'); @@ -192,6 +222,8 @@ export class AssetStaging extends CoreConstruct { this.stagedPath = staged.stagedPath; this.absoluteStagedPath = staged.stagedPath; this.assetHash = staged.assetHash; + this.packaging = staged.packaging; + this.isArchive = staged.isArchive; } /** @@ -248,8 +280,18 @@ export class AssetStaging extends CoreConstruct { ? this.sourcePath : path.resolve(this.assetOutdir, renderAssetFilename(assetHash, path.extname(this.sourcePath))); + if (!this.sourceStats.isDirectory() && !this.sourceStats.isFile()) { + throw new Error(`Asset ${this.sourcePath} is expected to be either a directory or a regular file`); + } + this.stageAsset(this.sourcePath, stagedPath, 'copy'); - return { assetHash, stagedPath }; + + return { + assetHash, + stagedPath, + packaging: this.sourceStats.isDirectory() ? FileAssetPackaging.ZIP_DIRECTORY : FileAssetPackaging.FILE, + isArchive: this.sourceStats.isDirectory() || ARCHIVE_EXTENSIONS.includes(path.extname(this.sourcePath).toLowerCase()), + }; } /** @@ -258,6 +300,10 @@ export class AssetStaging extends CoreConstruct { * Optionally skip, in which case we pretend we did something but we don't really. */ private stageByBundling(bundling: BundlingOptions, skip: boolean): StagedAsset { + if (!this.sourceStats.isDirectory()) { + throw new Error(`Asset ${this.sourcePath} is expected to be a directory when bundling`); + } + if (skip) { // We should have bundled, but didn't to save time. Still pretend to have a hash. // If the asset uses OUTPUT or BUNDLE, we use a CUSTOM hash to avoid fingerprinting @@ -270,6 +316,8 @@ export class AssetStaging extends CoreConstruct { return { assetHash: this.calculateHash(hashType, bundling), stagedPath: this.sourcePath, + packaging: FileAssetPackaging.ZIP_DIRECTORY, + isArchive: true, }; } @@ -281,12 +329,21 @@ export class AssetStaging extends CoreConstruct { const bundleDir = this.determineBundleDir(this.assetOutdir, assetHash); this.bundle(bundling, bundleDir); - // Calculate assetHash afterwards if we still must - assetHash = assetHash ?? this.calculateHash(this.hashType, bundling, bundleDir); - const stagedPath = path.resolve(this.assetOutdir, renderAssetFilename(assetHash)); + // Check bundling output content and determine if we will need to archive + const bundlingOutputType = bundling.outputType ?? BundlingOutput.AUTO_DISCOVER; + const bundledAsset = determineBundledAsset(bundleDir, bundlingOutputType); - this.stageAsset(bundleDir, stagedPath, 'move'); - return { assetHash, stagedPath }; + // Calculate assetHash afterwards if we still must + assetHash = assetHash ?? this.calculateHash(this.hashType, bundling, bundledAsset.path); + const stagedPath = path.resolve(this.assetOutdir, renderAssetFilename(assetHash, bundledAsset.extension)); + + this.stageAsset(bundledAsset.path, stagedPath, 'move'); + return { + assetHash, + stagedPath, + packaging: bundledAsset.packaging, + isArchive: true, // bundling always produces an archive + }; } /** @@ -320,10 +377,9 @@ export class AssetStaging extends CoreConstruct { } // Copy file/directory to staging directory - const stat = fs.statSync(sourcePath); - if (stat.isFile()) { + if (this.sourceStats.isFile()) { fs.copyFileSync(sourcePath, targetPath); - } else if (stat.isDirectory()) { + } else if (this.sourceStats.isDirectory()) { fs.mkdirSync(targetPath); FileSystem.copyDirectory(sourcePath, targetPath, this.fingerprintOptions); } else { @@ -502,3 +558,57 @@ function sortObject(object: { [key: string]: any }): { [key: string]: any } { } return ret; } + +/** + * Returns the single archive file of a directory or undefined + */ +function singleArchiveFile(directory: string): string | undefined { + if (!fs.existsSync(directory)) { + throw new Error(`Directory ${directory} does not exist.`); + } + + if (!fs.statSync(directory).isDirectory()) { + throw new Error(`${directory} is not a directory.`); + } + + const content = fs.readdirSync(directory); + if (content.length === 1) { + const file = path.join(directory, content[0]); + const extension = path.extname(content[0]).toLowerCase(); + if (fs.statSync(file).isFile() && ARCHIVE_EXTENSIONS.includes(extension)) { + return file; + } + } + + return undefined; +} + +interface BundledAsset { + path: string, + packaging: FileAssetPackaging, + extension?: string +} + +/** + * Returns the bundled asset to use based on the content of the bundle directory + * and the type of output. + */ +function determineBundledAsset(bundleDir: string, outputType: BundlingOutput): BundledAsset { + const archiveFile = singleArchiveFile(bundleDir); + + // auto-discover means that if there is an archive file, we take it as the + // bundle, otherwise, we will archive here. + if (outputType === BundlingOutput.AUTO_DISCOVER) { + outputType = archiveFile ? BundlingOutput.ARCHIVED : BundlingOutput.NOT_ARCHIVED; + } + + switch (outputType) { + case BundlingOutput.NOT_ARCHIVED: + return { path: bundleDir, packaging: FileAssetPackaging.ZIP_DIRECTORY }; + case BundlingOutput.ARCHIVED: + if (!archiveFile) { + throw new Error('Bundling output directory is expected to include only a single .zip or .jar file when `output` is set to `ARCHIVED`'); + } + return { path: archiveFile, packaging: FileAssetPackaging.FILE, extension: path.extname(archiveFile) }; + } +} diff --git a/packages/@aws-cdk/core/lib/bundling.ts b/packages/@aws-cdk/core/lib/bundling.ts index 0179b07d2f8f3..e3c1458aa0ab9 100644 --- a/packages/@aws-cdk/core/lib/bundling.ts +++ b/packages/@aws-cdk/core/lib/bundling.ts @@ -80,6 +80,41 @@ export interface BundlingOptions { * @experimental */ readonly local?: ILocalBundling; + + /** + * The type of output that this bundling operation is producing. + * + * @default BundlingOutput.AUTO_DISCOVER + * + * @experimental + */ + readonly outputType?: BundlingOutput; +} + +/** + * The type of output that a bundling operation is producing. + * + * @experimental + */ +export enum BundlingOutput { + /** + * The bundling output directory includes a single .zip or .jar file which + * will be used as the final bundle. If the output directory does not + * include exactly a single archive, bundling will fail. + */ + ARCHIVED = 'archived', + + /** + * The bundling output directory contains one or more files which will be + * archived and uploaded as a .zip file to S3. + */ + NOT_ARCHIVED = 'not-archived', + + /** + * If the bundling output directory contains a single archive file (zip or jar) + * it will be used as the bundle output as-is. Otherwise all the files in the bundling output directory will be zipped. + */ + AUTO_DISCOVER = 'auto-discover', } /** @@ -101,6 +136,8 @@ export interface ILocalBundling { /** * A Docker image used for asset bundling + * + * @deprecated use DockerImage */ export class BundlingDockerImage { /** @@ -117,6 +154,8 @@ export class BundlingDockerImage { * * @param path The path to the directory containing the Docker file * @param options Docker build options + * + * @deprecated use DockerImage.fromBuild() */ public static fromAsset(path: string, options: DockerBuildOptions = {}) { const buildArgs = options.buildArgs || {}; @@ -149,7 +188,7 @@ export class BundlingDockerImage { } /** @param image The Docker image */ - private constructor(public readonly image: string, private readonly _imageHash?: string) {} + protected constructor(public readonly image: string, private readonly _imageHash?: string) {} /** * Provides a stable representation of this image for JSON serialization. @@ -197,10 +236,16 @@ export class BundlingDockerImage { } /** - * Copies a file or directory out of the Docker image to the local filesystem + * Copies a file or directory out of the Docker image to the local filesystem. + * + * If `outputPath` is omitted the destination path is a temporary directory. + * + * @param imagePath the path in the Docker image + * @param outputPath the destination path for the copy operation + * @returns the destination path */ - public cp(imagePath: string, outputPath: string) { - const { stdout } = dockerExec(['create', this.image]); + public cp(imagePath: string, outputPath?: string): string { + const { stdout } = dockerExec(['create', this.image], {}); // Empty options to avoid stdout redirect here const match = stdout.toString().match(/([0-9a-f]{16,})/); if (!match) { throw new Error('Failed to extract container ID from Docker create output'); @@ -208,16 +253,33 @@ export class BundlingDockerImage { const containerId = match[1]; const containerPath = `${containerId}:${imagePath}`; + const destPath = outputPath ?? FileSystem.mkdtemp('cdk-docker-cp-'); try { - dockerExec(['cp', containerPath, outputPath]); + dockerExec(['cp', containerPath, destPath]); + return destPath; } catch (err) { - throw new Error(`Failed to copy files from ${containerPath} to ${outputPath}: ${err}`); + throw new Error(`Failed to copy files from ${containerPath} to ${destPath}: ${err}`); } finally { dockerExec(['rm', '-v', containerId]); } } } +/** + * A Docker image + */ +export class DockerImage extends BundlingDockerImage { + /** + * Builds a Docker image + * + * @param path The path to the directory containing the Docker file + * @param options Docker build options + */ + public static fromBuild(path: string, options: DockerBuildOptions = {}) { + return BundlingDockerImage.fromAsset(path, options); + } +} + /** * A Docker volume */ diff --git a/packages/@aws-cdk/core/lib/custom-resource-provider/custom-resource-provider.ts b/packages/@aws-cdk/core/lib/custom-resource-provider/custom-resource-provider.ts index d6b0a2db982c7..c7f3776339907 100644 --- a/packages/@aws-cdk/core/lib/custom-resource-provider/custom-resource-provider.ts +++ b/packages/@aws-cdk/core/lib/custom-resource-provider/custom-resource-provider.ts @@ -84,8 +84,20 @@ export interface CustomResourceProviderProps { export enum CustomResourceProviderRuntime { /** * Node.js 12.x + * + * @deprecated Use {@link NODEJS_12_X} + */ + NODEJS_12 = 'nodejs12.x', + + /** + * Node.js 12.x + */ + NODEJS_12_X = 'nodejs12.x', + + /** + * Node.js 14.x */ - NODEJS_12 = 'nodejs12.x' + NODEJS_14_X = 'nodejs14.x', } /** diff --git a/packages/@aws-cdk/core/lib/private/cfn-utils-provider.ts b/packages/@aws-cdk/core/lib/private/cfn-utils-provider.ts index dae7253720041..8200165fbfe34 100644 --- a/packages/@aws-cdk/core/lib/private/cfn-utils-provider.ts +++ b/packages/@aws-cdk/core/lib/private/cfn-utils-provider.ts @@ -7,7 +7,7 @@ import { CustomResourceProvider, CustomResourceProviderRuntime } from '../custom export class CfnUtilsProvider extends Construct { public static getOrCreate(scope: Construct) { return CustomResourceProvider.getOrCreate(scope, 'AWSCDKCfnUtilsProvider', { - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, codeDirectory: `${__dirname}/cfn-utils-provider`, }); } diff --git a/packages/@aws-cdk/core/test/archive/archive.zip b/packages/@aws-cdk/core/test/archive/archive.zip new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/@aws-cdk/core/test/bundling.test.ts b/packages/@aws-cdk/core/test/bundling.test.ts index cac2b494eb003..99548030c011a 100644 --- a/packages/@aws-cdk/core/test/bundling.test.ts +++ b/packages/@aws-cdk/core/test/bundling.test.ts @@ -3,7 +3,7 @@ import * as crypto from 'crypto'; import * as path from 'path'; import { nodeunitShim, Test } from 'nodeunit-shim'; import * as sinon from 'sinon'; -import { BundlingDockerImage, FileSystem } from '../lib'; +import { BundlingDockerImage, DockerImage, FileSystem } from '../lib'; nodeunitShim({ 'tearDown'(callback: any) { @@ -265,4 +265,25 @@ nodeunitShim({ test.ok(spawnSyncStub.calledWith(sinon.match.any, ['rm', '-v', containerId])); test.done(); }, + + 'cp utility copies to a temp dir of outputPath is omitted'(test: Test) { + // GIVEN + const containerId = '1234567890abcdef1234567890abcdef'; + sinon.stub(child_process, 'spawnSync').returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from(`${containerId}\n`), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + + // WHEN + const tempPath = DockerImage.fromRegistry('alpine').cp('/foo/bar'); + + // THEN + test.ok(/cdk-docker-cp-/.test(tempPath)); + + test.done(); + }, }); diff --git a/packages/@aws-cdk/core/test/custom-resource-provider/custom-resource-provider.test.ts b/packages/@aws-cdk/core/test/custom-resource-provider/custom-resource-provider.test.ts index 594f9c2936ff1..5fc12ecc17c2f 100644 --- a/packages/@aws-cdk/core/test/custom-resource-provider/custom-resource-provider.test.ts +++ b/packages/@aws-cdk/core/test/custom-resource-provider/custom-resource-provider.test.ts @@ -14,7 +14,7 @@ nodeunitShim({ // WHEN CustomResourceProvider.getOrCreate(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, }); // THEN @@ -149,7 +149,7 @@ nodeunitShim({ // WHEN CustomResourceProvider.getOrCreate(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, }); // THEN -- no exception @@ -167,7 +167,7 @@ nodeunitShim({ // WHEN CustomResourceProvider.getOrCreate(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, policyStatements: [ { statement1: 123 }, { statement2: { foo: 111 } }, @@ -194,7 +194,7 @@ nodeunitShim({ // WHEN CustomResourceProvider.getOrCreate(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, memorySize: Size.gibibytes(2), timeout: Duration.minutes(5), description: 'veni vidi vici', @@ -216,7 +216,7 @@ nodeunitShim({ // WHEN CustomResourceProvider.getOrCreate(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, environment: { B: 'b', A: 'a', @@ -242,7 +242,7 @@ nodeunitShim({ // WHEN const cr = CustomResourceProvider.getOrCreateProvider(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, }); // THEN diff --git a/packages/@aws-cdk/core/test/docker-stub.sh b/packages/@aws-cdk/core/test/docker-stub.sh index fe48e93d4a207..94f806f69a120 100755 --- a/packages/@aws-cdk/core/test/docker-stub.sh +++ b/packages/@aws-cdk/core/test/docker-stub.sh @@ -24,5 +24,18 @@ if echo "$@" | grep "DOCKER_STUB_SUCCESS"; then exit 0 fi -echo "Docker mock only supports one of the following commands: DOCKER_STUB_SUCCESS_NO_OUTPUT,DOCKER_STUB_FAIL,DOCKER_STUB_SUCCESS" +if echo "$@" | grep "DOCKER_STUB_MULTIPLE_FILES"; then + outdir=$(echo "$@" | xargs -n1 | grep "/asset-output" | head -n1 | cut -d":" -f1) + touch ${outdir}/test1.txt + touch ${outdir}/test2.txt + exit 0 +fi + +if echo "$@" | grep "DOCKER_STUB_SINGLE_ARCHIVE"; then + outdir=$(echo "$@" | xargs -n1 | grep "/asset-output" | head -n1 | cut -d":" -f1) + touch ${outdir}/test.zip + exit 0 +fi + +echo "Docker mock only supports one of the following commands: DOCKER_STUB_SUCCESS_NO_OUTPUT,DOCKER_STUB_FAIL,DOCKER_STUB_SUCCESS,DOCKER_STUB_MULTIPLE_FILES,DOCKER_SINGLE_ARCHIVE" exit 1 diff --git a/packages/@aws-cdk/core/test/staging.test.ts b/packages/@aws-cdk/core/test/staging.test.ts index 347c5fcea3b63..ee87780a0957e 100644 --- a/packages/@aws-cdk/core/test/staging.test.ts +++ b/packages/@aws-cdk/core/test/staging.test.ts @@ -1,10 +1,11 @@ import * as os from 'os'; import * as path from 'path'; +import { FileAssetPackaging } from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; import { nodeunitShim, Test } from 'nodeunit-shim'; import * as sinon from 'sinon'; -import { App, AssetHashType, AssetStaging, BundlingDockerImage, BundlingOptions, FileSystem, Stack, Stage } from '../lib'; +import { App, AssetHashType, AssetStaging, BundlingDockerImage, BundlingOptions, BundlingOutput, FileSystem, Stack, Stage } from '../lib'; const STUB_INPUT_FILE = '/tmp/docker-stub.input'; const STUB_INPUT_CONCAT_FILE = '/tmp/docker-stub.input.concat'; @@ -12,7 +13,9 @@ const STUB_INPUT_CONCAT_FILE = '/tmp/docker-stub.input.concat'; enum DockerStubCommand { SUCCESS = 'DOCKER_STUB_SUCCESS', FAIL = 'DOCKER_STUB_FAIL', - SUCCESS_NO_OUTPUT = 'DOCKER_STUB_SUCCESS_NO_OUTPUT' + SUCCESS_NO_OUTPUT = 'DOCKER_STUB_SUCCESS_NO_OUTPUT', + MULTIPLE_FILES = 'DOCKER_STUB_MULTIPLE_FILES', + SINGLE_ARCHIVE = 'DOCKER_STUB_SINGLE_ARCHIVE', } const FIXTURE_TEST1_DIR = path.join(__dirname, 'fs', 'fixtures', 'test1'); @@ -50,6 +53,84 @@ nodeunitShim({ test.deepEqual(staging.sourcePath, sourcePath); test.deepEqual(path.basename(staging.stagedPath), 'asset.2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00'); test.deepEqual(path.basename(staging.relativeStagedPath(stack)), 'asset.2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00'); + test.deepEqual(staging.packaging, FileAssetPackaging.ZIP_DIRECTORY); + test.deepEqual(staging.isArchive, true); + test.done(); + }, + + 'staging of an archive file correctly sets packaging and isArchive'(test: Test) { + // GIVEN + const stack = new Stack(); + const sourcePath = path.join(__dirname, 'archive', 'archive.zip'); + + // WHEN + const staging = new AssetStaging(stack, 's1', { sourcePath }); + + test.deepEqual(staging.packaging, FileAssetPackaging.FILE); + test.deepEqual(staging.isArchive, true); + test.done(); + }, + + 'asset packaging type is correct when staging is skipped because of memory cache'(test: Test) { + // GIVEN + const stack = new Stack(); + const sourcePath = path.join(__dirname, 'archive', 'archive.zip'); + + // WHEN + const staging1 = new AssetStaging(stack, 's1', { sourcePath }); + const staging2 = new AssetStaging(stack, 's2', { sourcePath }); + + test.deepEqual(staging1.packaging, FileAssetPackaging.FILE); + test.deepEqual(staging1.isArchive, true); + test.deepEqual(staging2.packaging, staging1.packaging); + test.deepEqual(staging2.isArchive, staging1.isArchive); + test.done(); + }, + + 'asset packaging type is correct when staging is skipped because of disk cache'(test: Test) { + // GIVEN + const TEST_OUTDIR = path.join(__dirname, 'cdk.out'); + if (fs.existsSync(TEST_OUTDIR)) { + fs.removeSync(TEST_OUTDIR); + } + + const sourcePath = path.join(__dirname, 'archive', 'archive.zip'); + + const app1 = new App({ outdir: TEST_OUTDIR }); + const stack1 = new Stack(app1, 'Stack'); + + const app2 = new App({ outdir: TEST_OUTDIR }); // same OUTDIR + const stack2 = new Stack(app2, 'stack'); + + // WHEN + const staging1 = new AssetStaging(stack1, 'Asset', { sourcePath }); + + // Now clear asset hash cache to show that during the second staging + // even though the asset is already available on disk it will correctly + // be considered as a FileAssetPackaging.FILE. + AssetStaging.clearAssetHashCache(); + + const staging2 = new AssetStaging(stack2, 'Asset', { sourcePath }); + + // THEN + test.deepEqual(staging1.packaging, FileAssetPackaging.FILE); + test.deepEqual(staging1.isArchive, true); + test.deepEqual(staging2.packaging, staging1.packaging); + test.deepEqual(staging2.isArchive, staging1.isArchive); + + test.done(); + }, + + 'staging of a non-archive file correctly sets packaging and isArchive'(test: Test) { + // GIVEN + const stack = new Stack(); + const sourcePath = __filename; + + // WHEN + const staging = new AssetStaging(stack, 's1', { sourcePath }); + + test.deepEqual(staging.packaging, FileAssetPackaging.FILE); + test.deepEqual(staging.isArchive, false); test.done(); }, @@ -785,6 +866,89 @@ nodeunitShim({ ); test.equal(asset.assetHash, '33cbf2cae5432438e0f046bc45ba8c3cef7b6afcf47b59d1c183775c1918fb1f'); // hash of MyStack/Asset + test.done(); + }, + + 'bundling that produces a single archive file is autodiscovered'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + const staging = new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SINGLE_ARCHIVE], + }, + }); + + // THEN + const assembly = app.synth(); + test.deepEqual(fs.readdirSync(assembly.directory), [ + 'asset.f43148c61174f444925231b5849b468f21e93b5d1469cd07c53625ffd039ef48', // this is the bundle dir but it's empty + 'asset.f43148c61174f444925231b5849b468f21e93b5d1469cd07c53625ffd039ef48.zip', + 'cdk.out', + 'manifest.json', + 'stack.template.json', + 'tree.json', + ]); + test.equal(fs.readdirSync(path.join(assembly.directory, 'asset.f43148c61174f444925231b5849b468f21e93b5d1469cd07c53625ffd039ef48')).length, 0); // empty bundle dir + test.deepEqual(staging.packaging, FileAssetPackaging.FILE); + test.deepEqual(staging.isArchive, true); + + test.done(); + }, + + 'bundling that produces a single archive file with NOT_ARCHIVED'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + const staging = new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SINGLE_ARCHIVE], + outputType: BundlingOutput.NOT_ARCHIVED, + }, + }); + + // THEN + const assembly = app.synth(); + test.deepEqual(fs.readdirSync(assembly.directory), [ + 'asset.86ec07746e1d859290cfd8b9c648e581555649c75f51f741f11e22cab6775abc', + 'cdk.out', + 'manifest.json', + 'stack.template.json', + 'tree.json', + ]); + test.deepEqual(staging.packaging, FileAssetPackaging.ZIP_DIRECTORY); + test.deepEqual(staging.isArchive, true); + + test.done(); + }, + + 'throws with ARCHIVED and bundling that does not produce a single archive file'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + test.throws(() => new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.MULTIPLE_FILES], + outputType: BundlingOutput.ARCHIVED, + }, + }), /Bundling output directory is expected to include only a single .zip or .jar file when `output` is set to `ARCHIVED`/); + + test.done(); }, }); diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index aed86e63bdb0f..0044f79eda383 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -84,7 +84,7 @@ "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "fs-extra": "^9.1.0", - "nock": "^13.0.7", + "nock": "^13.0.9", "pkglint": "0.0.0", "sinon": "^9.2.4" }, diff --git a/packages/@monocdk-experiment/assert/package.json b/packages/@monocdk-experiment/assert/package.json index dbd0d792e8a60..a4d825852a613 100644 --- a/packages/@monocdk-experiment/assert/package.json +++ b/packages/@monocdk-experiment/assert/package.json @@ -41,7 +41,7 @@ "jest": "^26.6.3", "monocdk": "0.0.0", "pkglint": "0.0.0", - "ts-jest": "^26.5.1" + "ts-jest": "^26.5.2" }, "dependencies": { "@aws-cdk/cloudformation-diff": "0.0.0" diff --git a/packages/aws-cdk/lib/init-templates/v1/app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py b/packages/aws-cdk/lib/init-templates/v1/app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py index 9d86ad16906e6..6b4ed6e8ea6ed 100644 --- a/packages/aws-cdk/lib/init-templates/v1/app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py +++ b/packages/aws-cdk/lib/init-templates/v1/app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py @@ -1,9 +1,15 @@ +from aws_cdk import core as cdk + +# For consistency with other languages, `cdk` is the preferred import name for +# the CDK's core module. The following line also imports it as `core` for use +# with examples from the CDK Developer's Guide, which are in the process of +# being updated to use `cdk`. You may delete this import if you don't need it. from aws_cdk import core -class %name.PascalCased%Stack(core.Stack): +class %name.PascalCased%Stack(cdk.Stack): - def __init__(self, scope: core.Construct, construct_id: str, **kwargs) -> None: + def __init__(self, scope: cdk.Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) # The code that defines your stack goes here diff --git a/packages/aws-cdk/lib/init-templates/v1/app/python/app.template.py b/packages/aws-cdk/lib/init-templates/v1/app/python/app.template.py index 808bc22af32e4..bc43099fd2026 100644 --- a/packages/aws-cdk/lib/init-templates/v1/app/python/app.template.py +++ b/packages/aws-cdk/lib/init-templates/v1/app/python/app.template.py @@ -1,11 +1,17 @@ #!/usr/bin/env python3 +from aws_cdk import core as cdk + +# For consistency with TypeScript code, `cdk` is the preferred import name for +# the CDK's core module. The following line also imports it as `core` for use +# with examples from the CDK Developer's Guide, which are in the process of +# being updated to use `cdk`. You may delete this import if you don't need it. from aws_cdk import core from %name.PythonModule%.%name.PythonModule%_stack import %name.PascalCased%Stack -app = core.App() +app = cdk.App() %name.PascalCased%Stack(app, "%name.StackName%") app.synth() diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index a3f34cdacf9d4..785087c8fb196 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -39,7 +39,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/core": "0.0.0", - "@octokit/rest": "^18.2.0", + "@octokit/rest": "^18.3.1", "@types/archiver": "^5.1.0", "@types/fs-extra": "^8.1.1", "@types/glob": "^7.1.3", @@ -59,10 +59,10 @@ "jest": "^26.6.3", "make-runnable": "^1.3.8", "mockery": "^2.1.0", - "nock": "^13.0.7", + "nock": "^13.0.9", "pkglint": "0.0.0", "sinon": "^9.2.4", - "ts-jest": "^26.5.1", + "ts-jest": "^26.5.2", "ts-mock-imports": "^1.3.3", "xml-js": "^1.6.11" }, diff --git a/packages/awslint/package.json b/packages/awslint/package.json index f0a74898f0148..3a5e17066742b 100644 --- a/packages/awslint/package.json +++ b/packages/awslint/package.json @@ -16,11 +16,11 @@ "awslint": "bin/awslint" }, "dependencies": { - "@jsii/spec": "^1.21.0", + "@jsii/spec": "^1.23.0", "camelcase": "^6.2.0", "colors": "^1.4.0", "fs-extra": "^9.1.0", - "jsii-reflect": "^1.21.0", + "jsii-reflect": "^1.23.0", "yargs": "^16.2.0" }, "devDependencies": { diff --git a/packages/cdk-dasm/package.json b/packages/cdk-dasm/package.json index 917a0b25715aa..a60843d4454b7 100644 --- a/packages/cdk-dasm/package.json +++ b/packages/cdk-dasm/package.json @@ -26,7 +26,7 @@ }, "license": "Apache-2.0", "dependencies": { - "codemaker": "^1.21.0", + "codemaker": "^1.23.0", "yaml": "1.10.0" }, "devDependencies": { diff --git a/packages/decdk/package.json b/packages/decdk/package.json index ec918d8d77feb..2fa2feeaabae7 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -210,7 +210,7 @@ "@aws-cdk/yaml-cfn": "0.0.0", "constructs": "^3.2.0", "fs-extra": "^9.1.0", - "jsii-reflect": "^1.21.0", + "jsii-reflect": "^1.23.0", "jsonschema": "^1.4.0", "yaml": "1.10.0", "yargs": "^16.2.0" @@ -221,7 +221,7 @@ "@types/yaml": "1.9.7", "@types/yargs": "^15.0.13", "jest": "^26.6.3", - "jsii": "^1.21.0" + "jsii": "^1.23.0" }, "keywords": [ "aws", diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index 8b2a6236620e9..c9bd2823410f3 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -39,11 +39,11 @@ "pkglint": "0.0.0" }, "dependencies": { - "@typescript-eslint/eslint-plugin": "^4.15.1", - "@typescript-eslint/parser": "^4.15.1", + "@typescript-eslint/eslint-plugin": "^4.16.1", + "@typescript-eslint/parser": "^4.16.1", "awslint": "0.0.0", "colors": "^1.4.0", - "eslint": "^7.20.0", + "eslint": "^7.21.0", "eslint-import-resolver-node": "^0.3.4", "eslint-import-resolver-typescript": "^2.4.0", "eslint-plugin-cdk": "0.0.0", @@ -51,13 +51,13 @@ "eslint-plugin-jest": "^24.1.5", "fs-extra": "^9.1.0", "jest": "^26.6.3", - "jsii": "^1.21.0", - "jsii-pacmak": "^1.21.0", - "markdownlint-cli": "^0.26.0", + "jsii": "^1.23.0", + "jsii-pacmak": "^1.23.0", + "markdownlint-cli": "^0.27.0", "nodeunit": "^0.11.3", "nyc": "^15.1.0", "semver": "^7.3.4", - "ts-jest": "^26.5.1", + "ts-jest": "^26.5.2", "typescript": "~3.9.9", "yargs": "^16.2.0", "yarn-cling": "0.0.0" diff --git a/tools/cfn2ts/package.json b/tools/cfn2ts/package.json index 7a62104df3ea9..bcee4f73d7020 100644 --- a/tools/cfn2ts/package.json +++ b/tools/cfn2ts/package.json @@ -30,7 +30,7 @@ "license": "Apache-2.0", "dependencies": { "@aws-cdk/cfnspec": "0.0.0", - "codemaker": "^1.21.0", + "codemaker": "^1.23.0", "fast-json-patch": "^3.0.0-1", "fs-extra": "^9.1.0", "yargs": "^16.2.0" diff --git a/tools/eslint-plugin-cdk/package.json b/tools/eslint-plugin-cdk/package.json index 2c412f30e739e..f21cb470ca5f5 100644 --- a/tools/eslint-plugin-cdk/package.json +++ b/tools/eslint-plugin-cdk/package.json @@ -21,8 +21,8 @@ "typescript": "~3.9.9" }, "dependencies": { - "@typescript-eslint/parser": "^4.15.1", - "eslint": "^7.20.0", + "@typescript-eslint/parser": "^4.16.1", + "eslint": "^7.21.0", "fs-extra": "^9.1.0" }, "jest": { diff --git a/yarn.lock b/yarn.lock index 0044085ed9ce5..a882be99918c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,36 +16,52 @@ dependencies: "@babel/highlight" "^7.12.13" +"@babel/compat-data@^7.13.8": + version "7.13.8" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.8.tgz#5b783b9808f15cef71547f1b691f34f8ff6003a6" + integrity sha512-EaI33z19T4qN3xLXsGf48M2cDqa6ei9tPZlfLdb2HC+e/cFtREiRd8hdSqDbwdLB0/+gLwqJmCYASH0z2bUdog== + "@babel/core@^7.1.0", "@babel/core@^7.7.5": - version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.17.tgz#993c5e893333107a2815d8e0d73a2c3755e280b2" - integrity sha512-V3CuX1aBywbJvV2yzJScRxeiiw0v2KZZYYE3giywxzFJL13RiyPjaaDwhDnxmgFTTS7FgvM2ijr4QmKNIu0AtQ== + version "7.13.8" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.8.tgz#c191d9c5871788a591d69ea1dc03e5843a3680fb" + integrity sha512-oYapIySGw1zGhEFRd6lzWNLWFX2s5dA/jm+Pw/+59ZdXtjyIuwlXbrId22Md0rgZVop+aVoqow2riXhBLNyuQg== dependencies: "@babel/code-frame" "^7.12.13" - "@babel/generator" "^7.12.17" - "@babel/helper-module-transforms" "^7.12.17" - "@babel/helpers" "^7.12.17" - "@babel/parser" "^7.12.17" + "@babel/generator" "^7.13.0" + "@babel/helper-compilation-targets" "^7.13.8" + "@babel/helper-module-transforms" "^7.13.0" + "@babel/helpers" "^7.13.0" + "@babel/parser" "^7.13.4" "@babel/template" "^7.12.13" - "@babel/traverse" "^7.12.17" - "@babel/types" "^7.12.17" + "@babel/traverse" "^7.13.0" + "@babel/types" "^7.13.0" convert-source-map "^1.7.0" debug "^4.1.0" - gensync "^1.0.0-beta.1" + gensync "^1.0.0-beta.2" json5 "^2.1.2" lodash "^4.17.19" - semver "^5.4.1" + semver "^6.3.0" source-map "^0.5.0" -"@babel/generator@^7.12.17", "@babel/generator@^7.4.0": - version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.17.tgz#9ef1dd792d778b32284411df63f4f668a9957287" - integrity sha512-DSA7ruZrY4WI8VxuS1jWSRezFnghEoYEFrZcw9BizQRmOZiUsiHl59+qEARGPqPikwA/GPTyRCi7isuCK/oyqg== +"@babel/generator@^7.13.0", "@babel/generator@^7.4.0": + version "7.13.9" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.9.tgz#3a7aa96f9efb8e2be42d38d80e2ceb4c64d8de39" + integrity sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw== dependencies: - "@babel/types" "^7.12.17" + "@babel/types" "^7.13.0" jsesc "^2.5.1" source-map "^0.5.0" +"@babel/helper-compilation-targets@^7.13.8": + version "7.13.8" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.8.tgz#02bdb22783439afb11b2f009814bdd88384bd468" + integrity sha512-pBljUGC1y3xKLn1nrx2eAhurLMA8OqBtBP/JwG4U8skN7kf8/aqwwxpV1N6T0e7r6+7uNitIa/fUxPFagSXp3A== + dependencies: + "@babel/compat-data" "^7.13.8" + "@babel/helper-validator-option" "^7.12.17" + browserslist "^4.14.5" + semver "^6.3.0" + "@babel/helper-function-name@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz#93ad656db3c3c2232559fd7b2c3dbdcbe0eb377a" @@ -62,12 +78,12 @@ dependencies: "@babel/types" "^7.12.13" -"@babel/helper-member-expression-to-functions@^7.12.13": - version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.17.tgz#f82838eb06e1235307b6d71457b6670ff71ee5ac" - integrity sha512-Bzv4p3ODgS/qpBE0DiJ9qf5WxSmrQ8gVTe8ClMfwwsY2x/rhykxxy3bXzG7AGTnPB2ij37zGJ/Q/6FruxHxsxg== +"@babel/helper-member-expression-to-functions@^7.13.0": + version "7.13.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.0.tgz#6aa4bb678e0f8c22f58cdb79451d30494461b091" + integrity sha512-yvRf8Ivk62JwisqV1rFRMxiSMDGnN6KH1/mDMmIrij4jztpQNRoHqqMG3U6apYbGRPJpgPalhva9Yd06HlUxJQ== dependencies: - "@babel/types" "^7.12.17" + "@babel/types" "^7.13.0" "@babel/helper-module-imports@^7.12.13": version "7.12.13" @@ -76,19 +92,19 @@ dependencies: "@babel/types" "^7.12.13" -"@babel/helper-module-transforms@^7.12.17": - version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.17.tgz#7c75b987d6dfd5b48e575648f81eaac891539509" - integrity sha512-sFL+p6zOCQMm9vilo06M4VHuTxUAwa6IxgL56Tq1DVtA0ziAGTH1ThmJq7xwPqdQlgAbKX3fb0oZNbtRIyA5KQ== +"@babel/helper-module-transforms@^7.13.0": + version "7.13.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.13.0.tgz#42eb4bd8eea68bab46751212c357bfed8b40f6f1" + integrity sha512-Ls8/VBwH577+pw7Ku1QkUWIyRRNHpYlts7+qSqBBFCW3I8QteB9DxfcZ5YJpOwH6Ihe/wn8ch7fMGOP1OhEIvw== dependencies: "@babel/helper-module-imports" "^7.12.13" - "@babel/helper-replace-supers" "^7.12.13" + "@babel/helper-replace-supers" "^7.13.0" "@babel/helper-simple-access" "^7.12.13" "@babel/helper-split-export-declaration" "^7.12.13" "@babel/helper-validator-identifier" "^7.12.11" "@babel/template" "^7.12.13" - "@babel/traverse" "^7.12.17" - "@babel/types" "^7.12.17" + "@babel/traverse" "^7.13.0" + "@babel/types" "^7.13.0" lodash "^4.17.19" "@babel/helper-optimise-call-expression@^7.12.13": @@ -99,19 +115,19 @@ "@babel/types" "^7.12.13" "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.8.0": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.12.13.tgz#174254d0f2424d8aefb4dd48057511247b0a9eeb" - integrity sha512-C+10MXCXJLiR6IeG9+Wiejt9jmtFpxUc3MQqCmPY8hfCjyUGl9kT+B2okzEZrtykiwrc4dbCPdDoz0A/HQbDaA== + version "7.13.0" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz#806526ce125aed03373bc416a828321e3a6a33af" + integrity sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ== -"@babel/helper-replace-supers@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.13.tgz#00ec4fb6862546bd3d0aff9aac56074277173121" - integrity sha512-pctAOIAMVStI2TMLhozPKbf5yTEXc0OJa0eENheb4w09SrgOWEs+P4nTOZYJQCqs8JlErGLDPDJTiGIp3ygbLg== +"@babel/helper-replace-supers@^7.13.0": + version "7.13.0" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.13.0.tgz#6034b7b51943094cb41627848cb219cb02be1d24" + integrity sha512-Segd5me1+Pz+rmN/NFBOplMbZG3SqRJOBlY+mA0SxAv6rjj7zJqr1AVr3SfzUVTLCv7ZLU5FycOM/SBGuLPbZw== dependencies: - "@babel/helper-member-expression-to-functions" "^7.12.13" + "@babel/helper-member-expression-to-functions" "^7.13.0" "@babel/helper-optimise-call-expression" "^7.12.13" - "@babel/traverse" "^7.12.13" - "@babel/types" "^7.12.13" + "@babel/traverse" "^7.13.0" + "@babel/types" "^7.13.0" "@babel/helper-simple-access@^7.12.13": version "7.12.13" @@ -132,28 +148,33 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== -"@babel/helpers@^7.12.17": +"@babel/helper-validator-option@^7.12.17": version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.12.17.tgz#71e03d2981a6b5ee16899964f4101dc8471d60bc" - integrity sha512-tEpjqSBGt/SFEsFikKds1sLNChKKGGR17flIgQKXH4fG6m9gTgl3gnOC1giHNyaBCSKuTfxaSzHi7UnvqiVKxg== + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz#d1fbf012e1a79b7eebbfdc6d270baaf8d9eb9831" + integrity sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw== + +"@babel/helpers@^7.13.0": + version "7.13.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.13.0.tgz#7647ae57377b4f0408bf4f8a7af01c42e41badc0" + integrity sha512-aan1MeFPxFacZeSz6Ld7YZo5aPuqnKlD7+HZY75xQsueczFccP9A7V05+oe0XpLwHK3oLorPe9eaAUljL7WEaQ== dependencies: "@babel/template" "^7.12.13" - "@babel/traverse" "^7.12.17" - "@babel/types" "^7.12.17" + "@babel/traverse" "^7.13.0" + "@babel/types" "^7.13.0" "@babel/highlight@^7.10.4", "@babel/highlight@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.12.13.tgz#8ab538393e00370b26271b01fa08f7f27f2e795c" - integrity sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww== + version "7.13.8" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.8.tgz#10b2dac78526424dfc1f47650d0e415dfd9dc481" + integrity sha512-4vrIhfJyfNf+lCtXC2ck1rKSzDwciqF7IWFhXXrSOUC2O5DrVp+w4c6ed4AllTxhTkUP5x2tYj41VaxdVMMRDw== dependencies: "@babel/helper-validator-identifier" "^7.12.11" chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.12.17", "@babel/parser@^7.4.3": - version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.17.tgz#bc85d2d47db38094e5bb268fc761716e7d693848" - integrity sha512-r1yKkiUTYMQ8LiEI0UcQx5ETw5dpTLn9wijn9hk6KkTtOK95FndDN10M+8/s6k/Ymlbivw0Av9q4SlgF80PtHg== +"@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.13.0", "@babel/parser@^7.13.4", "@babel/parser@^7.4.3": + version "7.13.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.9.tgz#ca34cb95e1c2dd126863a84465ae8ef66114be99" + integrity sha512-nEUfRiARCcaVo3ny3ZQjURjHQZUo/JkEw7rLlSZy/psWGnvwXFtPcr6jb7Yb41DVW5LTe6KRq9LGleRNsg1Frw== "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" @@ -248,25 +269,25 @@ "@babel/parser" "^7.12.13" "@babel/types" "^7.12.13" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.12.13", "@babel/traverse@^7.12.17", "@babel/traverse@^7.4.3": - version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.17.tgz#40ec8c7ffb502c4e54c7f95492dc11b88d718619" - integrity sha512-LGkTqDqdiwC6Q7fWSwQoas/oyiEYw6Hqjve5KOSykXkmFJFqzvGMb9niaUEag3Rlve492Mkye3gLw9FTv94fdQ== +"@babel/traverse@^7.1.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.4.3": + version "7.13.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.0.tgz#6d95752475f86ee7ded06536de309a65fc8966cc" + integrity sha512-xys5xi5JEhzC3RzEmSGrs/b3pJW/o87SypZ+G/PhaE7uqVQNv/jlmVIBXuoh5atqQ434LfXV+sf23Oxj0bchJQ== dependencies: "@babel/code-frame" "^7.12.13" - "@babel/generator" "^7.12.17" + "@babel/generator" "^7.13.0" "@babel/helper-function-name" "^7.12.13" "@babel/helper-split-export-declaration" "^7.12.13" - "@babel/parser" "^7.12.17" - "@babel/types" "^7.12.17" + "@babel/parser" "^7.13.0" + "@babel/types" "^7.13.0" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.19" -"@babel/types@^7.0.0", "@babel/types@^7.12.13", "@babel/types@^7.12.17", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.0": - version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.17.tgz#9d711eb807e0934c90b8b1ca0eb1f7230d150963" - integrity sha512-tNMDjcv/4DIcHxErTgwB9q2ZcYyN0sUfgGKUK/mm1FJK7Wz+KstoEekxrl/tBiNDgLK1HGi+sppj1An/1DR4fQ== +"@babel/types@^7.0.0", "@babel/types@^7.12.13", "@babel/types@^7.13.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.0": + version "7.13.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.0.tgz#74424d2816f0171b4100f0ab34e9a374efdf7f80" + integrity sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA== dependencies: "@babel/helper-validator-identifier" "^7.12.11" lodash "^4.17.19" @@ -290,10 +311,10 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@eslint/eslintrc@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.3.0.tgz#d736d6963d7003b6514e6324bec9c602ac340318" - integrity sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg== +"@eslint/eslintrc@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.0.tgz#99cc0a0584d72f1df38b900fb062ba995f395547" + integrity sha512-2ZPCc+uNbjV5ERJr+aKSPRwZgKd2z11x0EgLvb1PURmUrn9QNRXFqje0Ldq454PfAVyaJYyrDvvIKSFP4NnBog== dependencies: ajv "^6.12.4" debug "^4.1.1" @@ -302,7 +323,6 @@ ignore "^4.0.6" import-fresh "^3.2.1" js-yaml "^3.13.1" - lodash "^4.17.20" minimatch "^3.0.4" strip-json-comments "^3.1.1" @@ -567,10 +587,10 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@jsii/spec@^1.21.0": - version "1.21.0" - resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.21.0.tgz#363c740567747aa03083d2f7bf9ef14e55d8ae9d" - integrity sha512-MWQpJKciYytEmYzuwsT+4UM1JPiQyCAqr3PfkZxuosoPUaF7vBrWSs2+TXDb5dcCwpSnSim9iKZrM/Uc2ppUzA== +"@jsii/spec@^1.23.0": + version "1.23.0" + resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.23.0.tgz#c554cc77d0206f00ba4c101e96cb89d4ecec2985" + integrity sha512-+8Df/m/SRnThSAARlfxknwq1iWnCc2I1kThopuGQiz+peTRMoDVXaH7ZKT+54iZFIrbSf9Pjetho8g0/AZwrKg== dependencies: jsonschema "^1.4.0" @@ -1330,10 +1350,10 @@ "@octokit/types" "^6.0.3" universal-user-agent "^6.0.0" -"@octokit/openapi-types@^5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-5.1.0.tgz#661fd03c7d55fbcb0a0937d3353d87dea012f52c" - integrity sha512-bodZvSYgycbUuuKrC/anCBUExvaSSWzMMFz0xl7pcJujxnmGxvqvcFHktjx1ZOSyeNKLfYF0QCgibaHUGsZTng== +"@octokit/openapi-types@^5.2.1": + version "5.2.1" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-5.2.1.tgz#5e846f86104aef96ace20091d8afb6be27979d8a" + integrity sha512-Bf7MBvQ1nMpv15ANaQtRBsC7YnwQFPM0eUztp3luQs9L6sBEiQ6ArM1Wx5CG+N7tXETtd0oE0DMcU4wbLlCZIw== "@octokit/plugin-enterprise-rest@^6.0.1": version "6.0.1" @@ -1348,11 +1368,11 @@ "@octokit/types" "^2.0.1" "@octokit/plugin-paginate-rest@^2.6.2": - version "2.10.0" - resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.10.0.tgz#5925156d809c94b7bfc47b28e17488415548fa67" - integrity sha512-71OsKBSMcQEu/6lfVbhv5C5ikU1rn10rKot/WiV7do7fyfElQ2eCUQFogHPbj0ci5lnKAjvahOiMAr6lcvL8Qw== + version "2.11.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.11.0.tgz#3568c43896a3355f4a0bbb3a64f443b2abdc760d" + integrity sha512-7L9xQank2G3r1dGqrVPo1z62V5utbykOUzlmNHPz87Pww/JpZQ9KyG5CHtUzgmB4n5iDRKYNK/86A8D98HP0yA== dependencies: - "@octokit/types" "^6.10.0" + "@octokit/types" "^6.11.0" "@octokit/plugin-request-log@^1.0.0", "@octokit/plugin-request-log@^1.0.2": version "1.0.3" @@ -1367,12 +1387,12 @@ "@octokit/types" "^2.0.1" deprecation "^2.3.1" -"@octokit/plugin-rest-endpoint-methods@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.12.0.tgz#1cec405cd4eaf0bdb58cb7d2a9b3d8473b3a70e8" - integrity sha512-RgnQ1aoetdOJjZYC37LV5FNlL7GY/v1CdC5dur1Zp/UiADJlbRFbAz/xLx26ovXw67dK7EUtwCghS+6QyiI9RA== +"@octokit/plugin-rest-endpoint-methods@4.13.1": + version "4.13.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.13.1.tgz#d8c807bbd0e187ac903620f53321e2634818bb30" + integrity sha512-T9YhQqpbO9Onmg+FYk09uci9pfChg8CZR9GBaPJWj+bDSzictW1xnU0NtCSSKKyrwvpW/opu7CtuDSs/HF1Syg== dependencies: - "@octokit/types" "^6.10.0" + "@octokit/types" "^6.11.1" deprecation "^2.3.1" "@octokit/request-error@^1.0.2": @@ -1429,15 +1449,15 @@ once "^1.4.0" universal-user-agent "^4.0.0" -"@octokit/rest@^18.2.0": - version "18.2.0" - resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.2.0.tgz#b75c87870bb1f7bc9f37ae0e9acb3a411a34a25f" - integrity sha512-xsp6bIqL2sb/NmgLXTxw96caegobRw+YHnzdIi70ruquHtPPDW2cBAONhDYMUuAOeXx0JH2auOeplpk4SQJy1w== +"@octokit/rest@^18.3.1": + version "18.3.1" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.3.1.tgz#6680331e941c422dbff0e758a9bd3dc4edcbd2db" + integrity sha512-g57ebsk7dtbLjiPBgEYDAiDTsyQM9kvlIt0J5UN6OSjG82K6fQQck6HXPpwcyNIDqbN7lIaWr3nsz56jBfI6qg== dependencies: "@octokit/core" "^3.2.3" "@octokit/plugin-paginate-rest" "^2.6.2" "@octokit/plugin-request-log" "^1.0.2" - "@octokit/plugin-rest-endpoint-methods" "4.12.0" + "@octokit/plugin-rest-endpoint-methods" "4.13.1" "@octokit/types@^2.0.0", "@octokit/types@^2.0.1": version "2.16.2" @@ -1446,12 +1466,12 @@ dependencies: "@types/node" ">= 8" -"@octokit/types@^6.0.3", "@octokit/types@^6.10.0", "@octokit/types@^6.7.1": - version "6.10.0" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.10.0.tgz#243faa864b0955f574012d52e179de38ac9ebafe" - integrity sha512-aMDo10kglofejJ96edCBIgQLVuzMDyjxmhdgEcoUUD64PlHYSrNsAGqN0wZtoiX4/PCQ3JLA50IpkP1bcKD/cA== +"@octokit/types@^6.0.3", "@octokit/types@^6.11.0", "@octokit/types@^6.11.1", "@octokit/types@^6.7.1": + version "6.11.1" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.11.1.tgz#54ece128029526fa99bd71e757b9e35478403d95" + integrity sha512-UiSRTG2lrFbMUMwhKNR0uSV33Fzv4bNu1n5iFuuNOg80XCh0VYNhR4TQWgrkLhVxdDuej6s61aP3eQvVm6K6uA== dependencies: - "@octokit/openapi-types" "^5.1.0" + "@octokit/openapi-types" "^5.2.1" "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.1": version "1.8.2" @@ -1761,13 +1781,13 @@ resolved "https://registry.yarnpkg.com/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.4.tgz#445251eb00bd9c1e751f82c7c6bf4f714edfd464" integrity sha512-/emrKCfQMQmFCqRqqBJ0JueHBT06jBRM3e8OgnvDUcvuExONujIk2hFA5dNsN9Nt41ljGVDdChvCydATZ+KOZw== -"@typescript-eslint/eslint-plugin@^4.15.1": - version "4.15.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.15.1.tgz#835f64aa0a403e5e9e64c10ceaf8d05c3f015180" - integrity sha512-yW2epMYZSpNJXZy22Biu+fLdTG8Mn6b22kR3TqblVk50HGNV8Zya15WAXuQCr8tKw4Qf1BL4QtI6kv6PCkLoJw== +"@typescript-eslint/eslint-plugin@^4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.16.1.tgz#2caf6a79dd19c3853b8d39769a27fccb24e4e651" + integrity sha512-SK777klBdlkUZpZLC1mPvyOWk9yAFCWmug13eAjVQ4/Q1LATE/NbcQL1xDHkptQkZOLnPmLUA1Y54m8dqYwnoQ== dependencies: - "@typescript-eslint/experimental-utils" "4.15.1" - "@typescript-eslint/scope-manager" "4.15.1" + "@typescript-eslint/experimental-utils" "4.16.1" + "@typescript-eslint/scope-manager" "4.16.1" debug "^4.1.1" functional-red-black-tree "^1.0.1" lodash "^4.17.15" @@ -1775,60 +1795,60 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@4.15.1", "@typescript-eslint/experimental-utils@^4.0.1": - version "4.15.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.15.1.tgz#d744d1ac40570a84b447f7aa1b526368afd17eec" - integrity sha512-9LQRmOzBRI1iOdJorr4jEnQhadxK4c9R2aEAsm7WE/7dq8wkKD1suaV0S/JucTL8QlYUPU1y2yjqg+aGC0IQBQ== +"@typescript-eslint/experimental-utils@4.16.1", "@typescript-eslint/experimental-utils@^4.0.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.16.1.tgz#da7a396dc7d0e01922acf102b76efff17320b328" + integrity sha512-0Hm3LSlMYFK17jO4iY3un1Ve9x1zLNn4EM50Lia+0EV99NdbK+cn0er7HC7IvBA23mBg3P+8dUkMXy4leL33UQ== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/scope-manager" "4.15.1" - "@typescript-eslint/types" "4.15.1" - "@typescript-eslint/typescript-estree" "4.15.1" + "@typescript-eslint/scope-manager" "4.16.1" + "@typescript-eslint/types" "4.16.1" + "@typescript-eslint/typescript-estree" "4.16.1" eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/parser@^4.15.1": - version "4.15.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.15.1.tgz#4c91a0602733db63507e1dbf13187d6c71a153c4" - integrity sha512-V8eXYxNJ9QmXi5ETDguB7O9diAXlIyS+e3xzLoP/oVE4WCAjssxLIa0mqCLsCGXulYJUfT+GV70Jv1vHsdKwtA== +"@typescript-eslint/parser@^4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.16.1.tgz#3bbd3234dd3c5b882b2bcd9899bc30e1e1586d2a" + integrity sha512-/c0LEZcDL5y8RyI1zLcmZMvJrsR6SM1uetskFkoh3dvqDKVXPsXI+wFB/CbVw7WkEyyTKobC1mUNp/5y6gRvXg== dependencies: - "@typescript-eslint/scope-manager" "4.15.1" - "@typescript-eslint/types" "4.15.1" - "@typescript-eslint/typescript-estree" "4.15.1" + "@typescript-eslint/scope-manager" "4.16.1" + "@typescript-eslint/types" "4.16.1" + "@typescript-eslint/typescript-estree" "4.16.1" debug "^4.1.1" -"@typescript-eslint/scope-manager@4.15.1": - version "4.15.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.15.1.tgz#f6511eb38def2a8a6be600c530c243bbb56ac135" - integrity sha512-ibQrTFcAm7yG4C1iwpIYK7vDnFg+fKaZVfvyOm3sNsGAerKfwPVFtYft5EbjzByDJ4dj1WD8/34REJfw/9wdVA== +"@typescript-eslint/scope-manager@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.16.1.tgz#244e2006bc60cfe46987e9987f4ff49c9e3f00d5" + integrity sha512-6IlZv9JaurqV0jkEg923cV49aAn8V6+1H1DRfhRcvZUrptQ+UtSKHb5kwTayzOYTJJ/RsYZdcvhOEKiBLyc0Cw== dependencies: - "@typescript-eslint/types" "4.15.1" - "@typescript-eslint/visitor-keys" "4.15.1" + "@typescript-eslint/types" "4.16.1" + "@typescript-eslint/visitor-keys" "4.16.1" -"@typescript-eslint/types@4.15.1": - version "4.15.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.15.1.tgz#da702f544ef1afae4bc98da699eaecd49cf31c8c" - integrity sha512-iGsaUyWFyLz0mHfXhX4zO6P7O3sExQpBJ2dgXB0G5g/8PRVfBBsmQIc3r83ranEQTALLR3Vko/fnCIVqmH+mPw== +"@typescript-eslint/types@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.16.1.tgz#5ba2d3e38b1a67420d2487519e193163054d9c15" + integrity sha512-nnKqBwMgRlhzmJQF8tnFDZWfunXmJyuXj55xc8Kbfup4PbkzdoDXZvzN8//EiKR27J6vUSU8j4t37yUuYPiLqA== -"@typescript-eslint/typescript-estree@4.15.1": - version "4.15.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.15.1.tgz#fa9a9ff88b4a04d901ddbe5b248bc0a00cd610be" - integrity sha512-z8MN3CicTEumrWAEB2e2CcoZa3KP9+SMYLIA2aM49XW3cWIaiVSOAGq30ffR5XHxRirqE90fgLw3e6WmNx5uNw== +"@typescript-eslint/typescript-estree@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.16.1.tgz#c2fc46b05a48fbf8bbe8b66a63f0a9ba04b356f1" + integrity sha512-m8I/DKHa8YbeHt31T+UGd/l8Kwr0XCTCZL3H4HMvvLCT7HU9V7yYdinTOv1gf/zfqNeDcCgaFH2BMsS8x6NvJg== dependencies: - "@typescript-eslint/types" "4.15.1" - "@typescript-eslint/visitor-keys" "4.15.1" + "@typescript-eslint/types" "4.16.1" + "@typescript-eslint/visitor-keys" "4.16.1" debug "^4.1.1" globby "^11.0.1" is-glob "^4.0.1" semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/visitor-keys@4.15.1": - version "4.15.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.15.1.tgz#c76abbf2a3be8a70ed760f0e5756bf62de5865dd" - integrity sha512-tYzaTP9plooRJY8eNlpAewTOqtWW/4ff/5wBjNVaJ0S0wC4Gpq/zDVRTJa5bq2v1pCNQ08xxMCndcvR+h7lMww== +"@typescript-eslint/visitor-keys@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.16.1.tgz#d7571fb580749fae621520deeb134370bbfc7293" + integrity sha512-s/aIP1XcMkEqCNcPQtl60ogUYjSM8FU2mq1O7y5cFf3Xcob1z1iXWNB6cC43Op+NGRTFgGolri6s8z/efA9i1w== dependencies: - "@typescript-eslint/types" "4.15.1" + "@typescript-eslint/types" "4.16.1" eslint-visitor-keys "^2.0.0" "@yarnpkg/lockfile@^1.1.0": @@ -2265,9 +2285,9 @@ aws-sdk-mock@^5.1.0: traverse "^0.6.6" aws-sdk@^2.596.0, aws-sdk@^2.637.0, aws-sdk@^2.848.0: - version "2.848.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.848.0.tgz#5e7706ddd30a55a2d5a5b64c29682a757607ee64" - integrity sha512-c/e5kaEFl+9aYkrYDkmu5mSZlL+EfP6DnBOMD06fH12gIsaFSMBGtbsDTHABhvSu++LxeI1dJAD148O17MuZvg== + version "2.854.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.854.0.tgz#531525cdfdfc774232d86619d2c2c2ae3e1a71a8" + integrity sha512-Ex/YJcRB4avEW64UZ7uJ7aBxN8qFPuoSaG7YpLWUGJv+p9Vsqm0v0IqmMo7O5/s2LpIXRG8mLi3UbNZYz2iGhg== dependencies: buffer "4.9.2" events "1.1.1" @@ -2393,9 +2413,9 @@ before-after-hook@^2.0.0, before-after-hook@^2.1.0: integrity sha512-5ekuQOvO04MDj7kYZJaMab2S8SPjGJbotVNyv7QYFCOAwrGZs/YnoDNlh1U+m5hl7H2D/+n0taaAV/tfyd3KMA== bind-obj-methods@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/bind-obj-methods/-/bind-obj-methods-2.0.0.tgz#0178140dbe7b7bb67dc74892ace59bc0247f06f0" - integrity sha512-3/qRXczDi2Cdbz6jE+W3IflJOutRVica8frpBn14de1mBOkzDo+6tY33kNhvkw54Kn3PzRRD2VnGbGPcTAk4sw== + version "2.0.1" + resolved "https://registry.yarnpkg.com/bind-obj-methods/-/bind-obj-methods-2.0.1.tgz#1c1295d6741c07b78d15f42080fe4a60a27f91f5" + integrity sha512-kKzUyCuc+jsWH4C2nW5KB2nh+rQRbQcdphfo9UN3j1uwIFGZ3JB8njtRZOiUAQCkxazH0nDQPN6x/zhvFcbZIw== bl@^4.0.3: version "4.1.0" @@ -2447,6 +2467,17 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== +browserslist@^4.14.5: + version "4.16.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717" + integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw== + dependencies: + caniuse-lite "^1.0.30001181" + colorette "^1.2.1" + electron-to-chromium "^1.3.649" + escalade "^3.1.1" + node-releases "^1.1.70" + bs-logger@0.x: version "0.2.6" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" @@ -2652,6 +2683,11 @@ camelcase@^6.0.0, camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== +caniuse-lite@^1.0.30001181: + version "1.0.30001194" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001194.tgz#3d16ff3d734a5a7d9818402c28b1f636c5be5bed" + integrity sha512-iDUOH+oFeBYk5XawYsPtsx/8fFpndAPUQJC7gBTfxHM8xw5nOZv7ceAD4frS1MKCLUac7QL5wdAJiFQlDRjXlA== + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -2832,10 +2868,10 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= -codemaker@^1.21.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.21.0.tgz#3dd1c236b6af3d8ac90f99e300e56e80c799ea76" - integrity sha512-YxTt3lWcR6PC/3fByU7FGjIoUDOcTs1KmqRJcK14xN9X7wxBOWO129WuSTm/4XfKlz/3iSo9CtRX/5HYkE1oCQ== +codemaker@^1.23.0: + version "1.23.0" + resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.23.0.tgz#2d2168e70829496cff0549983b6ea7b7f843c1a3" + integrity sha512-VpHeRuvibqJbayWj9xHb15yKRLBOJUrZuhsmH868m6xGz5FXwLD1aUdSY9rqaNI0df8FfFyCYhk5f/A+bi0/wg== dependencies: camelcase "^6.2.0" decamelize "^5.0.0" @@ -2883,6 +2919,11 @@ color-support@^1.1.0: resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== +colorette@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" + integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== + colors@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" @@ -2903,10 +2944,10 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@~6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" - integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== +commander@~7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.1.0.tgz#f2eaecf131f10e36e07d894698226e36ae0eb5ff" + integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg== commondir@^1.0.1: version "1.0.1" @@ -2985,9 +3026,9 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= constructs@^3.2.0: - version "3.3.29" - resolved "https://registry.yarnpkg.com/constructs/-/constructs-3.3.29.tgz#42d2fbc0d872a47701a70583a0d4356a3f11955c" - integrity sha512-rGQzkq2M/qKZ0hMEtt4YPpsZKOwzmiyAQx3PqexXXsjdVnTqEfIwQuDpc+1jP6CtaBHl7rR6CxQcfsP5DmaERw== + version "3.3.48" + resolved "https://registry.yarnpkg.com/constructs/-/constructs-3.3.48.tgz#56446b7bd8da8023f33a2e4c1bc7d2df3971e6eb" + integrity sha512-5AKrTtmiioQWloJ3WRFZb0/uR1lrRboaVE9go++XZltvRnZkN2/kQjaZ0gtFxynU5u5k9mWVtk8mNcgJ9yoRbQ== contains-path@^0.1.0: version "0.1.0" @@ -3740,6 +3781,11 @@ ejs@^2.5.2: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba" integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== +electron-to-chromium@^1.3.649: + version "1.3.677" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.677.tgz#b5d586b0d1976c97cc7e95262677ac5944199513" + integrity sha512-Tcmk+oKQgpjcM+KYanlkd76ZtpzalkpUULnlJDP6vjHtR7UU564IM9Qv5DxqHZNBQjzXm6mkn7Y8bw2OoE3FmQ== + emittery@^0.7.1: version "0.7.2" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82" @@ -3776,11 +3822,16 @@ enquirer@^2.3.5: dependencies: ansi-colors "^4.1.1" -entities@~2.0, entities@~2.0.0: +entities@~2.0: version "2.0.3" resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== +entities@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" + integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== + env-paths@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43" @@ -3804,24 +3855,26 @@ error-ex@^1.2.0, error-ex@^1.3.1: is-arrayish "^0.2.1" es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2: - version "1.18.0-next.2" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.2.tgz#088101a55f0541f595e7e057199e27ddc8f3a5c2" - integrity sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw== + version "1.18.0-next.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.3.tgz#56bc8b5cc36b2cca25a13be07f3c02c2343db6b7" + integrity sha512-VMzHx/Bczjg59E6jZOQjHeN3DEoptdhejpARgflAViidlqSpjdq9zA6lKwlhRRs/lOw1gHJv2xkkSFRgvEwbQg== dependencies: call-bind "^1.0.2" es-to-primitive "^1.2.1" function-bind "^1.1.1" - get-intrinsic "^1.0.2" + get-intrinsic "^1.1.1" has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" + has-symbols "^1.0.2" + is-callable "^1.2.3" is-negative-zero "^2.0.1" - is-regex "^1.1.1" + is-regex "^1.1.2" + is-string "^1.0.5" object-inspect "^1.9.0" object-keys "^1.1.1" object.assign "^4.1.2" - string.prototype.trimend "^1.0.3" - string.prototype.trimstart "^1.0.3" + string.prototype.trimend "^1.0.4" + string.prototype.trimstart "^1.0.4" + unbox-primitive "^1.0.0" es-get-iterator@^1.1.1: version "1.1.2" @@ -3868,10 +3921,10 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" -esbuild@^0.8.50: - version "0.8.50" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.8.50.tgz#ebf24fde0cdad1a369789dd6fd7a820b0a01e46c" - integrity sha512-oidFLXssA7IccYzkqLVZSqNJDwDq8Mh/vqvrW+3fPWM7iUiC5O2bCllhnO8+K9LlyL/2Z6n+WwRJAz9fqSIVRg== +esbuild@^0.8.54: + version "0.8.54" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.8.54.tgz#2f32ff80e95c69a0f25b799d76a27c05e2857cdf" + integrity sha512-DJH38OiTgXJxFb/EhHrCrY8eGmtdkTtWymHpN9IYN9AF+4jykT0dQArr7wzFejpVbaB0TMIq2+vfNRWr3LXpvw== escalade@^3.1.1: version "3.1.1" @@ -4023,13 +4076,13 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== -eslint@^7.20.0: - version "7.20.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.20.0.tgz#db07c4ca4eda2e2316e7aa57ac7fc91ec550bdc7" - integrity sha512-qGi0CTcOGP2OtCQBgWZlQjcTuP0XkIpYFj25XtRTQSHC+umNnp7UMshr2G8SLsRFYDdAPFeHOsiteadmMH02Yw== +eslint@^7.21.0: + version "7.21.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.21.0.tgz#4ecd5b8c5b44f5dedc9b8a110b01bbfeb15d1c83" + integrity sha512-W2aJbXpMNofUp0ztQaF40fveSsJBjlSCSWpy//gzfTvwC+USs/nceBrKmlJOiM8r1bLwP2EuYkCqArn/6QTIgg== dependencies: "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.3.0" + "@eslint/eslintrc" "^0.4.0" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" @@ -4042,7 +4095,7 @@ eslint@^7.20.0: espree "^7.3.1" esquery "^1.4.0" esutils "^2.0.2" - file-entry-cache "^6.0.0" + file-entry-cache "^6.0.1" functional-red-black-tree "^1.0.1" glob-parent "^5.0.0" globals "^12.1.0" @@ -4314,9 +4367,9 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= fastq@^1.6.0: - version "1.10.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.10.1.tgz#8b8f2ac8bf3632d67afcd65dac248d5fdc45385e" - integrity sha512-AWuv6Ery3pM+dY7LYS8YIaCiQvUaos9OB1RyNgaOWnaX+Tik7Onvcsf8x8c+YtDeT0maYLniBip2hox5KtEXXA== + version "1.11.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" + integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== dependencies: reusify "^1.0.4" @@ -4346,7 +4399,7 @@ figures@^3.1.0: dependencies: escape-string-regexp "^1.0.5" -file-entry-cache@^6.0.0: +file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== @@ -4471,9 +4524,9 @@ flush-write-stream@^1.0.0: readable-stream "^2.3.6" follow-redirects@^1.10.0, follow-redirects@^1.11.0: - version "1.13.2" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.2.tgz#dd73c8effc12728ba5cf4259d760ea5fb83e3147" - integrity sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA== + version "1.13.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267" + integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA== for-in@^1.0.2: version "1.0.2" @@ -4640,7 +4693,7 @@ genfun@^5.0.0: resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" integrity sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA== -gensync@^1.0.0-beta.1: +gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== @@ -4916,6 +4969,11 @@ hard-rejection@^2.1.0: resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== +has-bigints@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" + integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -4926,10 +4984,10 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== +has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" + integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== has-unicode@^2.0.0, has-unicode@^2.0.1: version "2.0.1" @@ -5307,7 +5365,7 @@ is-buffer@^1.1.5, is-buffer@~1.1.6: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-callable@^1.1.4, is-callable@^1.2.2: +is-callable@^1.1.4, is-callable@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== @@ -5495,7 +5553,7 @@ is-potential-custom-element-name@^1.0.0: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= -is-regex@^1.1.1: +is-regex@^1.1.1, is-regex@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== @@ -6135,7 +6193,7 @@ js-tokens@^4.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.1, js-yaml@^3.2.7, js-yaml@~3.14.1: +js-yaml@^3.13.1, js-yaml@^3.2.7: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -6143,6 +6201,13 @@ js-yaml@^3.13.1, js-yaml@^3.2.7, js-yaml@~3.14.1: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f" + integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q== + dependencies: + argparse "^2.0.1" + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -6185,65 +6250,65 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -jsii-diff@^1.21.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.21.0.tgz#4e7f5b38fa42b4ee109592686ec5d5b168491c0b" - integrity sha512-88YUb9FO2jb6We9fTowR2k1+YhnFJ1LYDOOw7ThyyNyjsgEAheOV5vB4u43HTduZncVmncUNjcMirrUDm2JDzA== +jsii-diff@^1.23.0: + version "1.23.0" + resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.23.0.tgz#57fc832155e4739fbfc7a235b177c305e9cb6f67" + integrity sha512-1g41UE2yJK8E50eDo4JSJQZRlfmUzWDw0D7DF40Z6upbay3jQKWjerXbJmD9RvJkD6I8khAoEnI77daw1AgQTw== dependencies: - "@jsii/spec" "^1.21.0" + "@jsii/spec" "^1.23.0" fs-extra "^9.1.0" - jsii-reflect "^1.21.0" + jsii-reflect "^1.23.0" log4js "^6.3.0" typescript "~3.9.9" yargs "^16.2.0" -jsii-pacmak@^1.21.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.21.0.tgz#b9da4b5d2e980bc75ec37336175a2b99752d677f" - integrity sha512-04/fIZqM31cfTf48v7ni7MGeAwBEREP1WhvGkf4TSAZmAdMx1FUWQxuKiDK1/YeEvIUhNHIy/Ng9GcoSf+Rwfg== +jsii-pacmak@^1.23.0: + version "1.23.0" + resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.23.0.tgz#aedc0f17dc420da5be9446189eae5096fe170bd3" + integrity sha512-LchV8amHqWHjypbPLDZqdLkmp0rVYJYhFbS88sApvO4TaDdh0C3KnbEjNf3Jn/JpPXxd2PUNtCw8D9fDy7zwVw== dependencies: - "@jsii/spec" "^1.21.0" + "@jsii/spec" "^1.23.0" clone "^2.1.2" - codemaker "^1.21.0" + codemaker "^1.23.0" commonmark "^0.29.3" escape-string-regexp "^4.0.0" fs-extra "^9.1.0" - jsii-reflect "^1.21.0" - jsii-rosetta "^1.21.0" + jsii-reflect "^1.23.0" + jsii-rosetta "^1.23.0" semver "^7.3.4" spdx-license-list "^6.4.0" xmlbuilder "^15.1.1" yargs "^16.2.0" -jsii-reflect@^1.21.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.21.0.tgz#2f6f7835d7428c5cc9d2c81f1c881e613710bc78" - integrity sha512-OwXhVhe+NRv/e6jaGBdIpm3S1KQcEXTZN+USiBd+c4kROLqxw+ubpMBsEVSKEZ7t+4WksLTWWNot31VZkJrZ5g== +jsii-reflect@^1.23.0: + version "1.23.0" + resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.23.0.tgz#fd046e25e4d89887bc0ea7416d68e42568c6689e" + integrity sha512-jxQnz5hm5tk7l85iWgfyuxnB+mpzmj/y96AIkSbbOfHDeHpLG7h+yOROZoe2K8tUcHKamA70p+gNcZOETZP3OA== dependencies: - "@jsii/spec" "^1.21.0" + "@jsii/spec" "^1.23.0" colors "^1.4.0" fs-extra "^9.1.0" - oo-ascii-tree "^1.21.0" + oo-ascii-tree "^1.23.0" yargs "^16.2.0" -jsii-rosetta@^1.21.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.21.0.tgz#591ebb2ce390b81f269a66d906446f8bebe9e23b" - integrity sha512-8W0vcWTr28q+1NWhVAY4lOwOlPHdGdg8b/gPHFccRi9ZM4uwRjW7YjmqD9FmX74dEg1Qmvd8nujW4Opow6PFtQ== +jsii-rosetta@^1.23.0: + version "1.23.0" + resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.23.0.tgz#fd62dbf84b16cb119c80aca4397ab5d6ee02f071" + integrity sha512-LBFq1LBrq97MUU5T9sgrIPq0wMWUCsUt3tnZEpjIDXPHYptVVn7HmKbu5PjG47GbNTOcsY/nP2r7N3RFrG881g== dependencies: - "@jsii/spec" "^1.21.0" + "@jsii/spec" "^1.23.0" commonmark "^0.29.3" fs-extra "^9.1.0" typescript "~3.9.9" xmldom "^0.4.0" yargs "^16.2.0" -jsii@^1.21.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.21.0.tgz#6fd5dd9a18bb820a127ab51f55b2081cf14b2181" - integrity sha512-6siaRt1OyrQxC9pzLaFGj6bDkHMTsofcu8ODM0NCcukq2P4PlF1O39H0DV8Z40QF3KWbawJ/Utl7GtaSrdG2Ww== +jsii@^1.23.0: + version "1.23.0" + resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.23.0.tgz#12e04a565114024bc6721f6482e9a362bbf50926" + integrity sha512-vwwFEt+7wXtex8G1ztWIuOPDhahZA/a+XvtiucZYnIO7VwkmeqAxrgwdBIrQoH2GJKYs55kMnDa4+yfnTGcOgw== dependencies: - "@jsii/spec" "^1.21.0" + "@jsii/spec" "^1.23.0" case "^1.6.3" colors "^1.4.0" deep-equal "^2.0.5" @@ -6804,48 +6869,48 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -markdown-it@12.0.2: - version "12.0.2" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.0.2.tgz#4401beae8df8aa2221fc6565a7188e60a06ef0ed" - integrity sha512-4Lkvjbv2kK+moL9TbeV+6/NHx+1Q+R/NIdUlFlkqkkzUcTod4uiyTJRiBidKR9qXSdkNFkgv+AELY8KN9vSgVA== +markdown-it@12.0.4: + version "12.0.4" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.0.4.tgz#eec8247d296327eac3ba9746bdeec9cfcc751e33" + integrity sha512-34RwOXZT8kyuOJy25oJNJoulO8L0bTHYWXcdZBYZqFnjIy3NgjeoM3FmPXIOFQ26/lSHYMr8oc62B6adxXcb3Q== dependencies: argparse "^2.0.1" - entities "~2.0.0" + entities "~2.1.0" linkify-it "^3.0.1" mdurl "^1.0.1" uc.micro "^1.0.5" -markdownlint-cli@^0.26.0: - version "0.26.0" - resolved "https://registry.yarnpkg.com/markdownlint-cli/-/markdownlint-cli-0.26.0.tgz#cd89e3e39a049303ec125c8aa291da4f3325df29" - integrity sha512-biLfeGNZG9nw0yJbtFBzRlew2/P5w7JSseKwolSox3zejs7dLpGvPgqbC+iqJnqqGWcWLtXaXh8bBEKWmfl10A== +markdownlint-cli@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/markdownlint-cli/-/markdownlint-cli-0.27.0.tgz#d89264319ef7ae6a02e0b2fd472fbbb11bde47f7" + integrity sha512-lci/GD1V2aysJOpJFXfIt5KlpF62Rw+7hUfGg3ZwkLBBeNuUwLZj/IomQ924m00Ido/tPGbFObelKlZaUlETpQ== dependencies: - commander "~6.2.1" + commander "~7.1.0" deep-extend "~0.6.0" get-stdin "~8.0.0" glob "~7.1.6" ignore "~5.1.8" - js-yaml "~3.14.1" + js-yaml "^4.0.0" jsonc-parser "~3.0.0" lodash.differencewith "~4.5.0" lodash.flatten "~4.4.0" - markdownlint "~0.22.0" - markdownlint-rule-helpers "~0.13.0" + markdownlint "~0.23.1" + markdownlint-rule-helpers "~0.14.0" minimatch "~3.0.4" minimist "~1.2.5" rc "~1.2.8" -markdownlint-rule-helpers@~0.13.0: - version "0.13.0" - resolved "https://registry.yarnpkg.com/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.13.0.tgz#7cc6553bc7f8c4c8a43cf66fb2a3a652124f46f9" - integrity sha512-rRY0itbcHG4e+ntz0bbY3AIceSJMKS0TafEMgEtKVHRZ54/JUSy6/4ypCL618RlJvYRej+xMLxX5nkJqIeTZaQ== +markdownlint-rule-helpers@~0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.14.0.tgz#4d0e1ae320e85559d8cbed1490934855791627bb" + integrity sha512-vRTPqSU4JK8vVXmjICHSBhwXUvbfh/VJo+j7hvxqe15tLJyomv3FLgFdFgb8kpj0Fe8SsJa/TZUAXv7/sN+N7A== -markdownlint@~0.22.0: - version "0.22.0" - resolved "https://registry.yarnpkg.com/markdownlint/-/markdownlint-0.22.0.tgz#4ed95b61c17ae9f4dfca6a01f038c744846c0a72" - integrity sha512-J4B+iMc12pOdp/wfYi03W2qfAfEyiZzq3qvQh/8vOMNU8vXYY6Jg440EY7dWTBCqROhb1i4nAn3BTByJ5kdx1w== +markdownlint@~0.23.1: + version "0.23.1" + resolved "https://registry.yarnpkg.com/markdownlint/-/markdownlint-0.23.1.tgz#98292b5d340d01e9c113f3d7fb3b2ccf89628dc2" + integrity sha512-iOEwhDfNmq2IJlaA8mzEkHYUi/Hwoa6Ss+HO5jkwUR6wQ4quFr0WzSx+Z9rsWZKUaPbyirIdL1zGmJRkWawr4Q== dependencies: - markdown-it "12.0.2" + markdown-it "12.0.4" md5@^2.3.0: version "2.3.0" @@ -7208,10 +7273,10 @@ nise@^4.0.4: just-extend "^4.0.2" path-to-regexp "^1.7.0" -nock@^13.0.7: - version "13.0.7" - resolved "https://registry.yarnpkg.com/nock/-/nock-13.0.7.tgz#9bc718c66bd0862dfa14601a9ba678a406127910" - integrity sha512-WBz73VYIjdbO6BwmXODRQLtn7B5tldA9pNpWJe5QTtTEscQlY5KXU4srnGzBOK2fWakkXj69gfTnXGzmrsaRWw== +nock@^13.0.9: + version "13.0.9" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.0.9.tgz#32b9f6408a71991b25f044109cac92ee556c8539" + integrity sha512-SoGx/J0SsZPOdBFrBC9PP6NwaEgOBQIRPbsOsO9q+OwOPWc5eT6wALSxn3ZNE4Fv2ImIUXM4Hv/07rjq/uWDew== dependencies: debug "^4.1.0" json-stringify-safe "^5.0.1" @@ -7278,6 +7343,11 @@ node-preload@^0.2.1: dependencies: process-on-spawn "^1.0.0" +node-releases@^1.1.70: + version "1.1.71" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" + integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg== + nodeunit@^0.11.3: version "0.11.3" resolved "https://registry.yarnpkg.com/nodeunit/-/nodeunit-0.11.3.tgz#313afae26cd11b407b731ff774b8e35e5d6f9568" @@ -7559,13 +7629,13 @@ object.pick@^1.3.0: isobject "^3.0.1" object.values@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.2.tgz#7a2015e06fcb0f546bd652486ce8583a4731c731" - integrity sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag== + version "1.1.3" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.3.tgz#eaa8b1e17589f02f698db093f7c62ee1699742ee" + integrity sha512-nkF6PfDB9alkOUxpf1HNm/QlkeW3SReqL5WXeBLpEJJnlPSvRaDQpW3gQTksTN3fgJX4hL42RzKyOin6ff3tyw== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" + es-abstract "^1.18.0-next.2" has "^1.0.3" octokit-pagination-methods@^1.1.0: @@ -7594,10 +7664,10 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" -oo-ascii-tree@^1.21.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.21.0.tgz#8408fceb90799c7af769b7ab709ec35ea9868b8b" - integrity sha512-N91VyM/R9K8axskaVYSg+IJiSDJVKFQ2IfQyBp5Rv7t2YETjJDMgA6Ew9MGv82fhpz95qKLlZmgrQsb7scb2Eg== +oo-ascii-tree@^1.23.0: + version "1.23.0" + resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.23.0.tgz#b7a8ece5d313a420689a6a9d728a2696e8991e40" + integrity sha512-+T0rUcBAvJaEXbdCe3p8A7qjJ82CRGSgfUSvkGscH2F0lFqrELN1ey0UGDf0UV7C/ivIFzct5DYJAhrVvFbeUw== opener@^1.5.1: version "1.5.2" @@ -8243,9 +8313,9 @@ qs@~6.5.2: integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== query-string@^6.13.8: - version "6.14.0" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.0.tgz#0b7b7ca326f5facf10dd2d45d26645cd287f8c92" - integrity sha512-In3o+lUxlgejoVJgwEdYtdxrmlL0cQWJXj0+kkI7RWVo7hg5AhFtybeKlC9Dpgbr8eOC4ydpEh8017WwyfzqVQ== + version "6.14.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a" + integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw== dependencies: decode-uri-component "^0.2.0" filter-obj "^1.1.0" @@ -8704,9 +8774,9 @@ run-queue@^1.0.0, run-queue@^1.0.3: aproba "^1.1.1" rxjs@^6.4.0: - version "6.6.3" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" - integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ== + version "6.6.6" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.6.tgz#14d8417aa5a07c5e633995b525e1e3c0dec03b70" + integrity sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg== dependencies: tslib "^1.9.0" @@ -9239,10 +9309,10 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -string-width@*, string-width@^4.1.0, string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== +string-width@*, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" + integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== dependencies: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" @@ -9279,20 +9349,20 @@ string.prototype.repeat@^0.2.0: resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-0.2.0.tgz#aba36de08dcee6a5a337d49b2ea1da1b28fc0ecf" integrity sha1-q6Nt4I3O5qWjN9SbLqHaGyj8Ds8= -string.prototype.trimend@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz#a22bd53cca5c7cf44d7c9d5c732118873d6cd18b" - integrity sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw== +string.prototype.trimend@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" + integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" -string.prototype.trimstart@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz#9b4cb590e123bb36564401d59824298de50fd5aa" - integrity sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg== +string.prototype.trimstart@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" + integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" string_decoder@^1.1.1: @@ -9793,10 +9863,10 @@ trivial-deferred@^1.0.1: resolved "https://registry.yarnpkg.com/trivial-deferred/-/trivial-deferred-1.0.1.tgz#376d4d29d951d6368a6f7a0ae85c2f4d5e0658f3" integrity sha1-N21NKdlR1jaKb3oK6FwvTV4GWPM= -ts-jest@^26.5.1: - version "26.5.1" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.1.tgz#4d53ee4481552f57c1624f0bd3425c8b17996150" - integrity sha512-G7Rmo3OJMvlqE79amJX8VJKDiRcd7/r61wh9fnvvG8cAjhA9edklGw/dCxRSQmfZ/z8NDums5srSVgwZos1qfg== +ts-jest@^26.5.2: + version "26.5.2" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.2.tgz#5281d6b44c2f94f71205728a389edc3d7995b0c4" + integrity sha512-bwyJ2zJieSugf7RB+o8fgkMeoMVMM2KPDE0UklRLuACxjwJsOrZNo6chrcScmK33YavPSwhARffy8dZx5LJdUQ== dependencies: "@types/jest" "26.x" bs-logger "0.x" @@ -9956,9 +10026,9 @@ typescript@^3.3.3, typescript@~3.9.9: integrity sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w== typescript@^4.1.3: - version "4.1.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72" - integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== + version "4.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.2.tgz#1450f020618f872db0ea17317d16d8da8ddb8c4c" + integrity sha512-tbb+NVrLfnsJy3M59lsDgrzWIflR4d4TIUjz+heUnHZwdF7YsrMTKoRERiIvI2lvBG95dfpLxB21WZhys1bgaQ== typescript@~3.8.3: version "3.8.3" @@ -9971,9 +10041,9 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== uglify-js@^3.1.4: - version "3.12.8" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.12.8.tgz#a82e6e53c9be14f7382de3d068ef1e26e7d4aaf8" - integrity sha512-fvBeuXOsvqjecUtF/l1dwsrrf5y2BCUk9AOJGzGcm6tE7vegku5u/YvqjyDaAGr422PLoLnrxg3EnRvTqsdC1w== + version "3.13.0" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.0.tgz#66ed69f7241f33f13531d3d51d5bcebf00df7f69" + integrity sha512-TWYSWa9T2pPN4DIJYbU9oAjQx+5qdV5RUDxwARg8fmJZrD/V27Zj0JngW5xg1DFz42G0uDYl2XhzF6alSzD62w== uid-number@0.0.6: version "0.0.6" @@ -9985,6 +10055,16 @@ umask@^1.1.0: resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d" integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0= +unbox-primitive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.0.tgz#eeacbc4affa28e9b3d36b5eaeccc50b3251b1d3f" + integrity sha512-P/51NX+JXyxK/aigg1/ZgyccdAxm5K1+n8+tvqSntjOivPt19gvm1VC49RWYetsiub8WViUchdxl/KWHHB0kzA== + dependencies: + function-bind "^1.1.1" + has-bigints "^1.0.0" + has-symbols "^1.0.0" + which-boxed-primitive "^1.0.1" + unicode-length@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/unicode-length/-/unicode-length-2.0.2.tgz#e5eb4c0d523fdf7bebb59ca261c9ca1cf732da96"