diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 99093538f6b52..caa7b80a94a8c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,37 +1,10 @@ -## Description - - -## Commit Message - -{*replace-with-pr-title*} (#{*replace-with-pr-number*}) - - -{replace-with-extended-commit-message} - - - - - - - - - - -## End Commit Message ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* - diff --git a/.github/actions/prlinter/index.js b/.github/actions/prlinter/index.js index b15aad8a9ca1a..e36cc7aa9ffba 100644 --- a/.github/actions/prlinter/index.js +++ b/.github/actions/prlinter/index.js @@ -3,8 +3,7 @@ const github = require('@actions/github'); const linter = require('prlint') const checks = { - "MANDATORY_CHANGES": linter.mandatoryChanges, - "COMMIT_MESSAGE": linter.commitMessage + "MANDATORY_CHANGES": linter.mandatoryChanges } async function run() { @@ -21,7 +20,7 @@ async function run() { } await check(number); - + } catch (error) { core.setFailed(error.message); diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1c34721284646..20c5277e90b23 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,8 +11,9 @@ and let us know if it's not up-to-date (even better, submit a PR with your corr - [Step 1: Open Issue](#step-1-open-issue) - [Step 2: Design (optional)](#step-2-design-optional) - [Step 3: Work your Magic](#step-3-work-your-magic) - - [Step 4: Pull Request](#step-4-pull-request) - - [Step 5: Merge](#step-5-merge) + - [Step 4: Commit](#step-4-commit) + - [Step 5: Pull Request](#step-5-pull-request) + - [Step 6: Merge](#step-6-merge) - [Tools](#tools) - [Main build scripts](#main-build-scripts) - [Partial build tools](#partial-build-tools) @@ -52,7 +53,7 @@ For day-to-day development and normal contributions, the following SDKs and tool - [.NET Core SDK 3.0](https://www.microsoft.com/net/download) - [Python 3.6.5](https://www.python.org/downloads/release/python-365/) - [Ruby 2.5.1](https://www.ruby-lang.org/en/news/2018/03/28/ruby-2-5-1-released/) - + The basic commands to get the repository cloned and built locally follow: ```console @@ -141,7 +142,7 @@ Integration tests perform a few functions in the CDK code base - 3. (Optionally) Acts as a way to validate that constructs set up the CloudFormation resources as expected. A successful CloudFormation deployment does not mean that the resources are set up correctly. -If you are working on a new feature that is using previously unused CloudFormation resource types, or involves +If you are working on a new feature that is using previously unused CloudFormation resource types, or involves configuring resource types across services, you need to write integration tests that use these resource types or features. @@ -161,37 +162,48 @@ Examples: * [integ.destinations.ts](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/aws-lambda-destinations/test/integ.destinations.ts#L7) * [integ.token-authorizer.ts](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.ts#L6) -### Step 4: Pull Request +### Step 4: Commit -* Push to a GitHub fork or to a branch (naming convention: `/`) -* Submit a Pull Request on GitHub and assign the PR for a review to the "aws/aws-cdk-team" team. The title and description will be used to format the commit message when its merged to master. This in turn, will translate to CHANGELOG entries. It is therefore important we be consistent and informative. Here is an example PR you should use as a reference: https://github.com/aws/aws-cdk/pull/6553. +Create a commit with the proposed change changes: - ### Title +* Commit title and message (and PR title and description) must adhere to [conventionalcommits](https://www.conventionalcommits.org). + * The title must begin with `feat(module): title`, `fix(module): title`, `refactor(module): title` or + `chore(module): title`. + * Title should be lowercase. + * No period at the end of the title. - * Must adhere to [conventionalcommits](https://www.conventionalcommits.org). - * The title must begin with one of: - - `feat(module): title` - - `fix(module): title` - - `refactor(module): title` - - `chore(module): title` - * Should be lowercase. - * No period at the end. +* Commit message should describe _motivation_. Think about your code reviewers and what information they need in + order to understand what you did. If it's a big commit (hopefully not), try to provide some good entry points so + it will be easier to follow. +* Commit message should indicate which issues are fixed: `fixes #` or `closes #`. - ### Description +* Shout out to collaborators. - * Simply follow the PR template carefully. +* If not obvious (i.e. from unit tests), describe how you verified that your change works. +* If this commit includes breaking changes, they must be listed at the end in the following format (notice how multiple breaking changes should be formatted): +``` +BREAKING CHANGE: Description of what broke and how to achieve this behavior now +* **module-name:** Another breaking change +* **module-name:** Yet another breaking change +``` + +### Step 5: Pull Request + +* Push to a GitHub fork or to a branch (naming convention: `/`) +* Submit a Pull Requests on GitHub and assign the PR for a review to the "awslabs/aws-cdk" team. * Please follow the PR checklist written below. We trust our contributors to self-check, and this helps that process! * Discuss review comments and iterate until you get at least one “Approve”. When iterating, push new commits to the same branch. Usually all these are going to be squashed when you merge to master. The commit messages should be hints for you when you finalize your merge commit message. -* Make sure to update the PR title/description if things change. +* Make sure to update the PR title/description if things change. The PR title/description are going to be used as the + commit title/message and will appear in the CHANGELOG, so maintain them all the way throughout the process. -### Step 5: Merge +### Step 6: Merge * Make sure your PR builds successfully (we have CodeBuild setup to automatically build all PRs) * Once approved and tested, a maintainer will squash-merge to master and will use your PR title/description as the diff --git a/allowed-breaking-changes.txt b/allowed-breaking-changes.txt index 427980fb5be9c..a84098fb3ab06 100644 --- a/allowed-breaking-changes.txt +++ b/allowed-breaking-changes.txt @@ -40,4 +40,4 @@ change-return-type:@aws-cdk/aws-lambda-destinations.EventBridgeDestination.bind change-return-type:@aws-cdk/aws-lambda-destinations.LambdaDestination.bind change-return-type:@aws-cdk/aws-lambda-destinations.SnsDestination.bind change-return-type:@aws-cdk/aws-lambda-destinations.SqsDestination.bind - +removed:@aws-cdk/cdk-assets-schema.DockerImageDestination.imageUri diff --git a/package.json b/package.json index 7a6e3617842e8..e7308399fc176 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "devDependencies": { "conventional-changelog-cli": "^2.0.31", "fs-extra": "^8.1.0", - "jsii-diff": "^1.0.0", + "jsii-diff": "^1.1.0", "jsii-pacmak": "^0.22.0", "jsii-rosetta": "^0.22.0", "lerna": "^3.20.2", diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index a2367e302a613..f1fba31480a77 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -153,6 +153,36 @@ plan.addApiStage({ }); ``` +In scenarios where you need to create a single api key and configure rate limiting for it, you can use `RateLimitedApiKey`. +This construct lets you specify rate limiting properties which should be applied only to the api key being created. +The API key created has the specified rate limits, such as quota and throttles, applied. + +The following example shows how to use a rate limited api key : +```ts +const hello = new lambda.Function(this, 'hello', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'hello.handler', + code: lambda.Code.fromAsset('lambda') +}); + +const api = new apigateway.RestApi(this, 'hello-api', { }); +const integration = new apigateway.LambdaIntegration(hello); + +const v1 = api.root.addResource('v1'); +const echo = v1.addResource('echo'); +const echoMethod = echo.addMethod('GET', integration, { apiKeyRequired: true }); + +const key = new apigateway.RateLimitedApiKey(this, 'rate-limited-api-key', { + customerId: 'hello-customer', + resources: [api], + quota: { + limit: 10000, + period: apigateway.Period.MONTH + } +}); + +``` + ### Working with models When you work with Lambda integrations that are not Proxy integrations, you @@ -343,7 +373,7 @@ that can be used for controlling access to your REST APIs. #### IAM-based authorizer -The following CDK code provides 'excecute-api' permission to an IAM user, via IAM policies, for the 'GET' method on the `books` resource: +The following CDK code provides 'execute-api' permission to an IAM user, via IAM policies, for the 'GET' method on the `books` resource: ```ts const getBooks = books.addMethod('GET', new apigateway.HttpIntegration('http://amazon.com'), { diff --git a/packages/@aws-cdk/aws-apigateway/lib/api-key.ts b/packages/@aws-cdk/aws-apigateway/lib/api-key.ts index 1e17498cd5c2e..0a11d61c2ac78 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/api-key.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/api-key.ts @@ -20,6 +20,7 @@ export interface IApiKey extends IResourceBase { */ export interface ApiKeyProps extends ResourceOptions { /** + * [disable-awslint:ref-via-interface] * A list of resources this api key is associated with. * @default none */ diff --git a/packages/@aws-cdk/aws-apigateway/lib/index.ts b/packages/@aws-cdk/aws-apigateway/lib/index.ts index 3ce1a7901dd96..eb2b1d5dc38a8 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/index.ts @@ -7,6 +7,7 @@ export * from './stage'; export * from './integrations'; export * from './lambda-api'; export * from './api-key'; +export * from './rate-limited-api-key'; export * from './usage-plan'; export * from './vpc-link'; export * from './methodresponse'; diff --git a/packages/@aws-cdk/aws-apigateway/lib/rate-limited-api-key.ts b/packages/@aws-cdk/aws-apigateway/lib/rate-limited-api-key.ts new file mode 100644 index 0000000000000..229f21460626a --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/rate-limited-api-key.ts @@ -0,0 +1,54 @@ +import { Construct, Resource } from '@aws-cdk/core'; +import { ApiKey, ApiKeyProps, IApiKey } from './api-key'; +import { QuotaSettings, ThrottleSettings, UsagePlan, UsagePlanPerApiStage } from './usage-plan'; + +/** + * RateLimitedApiKey properties. + */ +export interface RateLimitedApiKeyProps extends ApiKeyProps { + /** + * API Stages to be associated with the RateLimitedApiKey. + * @default none + */ + readonly apiStages?: UsagePlanPerApiStage[]; + + /** + * Number of requests clients can make in a given time period. + * @default none + */ + readonly quota?: QuotaSettings; + + /** + * Overall throttle settings for the API. + * @default none + */ + readonly throttle?: ThrottleSettings; +} + +/** + * An API Gateway ApiKey, for which a rate limiting configuration can be specified. + * + * @resource AWS::ApiGateway::ApiKey + */ +export class RateLimitedApiKey extends Resource implements IApiKey { + public readonly keyId: string; + + constructor(scope: Construct, id: string, props: RateLimitedApiKeyProps = { }) { + super(scope, id, { + physicalName: props.apiKeyName, + }); + + const resource = new ApiKey(this, 'Resource', props); + + if (props.apiStages || props.quota || props.throttle) { + new UsagePlan(this, 'UsagePlanResource', { + apiKey: resource, + apiStages: props.apiStages, + quota: props.quota, + throttle: props.throttle + }); + } + + this.keyId = resource.keyId; + } +} diff --git a/packages/@aws-cdk/aws-apigateway/test/test.rate-limited-api-key.ts b/packages/@aws-cdk/aws-apigateway/test/test.rate-limited-api-key.ts new file mode 100644 index 0000000000000..3cc35eeebd29c --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.rate-limited-api-key.ts @@ -0,0 +1,108 @@ +import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import * as cdk from '@aws-cdk/core'; +import { Test } from "nodeunit"; +import * as apigateway from '../lib'; + +const API_KEY_RESOURCE_TYPE = 'AWS::ApiGateway::ApiKey'; +const USAGE_PLAN_RESOURCE_TYPE = 'AWS::ApiGateway::UsagePlan'; +const USAGE_PLAN_KEY_RESOURCE_TYPE = 'AWS::ApiGateway::UsagePlanKey'; + +export = { + 'default setup'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'my-api', { cloudWatchRole: false, deploy: false }); + api.root.addMethod('GET'); // Need at least one method on the api + + // WHEN + new apigateway.RateLimitedApiKey(stack, 'my-api-key'); + + // THEN + // should have an api key with no props defined. + expect(stack).to(haveResource(API_KEY_RESOURCE_TYPE, undefined, ResourcePart.CompleteDefinition)); + // should not have a usage plan. + expect(stack).notTo(haveResource(USAGE_PLAN_RESOURCE_TYPE)); + // should not have a usage plan key. + expect(stack).notTo(haveResource(USAGE_PLAN_KEY_RESOURCE_TYPE)); + + test.done(); + }, + + 'only api key is created when rate limiting properties are not provided'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: true, deployOptions: { stageName: 'test' } }); + api.root.addMethod('GET'); // api must have atleast one method. + + // WHEN + new apigateway.RateLimitedApiKey(stack, 'test-api-key', { + customerId: 'test-customer', + resources: [api] + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::ApiKey', { + CustomerId: 'test-customer', + StageKeys: [ + { + RestApiId: { Ref: "testapiD6451F70" }, + StageName: { Ref: "testapiDeploymentStagetest5869DF71" } + } + ] + })); + // should not have a usage plan. + expect(stack).notTo(haveResource(USAGE_PLAN_RESOURCE_TYPE)); + // should not have a usage plan key. + expect(stack).notTo(haveResource(USAGE_PLAN_KEY_RESOURCE_TYPE)); + + test.done(); + }, + + 'api key and usage plan are created and linked when rate limiting properties are provided'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: true, deployOptions: { stageName: 'test' } }); + api.root.addMethod('GET'); // api must have atleast one method. + + // WHEN + new apigateway.RateLimitedApiKey(stack, 'test-api-key', { + customerId: 'test-customer', + resources: [api], + quota: { + limit: 10000, + period: apigateway.Period.MONTH + } + }); + + // THEN + // should have an api key + expect(stack).to(haveResource('AWS::ApiGateway::ApiKey', { + CustomerId: 'test-customer', + StageKeys: [ + { + RestApiId: { Ref: "testapiD6451F70" }, + StageName: { Ref: "testapiDeploymentStagetest5869DF71" } + } + ] + })); + // should have a usage plan with specified quota. + expect(stack).to(haveResource(USAGE_PLAN_RESOURCE_TYPE, { + Quota: { + Limit: 10000, + Period: 'MONTH' + } + }, ResourcePart.Properties)); + // should have a usage plan key linking the api key and usage plan + expect(stack).to(haveResource(USAGE_PLAN_KEY_RESOURCE_TYPE, { + KeyId: { + Ref: 'testapikey998028B6' + }, + KeyType: 'API_KEY', + UsagePlanId: { + Ref: 'testapikeyUsagePlanResource66DB63D6' + } + }, ResourcePart.Properties)); + + test.done(); + } +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js index 5f2eeca1e5c1f..ab112a3b248f0 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js +++ b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js @@ -77,10 +77,10 @@ let report = function(event, context, responseStatus, physicalResourceId, respon * @param {string} hostedZoneId the Route53 Hosted Zone ID * @returns {string} Validated certificate ARN */ -const requestCertificate = async function(requestId, domainName, subjectAlternativeNames, hostedZoneId, region) { +const requestCertificate = async function(requestId, domainName, subjectAlternativeNames, hostedZoneId, region, route53Endpoint) { const crypto = require('crypto'); const acm = new aws.ACM({ region }); - const route53 = new aws.Route53(); + const route53 = route53Endpoint ? new aws.Route53({endpoint: route53Endpoint}) : new aws.Route53(); if (waiter) { // Used by the test suite, since waiters aren't mockable yet route53.waitFor = acm.waitFor = waiter; @@ -227,6 +227,7 @@ exports.certificateRequestHandler = async function(event, context) { event.ResourceProperties.SubjectAlternativeNames, event.ResourceProperties.HostedZoneId, event.ResourceProperties.Region, + event.ResourceProperties.Route53Endpoint, ); responseData.Arn = physicalResourceId = certificateArn; break; diff --git a/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts b/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts index 238bc92c35ef9..08997e7cf3d20 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts +++ b/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts @@ -24,6 +24,20 @@ export interface DnsValidatedCertificateProps extends CertificateProps { */ readonly region?: string; + /** + * An endpoint of Route53 service, which is not necessary as AWS SDK could figure + * out the right endpoints for most regions, but for some regions such as those in + * aws-cn partition, the default endpoint is not working now, hence the right endpoint + * need to be specified through this prop. + * + * Route53 is not been offically launched in China, it is only available for AWS + * internal accounts now. To make DnsValidatedCertificate work for internal accounts + * now, a special endpoint needs to be provided. + * + * @default - The AWS SDK will determine the Route53 endpoint to use based on region + */ + readonly route53Endpoint?: string; + /** * Role to use for the custom resource that creates the validated certificate * @@ -85,6 +99,7 @@ export class DnsValidatedCertificate extends cdk.Resource implements ICertificat SubjectAlternativeNames: cdk.Lazy.listValue({ produce: () => props.subjectAlternativeNames }, { omitEmpty: true }), HostedZoneId: this.hostedZoneId, Region: props.region, + Route53Endpoint: props.route53Endpoint, } }); diff --git a/packages/@aws-cdk/aws-certificatemanager/test/test.dns-validated-certificate.ts b/packages/@aws-cdk/aws-certificatemanager/test/test.dns-validated-certificate.ts index be76466360e11..e1a3e0ef3084f 100644 --- a/packages/@aws-cdk/aws-certificatemanager/test/test.dns-validated-certificate.ts +++ b/packages/@aws-cdk/aws-certificatemanager/test/test.dns-validated-certificate.ts @@ -156,6 +156,7 @@ export = { new DnsValidatedCertificate(stack, 'Cert', { domainName: 'mydomain.com', hostedZone: imported, + route53Endpoint: "https://api.route53.xxx.com", }); // THEN @@ -167,7 +168,8 @@ export = { ] }, DomainName: 'mydomain.com', - HostedZoneId: 'DUMMY' + HostedZoneId: 'DUMMY', + Route53Endpoint: 'https://api.route53.xxx.com' })); test.done(); diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index 3383c1e6e7e02..02aba194d2c21 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.30", - "aws-sdk": "^2.637.0", + "aws-sdk": "^2.638.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index 1f1f3d2a7d703..fcdebbfea919f 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.30", - "aws-sdk": "^2.637.0", + "aws-sdk": "^2.638.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts index 1ff3107932eb9..41f9ecafc04e6 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts @@ -42,6 +42,7 @@ export enum ComparisonOperator { GREATER_THAN_THRESHOLD = 'GreaterThanThreshold', LESS_THAN_THRESHOLD = 'LessThanThreshold', LESS_THAN_OR_EQUAL_TO_THRESHOLD = 'LessThanOrEqualToThreshold', + LESS_THAN_LOWER_OR_GREATER_THAN_UPPER_THRESHOLD = 'LessThanLowerOrGreaterThanUpperThreshold', } const OPERATOR_SYMBOLS: {[key: string]: string} = { diff --git a/packages/@aws-cdk/aws-cloudwatch/package.json b/packages/@aws-cdk/aws-cloudwatch/package.json index 3a8c56165c82e..7c27985c54d12 100644 --- a/packages/@aws-cdk/aws-cloudwatch/package.json +++ b/packages/@aws-cdk/aws-cloudwatch/package.json @@ -131,6 +131,7 @@ "docs-public-apis:@aws-cdk/aws-cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD", "docs-public-apis:@aws-cdk/aws-cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD", "docs-public-apis:@aws-cdk/aws-cloudwatch.ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD", + "docs-public-apis:@aws-cdk/aws-cloudwatch.ComparisonOperator.LESS_THAN_LOWER_OR_GREATER_THAN_UPPER_THRESHOLD", "docs-public-apis:@aws-cdk/aws-cloudwatch.PeriodOverride", "docs-public-apis:@aws-cdk/aws-cloudwatch.PeriodOverride.AUTO", "docs-public-apis:@aws-cdk/aws-cloudwatch.PeriodOverride.INHERIT", diff --git a/packages/@aws-cdk/aws-codebuild/package.json b/packages/@aws-cdk/aws-codebuild/package.json index 5452f2080d5fe..0689d8f6daa5b 100644 --- a/packages/@aws-cdk/aws-codebuild/package.json +++ b/packages/@aws-cdk/aws-codebuild/package.json @@ -70,7 +70,7 @@ "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@types/nodeunit": "^0.0.30", - "aws-sdk": "^2.637.0", + "aws-sdk": "^2.638.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codecommit/package.json b/packages/@aws-cdk/aws-codecommit/package.json index 9efae1b9217fb..e7f41b2f8ddca 100644 --- a/packages/@aws-cdk/aws-codecommit/package.json +++ b/packages/@aws-cdk/aws-codecommit/package.json @@ -70,7 +70,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@types/nodeunit": "^0.0.30", - "aws-sdk": "^2.637.0", + "aws-sdk": "^2.638.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/ecs/deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/ecs/deploy-action.ts index 245764f12bb56..c7bc50dccf5c8 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/ecs/deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/ecs/deploy-action.ts @@ -39,7 +39,7 @@ export interface EcsDeployActionProps extends codepipeline.CommonAwsActionProps /** * The ECS Service to deploy. */ - readonly service: ecs.BaseService; + readonly service: ecs.IBaseService; } /** diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/test.ecs-deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/test.ecs-deploy-action.ts index 89902677ed82d..dfa0a546cbd8c 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/test.ecs-deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/test.ecs-deploy-action.ts @@ -1,6 +1,8 @@ +import { expect, haveResourceLike } from '@aws-cdk/assert'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; +import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as cpactions from '../../lib'; @@ -80,6 +82,71 @@ export = { test.done(); }, + + 'can be created by existing service'(test: Test) { + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc'); + const service = ecs.FargateService.fromFargateServiceAttributes(stack, 'FargateService', { + serviceName: 'service-name', + cluster: ecs.Cluster.fromClusterAttributes(stack, 'Cluster', { + vpc, + securityGroups: [], + clusterName: 'cluster-name', + }), + }); + const artifact = new codepipeline.Artifact('Artifact'); + const bucket = new s3.Bucket(stack, 'PipelineBucket', { + versioned: true, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + const source = new cpactions.S3SourceAction({ + actionName: 'Source', + output: artifact, + bucket, + bucketKey: 'key', + }); + const action = new cpactions.EcsDeployAction({ + actionName: 'ECS', + service, + imageFile: artifact.atPath('imageFile.json'), + }); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [source], + }, + { + stageName: 'Deploy', + actions: [action], + } + ], + }); + + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + {}, + { + Actions: [ + { + Name: 'ECS', + ActionTypeId: { + Category: "Deploy", + Provider: "ECS" + }, + Configuration: { + ClusterName: "cluster-name", + ServiceName: "service-name", + FileName: "imageFile.json" + } + } + ] + } + ] + })); + + test.done(); + }, }, }; diff --git a/packages/@aws-cdk/aws-config/lib/rule.ts b/packages/@aws-cdk/aws-config/lib/rule.ts index c4dc18fb36add..1502b197a0b75 100644 --- a/packages/@aws-cdk/aws-config/lib/rule.ts +++ b/packages/@aws-cdk/aws-config/lib/rule.ts @@ -167,10 +167,30 @@ abstract class RuleNew extends RuleBase { * The maximum frequency at which the AWS Config rule runs evaluations. */ export enum MaximumExecutionFrequency { + + /** + * 1 hour. + */ ONE_HOUR = 'One_Hour', + + /** + * 3 hours. + */ THREE_HOURS = 'Three_Hours', + + /** + * 6 hours. + */ SIX_HOURS = 'Six_Hours', + + /** + * 12 hours. + */ TWELVE_HOURS = 'Twelve_Hours', + + /** + * 24 hours. + */ TWENTY_FOUR_HOURS = 'TwentyFour_Hours' } diff --git a/packages/@aws-cdk/aws-config/package.json b/packages/@aws-cdk/aws-config/package.json index 64d5f7453468f..43fd56f67f746 100644 --- a/packages/@aws-cdk/aws-config/package.json +++ b/packages/@aws-cdk/aws-config/package.json @@ -89,14 +89,5 @@ "engines": { "node": ">= 10.3.0" }, - "stability": "experimental", - "awslint": { - "exclude": [ - "docs-public-apis:@aws-cdk/aws-config.MaximumExecutionFrequency.TWENTY_FOUR_HOURS", - "docs-public-apis:@aws-cdk/aws-config.MaximumExecutionFrequency.ONE_HOUR", - "docs-public-apis:@aws-cdk/aws-config.MaximumExecutionFrequency.THREE_HOURS", - "docs-public-apis:@aws-cdk/aws-config.MaximumExecutionFrequency.SIX_HOURS", - "docs-public-apis:@aws-cdk/aws-config.MaximumExecutionFrequency.TWELVE_HOURS" - ] - } + "stability": "experimental" } diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index 3226842f93060..dd1c9bd4d8aaf 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -64,8 +64,8 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.30", - "aws-sdk": "^2.637.0", - "aws-sdk-mock": "^5.0.0", + "aws-sdk": "^2.638.0", + "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts b/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts index 83d3709d1e9a7..a833471b733ef 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts @@ -1,7 +1,7 @@ import { expect, haveResource, ResourcePart, SynthUtils } from '@aws-cdk/assert'; import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; import * as iam from '@aws-cdk/aws-iam'; -import { App, CfnDeletionPolicy, ConstructNode, RemovalPolicy, Stack, Tag } from '@aws-cdk/core'; +import { App, CfnDeletionPolicy, ConstructNode, Duration, RemovalPolicy, Stack, Tag } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import { Attribute, @@ -1142,7 +1142,7 @@ export = { // THEN test.deepEqual(stack.resolve(table.metricConsumedReadCapacityUnits()), { - period: { amount: 5, unit: { label: 'minutes', inSeconds: 60 } }, + period: Duration.minutes(5), dimensions: { TableName: { Ref: 'TableCD117FA1' } }, namespace: 'AWS/DynamoDB', metricName: 'ConsumedReadCapacityUnits', @@ -1161,7 +1161,7 @@ export = { // THEN test.deepEqual(stack.resolve(table.metricConsumedWriteCapacityUnits()), { - period: { amount: 5, unit: { label: 'minutes', inSeconds: 60 } }, + period: Duration.minutes(5), dimensions: { TableName: { Ref: 'TableCD117FA1' } }, namespace: 'AWS/DynamoDB', metricName: 'ConsumedWriteCapacityUnits', @@ -1180,7 +1180,7 @@ export = { // THEN test.deepEqual(stack.resolve(table.metricSystemErrors()), { - period: { amount: 5, unit: { label: 'minutes', inSeconds: 60 } }, + period: Duration.minutes(5), dimensions: { TableName: { Ref: 'TableCD117FA1' } }, namespace: 'AWS/DynamoDB', metricName: 'SystemErrors', @@ -1199,7 +1199,7 @@ export = { // THEN test.deepEqual(stack.resolve(table.metricUserErrors()), { - period: { amount: 5, unit: { label: 'minutes', inSeconds: 60 } }, + period: Duration.minutes(5), dimensions: { TableName: { Ref: 'TableCD117FA1' } }, namespace: 'AWS/DynamoDB', metricName: 'UserErrors', @@ -1218,7 +1218,7 @@ export = { // THEN test.deepEqual(stack.resolve(table.metricConditionalCheckFailedRequests()), { - period: { amount: 5, unit: { label: 'minutes', inSeconds: 60 } }, + period: Duration.minutes(5), dimensions: { TableName: { Ref: 'TableCD117FA1' } }, namespace: 'AWS/DynamoDB', metricName: 'ConditionalCheckFailedRequests', @@ -1237,7 +1237,7 @@ export = { // THEN test.deepEqual(stack.resolve(table.metricSuccessfulRequestLatency()), { - period: { amount: 5, unit: { label: 'minutes', inSeconds: 60 } }, + period: Duration.minutes(5), dimensions: { TableName: { Ref: 'TableCD117FA1' } }, namespace: 'AWS/DynamoDB', metricName: 'SuccessfulRequestLatency', diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json index 4edd14a4b6144..ba5017dd9e617 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json @@ -545,7 +545,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters0b9723d54b7db3fbfc1a143398b02392e5fe080a68535480782a949b4372d0f0S3Bucket0997A4A0" + "Ref": "AssetParameters01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247S3Bucket3747EA0C" }, "S3Key": { "Fn::Join": [ @@ -558,7 +558,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters0b9723d54b7db3fbfc1a143398b02392e5fe080a68535480782a949b4372d0f0S3VersionKey83D9C166" + "Ref": "AssetParameters01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247S3VersionKey13E25E1F" } ] } @@ -571,7 +571,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters0b9723d54b7db3fbfc1a143398b02392e5fe080a68535480782a949b4372d0f0S3VersionKey83D9C166" + "Ref": "AssetParameters01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247S3VersionKey13E25E1F" } ] } @@ -865,17 +865,17 @@ } }, "Parameters": { - "AssetParameters0b9723d54b7db3fbfc1a143398b02392e5fe080a68535480782a949b4372d0f0S3Bucket0997A4A0": { + "AssetParameters01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247S3Bucket3747EA0C": { "Type": "String", - "Description": "S3 bucket for asset \"0b9723d54b7db3fbfc1a143398b02392e5fe080a68535480782a949b4372d0f0\"" + "Description": "S3 bucket for asset \"01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247\"" }, - "AssetParameters0b9723d54b7db3fbfc1a143398b02392e5fe080a68535480782a949b4372d0f0S3VersionKey83D9C166": { + "AssetParameters01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247S3VersionKey13E25E1F": { "Type": "String", - "Description": "S3 key for asset version \"0b9723d54b7db3fbfc1a143398b02392e5fe080a68535480782a949b4372d0f0\"" + "Description": "S3 key for asset version \"01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247\"" }, - "AssetParameters0b9723d54b7db3fbfc1a143398b02392e5fe080a68535480782a949b4372d0f0ArtifactHashF8F836D1": { + "AssetParameters01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247ArtifactHashFB4438F1": { "Type": "String", - "Description": "Artifact hash for asset \"0b9723d54b7db3fbfc1a143398b02392e5fe080a68535480782a949b4372d0f0\"" + "Description": "Artifact hash for asset \"01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247\"" } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index c83c54b03f379..dd51713f7937c 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -22,6 +22,13 @@ export interface IService extends IResource { * @attribute */ readonly serviceArn: string; + + /** + * The name of the service. + * + * @attribute + */ + readonly serviceName: string; } /** @@ -245,11 +252,21 @@ class NetworkListenerConfig extends ListenerConfig { } } +/** + * The interface for BaseService. + */ +export interface IBaseService extends IService { + /** + * The cluster that hosts the service. + */ + readonly cluster: ICluster; +} + /** * The base class for Ec2Service and FargateService services. */ export abstract class BaseService extends Resource - implements IService, elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, elb.ILoadBalancerTarget { + implements IBaseService, elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, elb.ILoadBalancerTarget { /** * The security groups which manage the allowed network traffic for the service. diff --git a/packages/@aws-cdk/aws-ecs/lib/base/from-service-attributes.ts b/packages/@aws-cdk/aws-ecs/lib/base/from-service-attributes.ts new file mode 100644 index 0000000000000..9128c00b0430d --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/base/from-service-attributes.ts @@ -0,0 +1,57 @@ +import { Construct, Resource, Stack } from '@aws-cdk/core'; +import { IBaseService } from '../base/base-service'; +import { ICluster } from '../cluster'; + +/** + * The properties to import from the service. + */ +export interface ServiceAttributes { + /** + * The cluster that hosts the service. + */ + readonly cluster: ICluster; + + /** + * The service ARN. + * + * @default - either this, or {@link serviceName}, is required + */ + readonly serviceArn?: string; + + /** + * The name of the service. + * + * @default - either this, or {@link serviceArn}, is required + */ + readonly serviceName?: string; +} + +export function fromServiceAtrributes(scope: Construct, id: string, attrs: ServiceAttributes): IBaseService { + if ((attrs.serviceArn && attrs.serviceName) || (!attrs.serviceArn && !attrs.serviceName)) { + throw new Error('You can only specify either serviceArn or serviceName.'); + } + + const stack = Stack.of(scope); + let name: string; + let arn: string; + if (attrs.serviceName) { + name = attrs.serviceName as string; + arn = stack.formatArn({ + partition: stack.partition, + service: 'ecs', + region: stack.region, + account: stack.account, + resource: 'service', + resourceName: name, + }); + } else { + arn = attrs.serviceArn as string; + name = stack.parseArn(arn).resourceName as string; + } + class Import extends Resource implements IBaseService { + public readonly serviceArn = arn; + public readonly serviceName = name; + public readonly cluster = attrs.cluster; + } + return new Import(scope, id); +} \ No newline at end of file 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 13877c0785126..64367f32d54aa 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -1,7 +1,9 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import { Construct, Lazy, Resource, Stack } from '@aws-cdk/core'; -import { BaseService, BaseServiceOptions, IService, LaunchType, PropagatedTagSource } from '../base/base-service'; +import { BaseService, BaseServiceOptions, IBaseService, IService, LaunchType, PropagatedTagSource } from '../base/base-service'; +import { fromServiceAtrributes } from '../base/from-service-attributes'; import { NetworkMode, TaskDefinition } from '../base/task-definition'; +import { ICluster } from '../cluster'; import { CfnService } from '../ecs.generated'; import { PlacementConstraint, PlacementStrategy } from '../placement'; @@ -87,6 +89,30 @@ export interface IEc2Service extends IService { } +/** + * The properties to import from the service using the EC2 launch type. + */ +export interface Ec2ServiceAttributes { + /** + * The cluster that hosts the service. + */ + readonly cluster: ICluster; + + /** + * The service ARN. + * + * @default - either this, or {@link serviceName}, is required + */ + readonly serviceArn?: string; + + /** + * The name of the service. + * + * @default - either this, or {@link serviceArn}, is required + */ + readonly serviceName?: string; +} + /** * This creates a service using the EC2 launch type on an ECS cluster. * @@ -100,10 +126,18 @@ export class Ec2Service extends BaseService implements IEc2Service { public static fromEc2ServiceArn(scope: Construct, id: string, ec2ServiceArn: string): IEc2Service { class Import extends Resource implements IEc2Service { public readonly serviceArn = ec2ServiceArn; + public readonly serviceName = Stack.of(scope).parseArn(ec2ServiceArn).resourceName as string; } return new Import(scope, id); } + /** + * Imports from the specified service attrributes. + */ + public static fromEc2ServiceAttributes(scope: Construct, id: string, attrs: Ec2ServiceAttributes): IBaseService { + return fromServiceAtrributes(scope, id, attrs); + } + private readonly constraints: CfnService.PlacementConstraintProperty[]; private readonly strategies: CfnService.PlacementStrategyProperty[]; private readonly daemon: boolean; 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 d2c064df93d6f..f8b741a584604 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -1,7 +1,9 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; -import { BaseService, BaseServiceOptions, IService, LaunchType, PropagatedTagSource } from '../base/base-service'; +import { BaseService, BaseServiceOptions, IBaseService, IService, LaunchType, PropagatedTagSource } from '../base/base-service'; +import { fromServiceAtrributes } from '../base/from-service-attributes'; import { TaskDefinition } from '../base/task-definition'; +import { ICluster } from '../cluster'; /** * The properties for defining a service using the Fargate launch type. @@ -65,6 +67,30 @@ export interface IFargateService extends IService { } +/** + * The properties to import from the service using the Fargate launch type. + */ +export interface FargateServiceAttributes { + /** + * The cluster that hosts the service. + */ + readonly cluster: ICluster; + + /** + * The service ARN. + * + * @default - either this, or {@link serviceName}, is required + */ + readonly serviceArn?: string; + + /** + * The name of the service. + * + * @default - either this, or {@link serviceArn}, is required + */ + readonly serviceName?: string; +} + /** * This creates a service using the Fargate launch type on an ECS cluster. * @@ -73,15 +99,23 @@ export interface IFargateService extends IService { export class FargateService extends BaseService implements IFargateService { /** - * Import a task definition from the specified task definition ARN. + * Imports from the specified service ARN. */ public static fromFargateServiceArn(scope: cdk.Construct, id: string, fargateServiceArn: string): IFargateService { class Import extends cdk.Resource implements IFargateService { public readonly serviceArn = fargateServiceArn; + public readonly serviceName = cdk.Stack.of(scope).parseArn(fargateServiceArn).resourceName as string; } return new Import(scope, id); } + /** + * Imports from the specified service attrributes. + */ + public static fromFargateServiceAttributes(scope: cdk.Construct, id: string, attrs: FargateServiceAttributes): IBaseService { + return fromServiceAtrributes(scope, id, attrs); + } + /** * Constructs a new instance of the FargateService class. */ diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts index 2cc7349ac6545..61582bfaa8928 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts @@ -1936,5 +1936,74 @@ export = { }); test.done(); - } + }, + + 'When import an EC2 Service': { + 'with serviceArn'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const cluster = new ecs.Cluster(stack, 'EcsCluster'); + + // WHEN + const service = ecs.Ec2Service.fromEc2ServiceAttributes(stack, 'EcsService', { + serviceArn: 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service', + cluster, + }); + + // THEN + test.equal(service.serviceArn, 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service'); + test.equal(service.serviceName, 'my-http-service'); + + test.done(); + }, + + 'with serviceName'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const pseudo = new cdk.ScopedAws(stack); + const cluster = new ecs.Cluster(stack, 'EcsCluster'); + + // WHEN + const service = ecs.Ec2Service.fromEc2ServiceAttributes(stack, 'EcsService', { + serviceName: 'my-http-service', + cluster, + }); + + // THEN + test.deepEqual(stack.resolve(service.serviceArn), stack.resolve(`arn:${pseudo.partition}:ecs:${pseudo.region}:${pseudo.accountId}:service/my-http-service`)); + test.equal(service.serviceName, 'my-http-service'); + + test.done(); + }, + + 'throws an exception if both serviceArn and serviceName were provided for fromEc2ServiceAttributes'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const cluster = new ecs.Cluster(stack, 'EcsCluster'); + + test.throws(() => { + ecs.Ec2Service.fromEc2ServiceAttributes(stack, 'EcsService', { + serviceArn: 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service', + serviceName: 'my-http-service', + cluster, + }); + }, /only specify either serviceArn or serviceName/); + + test.done(); + }, + + 'throws an exception if neither serviceArn nor serviceName were provided for fromEc2ServiceAttributes'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const cluster = new ecs.Cluster(stack, 'EcsCluster'); + + test.throws(() => { + ecs.Ec2Service.fromEc2ServiceAttributes(stack, 'EcsService', { + cluster, + }); + }, /only specify either serviceArn or serviceName/); + + test.done(); + }, + }, }; diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts index d953b16d78ed7..b798ead19f96f 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts @@ -1487,5 +1487,74 @@ export = { }); test.done(); - } + }, + + 'When import a Fargate Service': { + 'with serviceArn'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const cluster = new ecs.Cluster(stack, 'EcsCluster'); + + // WHEN + const service = ecs.FargateService.fromFargateServiceAttributes(stack, 'EcsService', { + serviceArn: 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service', + cluster, + }); + + // THEN + test.equal(service.serviceArn, 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service'); + test.equal(service.serviceName, 'my-http-service'); + + test.done(); + }, + + 'with serviceName'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const pseudo = new cdk.ScopedAws(stack); + const cluster = new ecs.Cluster(stack, 'EcsCluster'); + + // WHEN + const service = ecs.FargateService.fromFargateServiceAttributes(stack, 'EcsService', { + serviceName: 'my-http-service', + cluster, + }); + + // THEN + test.deepEqual(stack.resolve(service.serviceArn), stack.resolve(`arn:${pseudo.partition}:ecs:${pseudo.region}:${pseudo.accountId}:service/my-http-service`)); + test.equal(service.serviceName, 'my-http-service'); + + test.done(); + }, + + 'throws an exception if both serviceArn and serviceName were provided for fromEc2ServiceAttributes'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const cluster = new ecs.Cluster(stack, 'EcsCluster'); + + test.throws(() => { + ecs.FargateService.fromFargateServiceAttributes(stack, 'EcsService', { + serviceArn: 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service', + serviceName: 'my-http-service', + cluster, + }); + }, /only specify either serviceArn or serviceName/); + + test.done(); + }, + + 'throws an exception if neither serviceArn nor serviceName were provided for fromEc2ServiceAttributes'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const cluster = new ecs.Cluster(stack, 'EcsCluster'); + + test.throws(() => { + ecs.FargateService.fromFargateServiceAttributes(stack, 'EcsService', { + cluster, + }); + }, /only specify either serviceArn or serviceName/); + + test.done(); + }, + }, }; diff --git a/packages/@aws-cdk/aws-eks/lib/kubectl-layer.ts b/packages/@aws-cdk/aws-eks/lib/kubectl-layer.ts index 211f6d8b36abd..71fc2c0f1674c 100644 --- a/packages/@aws-cdk/aws-eks/lib/kubectl-layer.ts +++ b/packages/@aws-cdk/aws-eks/lib/kubectl-layer.ts @@ -3,6 +3,8 @@ import { CfnResource, Construct, Stack, Token } from '@aws-cdk/core'; import * as crypto from 'crypto'; const KUBECTL_APP_ARN = 'arn:aws:serverlessrepo:us-east-1:903779448426:applications/lambda-layer-kubectl'; +const KUBECTL_APP_CN_ARN = 'arn:aws-cn:serverlessrepo:cn-north-1:487369736442:applications/lambda-layer-kubectl'; + const KUBECTL_APP_VERSION = '1.13.7'; export interface KubectlLayerProps { @@ -56,7 +58,7 @@ export class KubectlLayer extends Construct implements lambda.ILayerVersion { type: 'AWS::Serverless::Application', properties: { Location: { - ApplicationId: KUBECTL_APP_ARN, + ApplicationId: this.isChina() ? KUBECTL_APP_CN_ARN : KUBECTL_APP_ARN, SemanticVersion: version }, Parameters: { @@ -75,4 +77,9 @@ export class KubectlLayer extends Construct implements lambda.ILayerVersion { public addPermission(_id: string, _permission: lambda.LayerVersionPermission): void { return; } + + public isChina(): boolean { + const region = this.stack.region; + return !Token.isUnresolved(region) && region.startsWith('cn-'); + } } diff --git a/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts b/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts index b3c9672fc3f06..e53324dbcd997 100644 --- a/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts +++ b/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts @@ -34,7 +34,7 @@ export class KubectlProvider extends NestedStack { runtime: lambda.Runtime.PYTHON_3_7, handler: 'index.handler', timeout: Duration.minutes(15), - layers: [ KubectlLayer.getOrCreate(this, { version: "2.0.0-beta1" }) ], + layers: [ KubectlLayer.getOrCreate(this, { version: "2.0.0-beta2" }) ], memorySize: 256, }); diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index cbdbe28a60e4c..462ef29a93022 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.30", - "aws-sdk": "^2.637.0", + "aws-sdk": "^2.638.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json index e8d6c0434be84..4e0fd0d7605a5 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json @@ -1756,7 +1756,7 @@ }, "/", { - "Ref": "AssetParameters5736fa6dc98806541544f2c33f17a2495fe0723bd1e59ec62991d68ac3a6e690S3BucketE6BD216D" + "Ref": "AssetParameters6a008e167065eeab066c7f96e7f3c21c2636476b93c075681fba2953ae54a859S3BucketBA51B749" }, "/", { @@ -1766,7 +1766,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5736fa6dc98806541544f2c33f17a2495fe0723bd1e59ec62991d68ac3a6e690S3VersionKey27D33001" + "Ref": "AssetParameters6a008e167065eeab066c7f96e7f3c21c2636476b93c075681fba2953ae54a859S3VersionKey723A87EA" } ] } @@ -1779,7 +1779,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5736fa6dc98806541544f2c33f17a2495fe0723bd1e59ec62991d68ac3a6e690S3VersionKey27D33001" + "Ref": "AssetParameters6a008e167065eeab066c7f96e7f3c21c2636476b93c075681fba2953ae54a859S3VersionKey723A87EA" } ] } @@ -1913,17 +1913,17 @@ "Type": "String", "Description": "Artifact hash for asset \"809b8ac7e88704d37fac32bbd5cfa56be7ea4d3e9ddb682d216c4b6868cd8fa2\"" }, - "AssetParameters5736fa6dc98806541544f2c33f17a2495fe0723bd1e59ec62991d68ac3a6e690S3BucketE6BD216D": { + "AssetParameters6a008e167065eeab066c7f96e7f3c21c2636476b93c075681fba2953ae54a859S3BucketBA51B749": { "Type": "String", - "Description": "S3 bucket for asset \"5736fa6dc98806541544f2c33f17a2495fe0723bd1e59ec62991d68ac3a6e690\"" + "Description": "S3 bucket for asset \"6a008e167065eeab066c7f96e7f3c21c2636476b93c075681fba2953ae54a859\"" }, - "AssetParameters5736fa6dc98806541544f2c33f17a2495fe0723bd1e59ec62991d68ac3a6e690S3VersionKey27D33001": { + "AssetParameters6a008e167065eeab066c7f96e7f3c21c2636476b93c075681fba2953ae54a859S3VersionKey723A87EA": { "Type": "String", - "Description": "S3 key for asset version \"5736fa6dc98806541544f2c33f17a2495fe0723bd1e59ec62991d68ac3a6e690\"" + "Description": "S3 key for asset version \"6a008e167065eeab066c7f96e7f3c21c2636476b93c075681fba2953ae54a859\"" }, - "AssetParameters5736fa6dc98806541544f2c33f17a2495fe0723bd1e59ec62991d68ac3a6e690ArtifactHash19392521": { + "AssetParameters6a008e167065eeab066c7f96e7f3c21c2636476b93c075681fba2953ae54a859ArtifactHash22D2ECF0": { "Type": "String", - "Description": "Artifact hash for asset \"5736fa6dc98806541544f2c33f17a2495fe0723bd1e59ec62991d68ac3a6e690\"" + "Description": "Artifact hash for asset \"6a008e167065eeab066c7f96e7f3c21c2636476b93c075681fba2953ae54a859\"" }, "AssetParameters6348c4414dfcbc19ed407c51ecc75d12faf4ee3219e972437d4ceed53e5b79a0S3BucketEF51ACE0": { "Type": "String", diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster.ts b/packages/@aws-cdk/aws-eks/test/test.cluster.ts index 477add829c524..7fe6f9c76fb61 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster.ts @@ -4,6 +4,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as eks from '../lib'; +import { KubectlLayer } from '../lib/kubectl-layer'; import { spotInterruptHandler } from '../lib/spot-interrupt-handler'; import { testFixture, testFixtureNoVpc } from './util'; @@ -32,6 +33,49 @@ export = { test.done(); }, + 'create custom cluster correctly in any aws region'(test: Test) { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack', { env: { region: 'us-east-1' } }); + + // WHEN + const vpc = new ec2.Vpc(stack, 'VPC'); + new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: true, defaultCapacity: 0 }); + const layer = KubectlLayer.getOrCreate(stack, {}); + + // THEN + expect(stack).to(haveResource('Custom::AWSCDK-EKS-Cluster')); + expect(stack).to(haveResourceLike('AWS::Serverless::Application', { + Location: { + ApplicationId: 'arn:aws:serverlessrepo:us-east-1:903779448426:applications/lambda-layer-kubectl', + } + })); + test.equal(layer.isChina(), false); + test.done(); + }, + + 'create custom cluster correctly in any aws region in china'(test: Test) { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack', { env: { region: 'cn-north-1' } }); + + // WHEN + const vpc = new ec2.Vpc(stack, 'VPC'); + new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: true, defaultCapacity: 0 }); + new KubectlLayer(stack, 'NewLayer'); + const layer = KubectlLayer.getOrCreate(stack); + + // THEN + expect(stack).to(haveResource('Custom::AWSCDK-EKS-Cluster')); + expect(stack).to(haveResourceLike('AWS::Serverless::Application', { + Location: { + ApplicationId: 'arn:aws-cn:serverlessrepo:cn-north-1:487369736442:applications/lambda-layer-kubectl', + } + })); + test.equal(layer.isChina(), true); + test.done(); + }, + 'if "vpc" is not specified, vpc with default configuration will be created'(test: Test) { // GIVEN const { stack } = testFixtureNoVpc(); diff --git a/packages/@aws-cdk/aws-events-targets/package.json b/packages/@aws-cdk/aws-events-targets/package.json index 1e67bc1fd1de0..4eaeb0d777ef3 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -86,8 +86,8 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-codecommit": "0.0.0", - "aws-sdk": "^2.637.0", - "aws-sdk-mock": "^5.0.0", + "aws-sdk": "^2.638.0", + "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^24.9.0", diff --git a/packages/@aws-cdk/aws-iam/README.md b/packages/@aws-cdk/aws-iam/README.md index 13ff25b951e71..59f0db61645c6 100644 --- a/packages/@aws-cdk/aws-iam/README.md +++ b/packages/@aws-cdk/aws-iam/README.md @@ -188,6 +188,46 @@ const role = new iam.Role(this, 'MyRole', { }); ``` +### Parsing JSON Policy Documents + +The `PolicyDocument.fromJson` and `PolicyStatement.fromJson` static methods can be used to parse JSON objects. For example: + +```ts +const policyDocument = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "FirstStatement", + "Effect": "Allow", + "Action": ["iam:ChangePassword"], + "Resource": "*" + }, + { + "Sid": "SecondStatement", + "Effect": "Allow", + "Action": "s3:ListAllMyBuckets", + "Resource": "*" + }, + { + "Sid": "ThirdStatement", + "Effect": "Allow", + "Action": [ + "s3:List*", + "s3:Get*" + ], + "Resource": [ + "arn:aws:s3:::confidential-data", + "arn:aws:s3:::confidential-data/*" + ], + "Condition": {"Bool": {"aws:MultiFactorAuthPresent": "true"}} + } + ] +}; + +const newPolicyDocument = PolicyDocument.fromJson(policyDocument); + +``` + ### Features * Policy name uniqueness is enforced. If two policies by the same name are attached to the same diff --git a/packages/@aws-cdk/aws-iam/lib/policy-document.ts b/packages/@aws-cdk/aws-iam/lib/policy-document.ts index e55cdf910674d..803b314cc4b7f 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy-document.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy-document.ts @@ -24,6 +24,22 @@ export interface PolicyDocumentProps { * A PolicyDocument is a collection of statements */ export class PolicyDocument implements cdk.IResolvable { + + /** + * Creates a new PolicyDocument based on the object provided. + * This will accept an object created from the `.toJSON()` call + * @param obj the PolicyDocument in object form. + */ + public static fromJson(obj: any): PolicyDocument { + const newPolicyDocument = new PolicyDocument(); + const statement = obj.Statement ?? []; + if (statement && !Array.isArray(statement)) { + throw new Error('Statement must be an array'); + } + newPolicyDocument.addStatements(...obj.Statement.map((s: any) => PolicyStatement.fromJson(s))); + return newPolicyDocument; + } + public readonly creationStack: string[]; private readonly statements = new Array(); private readonly autoAssignSids: boolean; diff --git a/packages/@aws-cdk/aws-iam/lib/policy-statement.ts b/packages/@aws-cdk/aws-iam/lib/policy-statement.ts index 425a69e6b8a4d..01d0c3934909b 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy-statement.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy-statement.ts @@ -1,12 +1,45 @@ import * as cdk from '@aws-cdk/core'; import { AccountPrincipal, AccountRootPrincipal, Anyone, ArnPrincipal, CanonicalUserPrincipal, - FederatedPrincipal, IPrincipal, ServicePrincipal, ServicePrincipalOpts } from './principals'; + FederatedPrincipal, IPrincipal, PrincipalBase, PrincipalPolicyFragment, ServicePrincipal, ServicePrincipalOpts } from './principals'; import { mergePrincipal } from './util'; +const ensureArrayOrUndefined = (field: any) => { + if (field === undefined) { + return undefined; + } + if (typeof (field) !== "string" && !Array.isArray(field)) { + throw new Error("Fields must be either a string or an array of strings"); + } + if (Array.isArray(field) && !!field.find((f: any) => typeof (f) !== "string")) { + throw new Error("Fields must be either a string or an array of strings"); + } + return Array.isArray(field) ? field : [field]; +}; + /** * Represents a statement in an IAM policy document. */ export class PolicyStatement { + + /** + * Creates a new PolicyStatement based on the object provided. + * This will accept an object created from the `.toJSON()` call + * @param obj the PolicyStatement in object form. + */ + public static fromJson(obj: any) { + return new PolicyStatement({ + sid: obj.Sid, + actions: ensureArrayOrUndefined(obj.Action), + resources: ensureArrayOrUndefined(obj.Resource), + conditions: obj.Condition, + effect: obj.Effect, + notActions: ensureArrayOrUndefined(obj.NotAction), + notResources: ensureArrayOrUndefined(obj.NotResource), + principals: obj.Principal ? [ new JsonPrincipal(obj.Principal) ] : undefined, + notPrincipals: obj.NotPrincipal ? [ new JsonPrincipal(obj.NotPrincipal) ] : undefined + }); + } + /** * Statement ID for this statement */ @@ -29,6 +62,7 @@ export class PolicyStatement { } } + this.sid = props.sid; this.effect = props.effect || Effect.ALLOW; this.addActions(...props.actions || []); @@ -273,6 +307,17 @@ export enum Effect { * Interface for creating a policy statement */ export interface PolicyStatementProps { + /** + * The Sid (statement ID) is an optional identifier that you provide for the + * policy statement. You can assign a Sid value to each statement in a + * statement array. In services that let you specify an ID element, such as + * SQS and SNS, the Sid value is just a sub-ID of the policy document's ID. In + * IAM, the Sid value must be unique within a JSON policy. + * + * @default - no sid + */ + readonly sid?: string; + /** * List of actions to add to the statement * @@ -339,3 +384,21 @@ function noUndef(x: any): any { } return ret; } + +class JsonPrincipal extends PrincipalBase { + public readonly policyFragment: PrincipalPolicyFragment; + + constructor(json: any = { }) { + super(); + + // special case: if principal is a string, turn it into an "AWS" principal + if (typeof(json) === 'string') { + json = { AWS: json }; + } + + this.policyFragment = { + principalJson: json, + conditions: [] + }; + } +} diff --git a/packages/@aws-cdk/aws-iam/lib/util.ts b/packages/@aws-cdk/aws-iam/lib/util.ts index cd1a29cd7d7c3..14aa2fa58c7de 100644 --- a/packages/@aws-cdk/aws-iam/lib/util.ts +++ b/packages/@aws-cdk/aws-iam/lib/util.ts @@ -69,9 +69,9 @@ export function mergePrincipal(target: { [key: string]: string[] }, source: { [k for (const key of Object.keys(source)) { target[key] = target[key] || []; - const value = source[key]; + let value = source[key]; if (!Array.isArray(value)) { - throw new Error(`Principal value must be an array (it will be normalized later): ${value}`); + value = [ value ]; } target[key].push(...value); diff --git a/packages/@aws-cdk/aws-iam/test/policy-document.test.ts b/packages/@aws-cdk/aws-iam/test/policy-document.test.ts index 1b4e359e98d4b..a1cb97fed9821 100644 --- a/packages/@aws-cdk/aws-iam/test/policy-document.test.ts +++ b/packages/@aws-cdk/aws-iam/test/policy-document.test.ts @@ -573,18 +573,28 @@ describe('IAM polocy document', () => { expect(stack.resolve(doc1)).toEqual(stack.resolve(doc2)); }); + describe('fromJson', () => { + test("throws error when Statement isn't an array", () => { + expect(() => { + PolicyDocument.fromJson({ + Statement: 'asdf' + }); + }).toThrow(/Statement must be an array/); + }); + }); + test('adding another condition with the same operator does not delete the original', () => { const stack = new Stack(); const p = new PolicyStatement(); - p.addCondition('StringEquals', { 'kms:ViaService': 'service' }); + p.addCondition('StringEquals', {'kms:ViaService': 'service'}); p.addAccountCondition('12221121221'); expect(stack.resolve(p.toStatementJson())).toEqual({ Effect: 'Allow', - Condition: { StringEquals: { 'kms:ViaService': 'service', 'sts:ExternalId': '12221121221' } } + Condition: {StringEquals: {'kms:ViaService': 'service', 'sts:ExternalId': '12221121221'}} }); }); }); diff --git a/packages/@aws-cdk/aws-iam/test/policy-statement.test.ts b/packages/@aws-cdk/aws-iam/test/policy-statement.test.ts new file mode 100644 index 0000000000000..11207b7ed541b --- /dev/null +++ b/packages/@aws-cdk/aws-iam/test/policy-statement.test.ts @@ -0,0 +1,185 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import {AnyPrincipal, PolicyDocument, PolicyStatement} from '../lib'; + +describe('IAM policy statement', () => { + + describe('from JSON', () => { + test('parses with no principal', () => { + // given + const stack = new Stack(); + + const s = new PolicyStatement(); + s.addActions('service:action1', 'service:action2'); + s.addAllResources(); + s.addCondition('key', { equals: 'value' }); + + const doc1 = new PolicyDocument(); + doc1.addStatements(s); + + // when + const doc2 = PolicyDocument.fromJson(doc1.toJSON()); + + // then + expect(stack.resolve(doc2)).toEqual(stack.resolve(doc1)); + }); + + test('parses a given Principal', () => { + const stack = new Stack(); + + const s = new PolicyStatement(); + s.addActions('service:action1', 'service:action2'); + s.addAllResources(); + s.addArnPrincipal('somearn'); + s.addCondition('key', { equals: 'value' }); + + const doc1 = new PolicyDocument(); + doc1.addStatements(s); + + const doc2 = PolicyDocument.fromJson(doc1.toJSON()); + + expect(stack.resolve(doc2)).toEqual(stack.resolve(doc1)); + + }); + + test('parses a given notPrincipal', () => { + const stack = new Stack(); + + const s = new PolicyStatement(); + s.addActions('service:action1', 'service:action2'); + s.addAllResources(); + s.addNotPrincipals(new AnyPrincipal()); + s.addCondition('key', { equals: 'value' }); + + const doc1 = new PolicyDocument(); + doc1.addStatements(s); + + const doc2 = PolicyDocument.fromJson(doc1.toJSON()); + + expect(stack.resolve(doc2)).toEqual(stack.resolve(doc1)); + + }); + + test('parses with notAction', () => { + const stack = new Stack(); + + const s = new PolicyStatement(); + s.addNotActions('service:action3'); + s.addAllResources(); + + const doc1 = new PolicyDocument(); + doc1.addStatements(s); + + const doc2 = PolicyDocument.fromJson(doc1.toJSON()); + + expect(stack.resolve(doc2)).toEqual(stack.resolve(doc1)); + + }); + + test('parses with notActions', () => { + const stack = new Stack(); + + const s = new PolicyStatement(); + s.addNotActions('service:action3', 'service:action4'); + s.addAllResources(); + + const doc1 = new PolicyDocument(); + doc1.addStatements(s); + + const doc2 = PolicyDocument.fromJson(doc1.toJSON()); + + expect(stack.resolve(doc2)).toEqual(stack.resolve(doc1)); + + }); + + test('parses with notResource', () => { + const stack = new Stack(); + + const s = new PolicyStatement(); + s.addActions('service:action3', 'service:action4'); + s.addNotResources('resource1'); + + const doc1 = new PolicyDocument(); + doc1.addStatements(s); + + const doc2 = PolicyDocument.fromJson(doc1.toJSON()); + + expect(stack.resolve(doc2)).toEqual(stack.resolve(doc1)); + + }); + + test('parses with notResources', () => { + const stack = new Stack(); + + const s = new PolicyStatement(); + s.addActions('service:action3', 'service:action4'); + s.addNotResources('resource1', 'resource2'); + + const doc1 = new PolicyDocument(); + doc1.addStatements(s); + + const doc2 = PolicyDocument.fromJson(doc1.toJSON()); + + expect(stack.resolve(doc2)).toEqual(stack.resolve(doc1)); + + }); + + test('the kitchen sink', () => { + const stack = new Stack(); + + /* tslint:disable */ + const policyDocument = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "FirstStatement", + "Effect": "Allow", + "Action": "iam:ChangePassword", + "Resource": "*" + }, + { + "Sid": "SecondStatement", + "Effect": "Allow", + "Action": "s3:ListAllMyBuckets", + "Resource": "*" + }, + { + "Sid": "ThirdStatement", + "Effect": "Allow", + "Action": [ + "s3:List*", + "s3:Get*" + ], + "Resource": [ + "arn:aws:s3:::confidential-data", + "arn:aws:s3:::confidential-data/*" + ], + "Condition": {"Bool": {"aws:MultiFactorAuthPresent": "true"}} + } + ] + }; + /* tslint:enable */ + + const doc = PolicyDocument.fromJson(policyDocument); + + expect(stack.resolve(doc)).toEqual(policyDocument); + }); + + test('throws error with field data being object', () => { + expect(() => { + PolicyStatement.fromJson({ + Action: {} + }); + }).toThrow(/Fields must be either a string or an array of strings/); + }); + + test('throws error with field data being object', () => { + expect(() => { + PolicyStatement.fromJson({ + Action: [{}] + }); + }).toThrow(/Fields must be either a string or an array of strings/); + }); + }); + +}); diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index 6aec835a79dba..9bfbb0033314d 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -71,8 +71,8 @@ "@types/lodash": "^4.14.149", "@types/nodeunit": "^0.0.30", "@types/sinon": "^7.5.2", - "aws-sdk": "^2.637.0", - "aws-sdk-mock": "^5.0.0", + "aws-sdk": "^2.638.0", + "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index f4a7eb9d3e3f0..81b11125217d5 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.30", - "aws-sdk": "^2.637.0", + "aws-sdk": "^2.638.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-sqs/package.json b/packages/@aws-cdk/aws-sqs/package.json index 5957c20066769..7327831a320ff 100644 --- a/packages/@aws-cdk/aws-sqs/package.json +++ b/packages/@aws-cdk/aws-sqs/package.json @@ -65,7 +65,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@types/nodeunit": "^0.0.30", - "aws-sdk": "^2.637.0", + "aws-sdk": "^2.638.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/publish-to-topic.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/publish-to-topic.ts index 9c20fce63c4e0..4d1aa2b02decd 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/publish-to-topic.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/publish-to-topic.ts @@ -21,11 +21,15 @@ export interface PublishToTopicProps { * being sent to every subscription type. * * @see https://docs.aws.amazon.com/sns/latest/api/API_Publish.html#API_Publish_RequestParameters + * @default false */ readonly messagePerSubscriptionType?: boolean; /** - * Message subject + * Used as the "Subject" line when the message is delivered to email endpoints. + * Also included, if present, in the standard JSON messages delivered to other endpoints. + * + * @default - No subject */ readonly subject?: string; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-task-base-types.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-task-base-types.ts index a4238287cd819..aee94c1af0af7 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-task-base-types.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-task-base-types.ts @@ -1,3 +1,7 @@ +/** + * A list of container overrides that specify the name of a container + * and the overrides it should receive. + */ export interface ContainerOverride { /** * Name of the container inside the task definition @@ -7,33 +11,38 @@ export interface ContainerOverride { /** * Command to run inside the container * - * @default Default command + * @default - Default command from the Docker image or the task definition */ readonly command?: string[]; /** - * Variables to set in the container's environment + * The environment variables to send to the container. + * + * You can add new environment variables, which are added to the container at launch, + * or you can override the existing environment variables from the Docker image or the task definition. + * + * @default - The existing environment variables from the Docker image or the task definition */ readonly environment?: TaskEnvironmentVariable[]; /** * The number of cpu units reserved for the container * - * @Default The default value from the task definition. + * @default - The default value from the task definition. */ readonly cpu?: number; /** - * Hard memory limit on the container + * The hard limit (in MiB) of memory to present to the container * - * @Default The default value from the task definition. + * @default - The default value from the task definition. */ readonly memoryLimit?: number; /** - * Soft memory limit on the container + * The soft limit (in MiB) of memory to reserve for the container * - * @Default The default value from the task definition. + * @default - The default value from the task definition. */ readonly memoryReservation?: number; } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-task-base.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-task-base.ts index afbac0c0446c7..18552c8ebd64a 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-task-base.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-task-base.ts @@ -28,6 +28,8 @@ export interface CommonEcsRunTaskProps { * * Key is the name of the container to override, value is the * values you want to override. + * + * @default - No overrides */ readonly containerOverrides?: ContainerOverride[]; @@ -47,6 +49,8 @@ export interface CommonEcsRunTaskProps { export interface EcsRunTaskBaseProps extends CommonEcsRunTaskProps { /** * Additional parameters to pass to the base task + * + * @default - No additional parameters passed */ readonly parameters?: {[key: string]: any}; } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-lambda-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-lambda-task.ts index 0623ccf38f699..ba3b52187349c 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-lambda-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-lambda-task.ts @@ -9,6 +9,8 @@ import { getResourceArn } from './resource-arn-suffix'; export interface RunLambdaTaskProps { /** * The JSON that you want to provide to your Lambda function as input. + * + * @default - No payload */ readonly payload?: { [key: string]: any }; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker-task-base-types.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker-task-base-types.ts index 13d7f3f82776c..798d72db2d5d9 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker-task-base-types.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker-task-base-types.ts @@ -7,36 +7,45 @@ import * as s3 from '@aws-cdk/aws-s3'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import { Construct, Duration } from '@aws-cdk/core'; +/** + * Task to train a machine learning model using Amazon SageMaker + * @experimental + */ export interface ISageMakerTask extends sfn.IStepFunctionsTask, iam.IGrantable {} -// -// Create Training Job types -// - /** + * Specify the training algorithm and algorithm-specific metadata * @experimental */ export interface AlgorithmSpecification { /** * Name of the algorithm resource to use for the training job. + * This must be an algorithm resource that you created or subscribe to on AWS Marketplace. + * If you specify a value for this parameter, you can't specify a value for TrainingImage. + * + * @default - No algorithm is specified */ readonly algorithmName?: string; /** * List of metric definition objects. Each object specifies the metric name and regular expressions used to parse algorithm logs. + * + * @default - No metrics */ readonly metricDefinitions?: MetricDefinition[]; /** * Registry path of the Docker image that contains the training algorithm. + * + * @default - No Docker image is specified */ readonly trainingImage?: DockerImage; /** * Input mode that the algorithm supports. * - * @default is 'File' mode + * @default 'File' mode */ readonly trainingInputMode?: InputMode; } @@ -55,31 +64,43 @@ export interface Channel { /** * Compression type if training data is compressed + * + * @default - None */ readonly compressionType?: CompressionType; /** - * Content type + * The MIME type of the data. + * + * @default - None */ readonly contentType?: string; /** - * Location of the data channel + * Location of the channel data. */ readonly dataSource: DataSource; /** * Input mode to use for the data channel in a training job. + * + * @default - None */ readonly inputMode?: InputMode; /** - * Record wrapper type + * Specify RecordIO as the value when input data is in raw format but the training algorithm requires the RecordIO format. + * In this case, Amazon SageMaker wraps each individual S3 object in a RecordIO record. + * If the input data is already in RecordIO format, you don't need to set this attribute. + * + * @default - None */ readonly recordWrapperType?: RecordWrapperType; /** * Shuffle config option for input data in a channel. + * + * @default - None */ readonly shuffleConfig?: ShuffleConfig; } @@ -111,21 +132,29 @@ export interface DataSource { /** * S3 location of the channel data. * + * @see https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_S3DataSource.html + * * @experimental */ export interface S3DataSource { /** * List of one or more attribute names to use that are found in a specified augmented manifest file. + * + * @default - No attribute names */ readonly attributeNames?: string[]; /** * S3 Data Distribution Type + * + * @default - None */ readonly s3DataDistributionType?: S3DataDistributionType; /** * S3 Data Type + * + * @default S3_PREFIX */ readonly s3DataType?: S3DataType; @@ -136,11 +165,14 @@ export interface S3DataSource { } /** + * Configures the S3 bucket where SageMaker will save the result of model training * @experimental */ export interface OutputDataConfig { /** * Optional KMS encryption key that Amazon SageMaker uses to encrypt the model artifacts at rest using Amazon S3 server-side encryption. + * + * @default - Amazon SageMaker uses the default KMS key for Amazon S3 for your role's account */ readonly encryptionKey?: kms.IKey; @@ -151,16 +183,23 @@ export interface OutputDataConfig { } /** + * Specifies a limit to how long a model training job can run. + * When the job reaches the time limit, Amazon SageMaker ends the training job. + * * @experimental */ export interface StoppingCondition { /** * The maximum length of time, in seconds, that the training or compilation job can run. + * + * @default - 1 hour */ readonly maxRuntime?: Duration; } /** + * Specifies the resources, ML compute instances, and ML storage volumes to deploy for model training. + * * @experimental */ export interface ResourceConfig { @@ -181,6 +220,8 @@ export interface ResourceConfig { /** * KMS key that Amazon SageMaker uses to encrypt data on the storage volume attached to the ML compute instance(s) that run the training job. + * + * @default - Amazon SageMaker uses the default KMS key for Amazon S3 for your role's account */ readonly volumeEncryptionKey?: kms.IKey; @@ -193,17 +234,20 @@ export interface ResourceConfig { } /** + * Specifies the VPC that you want your Amazon SageMaker training job to connect to. * * @experimental */ export interface VpcConfig { /** - * VPC id + * VPC */ readonly vpc: ec2.IVpc; /** * VPC subnets. + * + * @default - Private Subnets are selected */ readonly subnets?: ec2.SubnetSelection; } @@ -227,9 +271,15 @@ export interface MetricDefinition { } /** + * Stores information about the location of an object in Amazon S3 + * * @experimental */ export interface S3LocationConfig { + + /** + * Uniquely identifies the resource in Amazon S3 + */ readonly uri: string; } @@ -458,11 +508,15 @@ export interface TransformInput { /** * The compression type of the transform data. + * + * @default NONE */ readonly compressionType?: CompressionType; /** * Multipurpose internet mail extension (MIME) type of the data. + * + * @default - None */ readonly contentType?: string; @@ -473,6 +527,8 @@ export interface TransformInput { /** * Method to use to split the transform job's data files into smaller batches. + * + * @default NONE */ readonly splitType?: SplitType; } @@ -519,16 +575,22 @@ export interface TransformOutput { /** * MIME type used to specify the output data. + * + * @default - None */ readonly accept?: string; /** * Defines how to assemble the results of the transform job as a single S3 object. + * + * @default - None */ readonly assembleWith?: AssembleWith; /** * AWS KMS key that Amazon SageMaker uses to encrypt the model artifacts at rest using Amazon S3 server-side encryption. + * + * @default - default KMS key for Amazon S3 for your role's account. */ readonly encryptionKey?: kms.Key; @@ -546,7 +608,7 @@ export interface TransformOutput { export interface TransformResources { /** - * Nmber of ML compute instances to use in the transform job + * Number of ML compute instances to use in the transform job */ readonly instanceCount: number; @@ -557,6 +619,8 @@ export interface TransformResources { /** * AWS KMS key that Amazon SageMaker uses to encrypt data on the storage volume attached to the ML compute instance(s). + * + * @default - None */ readonly volumeKmsKeyId?: kms.Key; } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker-train-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker-train-task.ts index 8f33ab81e4ef7..902f253ac794e 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker-train-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker-train-task.ts @@ -7,7 +7,9 @@ import { AlgorithmSpecification, Channel, InputMode, OutputDataConfig, ResourceC S3DataType, StoppingCondition, VpcConfig, } from './sagemaker-task-base-types'; /** - * @experimental + * Properties for creating an Amazon SageMaker training job + * + * @experimental */ export interface SagemakerTrainTaskProps { @@ -41,7 +43,11 @@ export interface SagemakerTrainTaskProps { readonly algorithmSpecification: AlgorithmSpecification; /** - * Hyperparameters to be used for the train job. + * Algorithm-specific parameters that influence the quality of the model. Set hyperparameters before you start the learning process. + * For a list of hyperparameters provided by Amazon SageMaker + * @see https://docs.aws.amazon.com/sagemaker/latest/dg/algos.html + * + * @default - No hyperparameters */ readonly hyperparameters?: {[key: string]: any}; @@ -52,6 +58,8 @@ export interface SagemakerTrainTaskProps { /** * Tags to be applied to the train job. + * + * @default - No tags */ readonly tags?: {[key: string]: string}; @@ -61,17 +69,23 @@ export interface SagemakerTrainTaskProps { readonly outputDataConfig: OutputDataConfig; /** - * Identifies the resources, ML compute instances, and ML storage volumes to deploy for model training. + * Specifies the resources, ML compute instances, and ML storage volumes to deploy for model training. + * + * @default - 1 instance of EC2 `M4.XLarge` with `10GB` volume */ readonly resourceConfig?: ResourceConfig; /** * Sets a time limit for training. + * + * @default - max runtime of 1 hour */ readonly stoppingCondition?: StoppingCondition; /** * Specifies the VPC that you want your training job to connect to. + * + * @default - No VPC */ readonly vpcConfig?: VpcConfig; } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker-transform-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker-transform-task.ts index 37c15643dd84a..c185303a0844c 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker-transform-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker-transform-task.ts @@ -6,6 +6,8 @@ import { getResourceArn } from './resource-arn-suffix'; import { BatchStrategy, S3DataType, TransformInput, TransformOutput, TransformResources } from './sagemaker-task-base-types'; /** + * Properties for creating an Amazon SageMaker training job task + * * @experimental */ export interface SagemakerTransformProps { @@ -16,7 +18,9 @@ export interface SagemakerTransformProps { readonly transformJobName: string; /** - * Role for thte Training Job. + * Role for the Training Job. + * + * @default - A role is created with `AmazonSageMakerFullAccess` managed policy */ readonly role?: iam.IRole; @@ -31,21 +35,30 @@ export interface SagemakerTransformProps { /** * Number of records to include in a mini-batch for an HTTP inference request. + * + * @default - No batch strategy */ readonly batchStrategy?: BatchStrategy; /** * Environment variables to set in the Docker container. + * + * @default - No environment variables */ readonly environment?: {[key: string]: string}; /** * Maximum number of parallel requests that can be sent to each instance in a transform job. + * + * @default - Amazon SageMaker checks the optional execution-parameters to determine the settings for your chosen algorithm. + * If the execution-parameters endpoint is not enabled, the default value is 1. */ readonly maxConcurrentTransforms?: number; /** * Maximum allowed size of the payload, in MB. + * + * @default 6 */ readonly maxPayloadInMB?: number; @@ -56,6 +69,8 @@ export interface SagemakerTransformProps { /** * Tags to be applied to the train job. + * + * @default - No tags */ readonly tags?: {[key: string]: string}; @@ -71,6 +86,8 @@ export interface SagemakerTransformProps { /** * ML compute instances for the transform job. + * + * @default - 1 instance of type M4.XLarge */ readonly transformResources?: TransformResources; } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/start-execution.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/start-execution.ts index a333f99822eab..3ac89799f2cc1 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/start-execution.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/start-execution.ts @@ -11,6 +11,8 @@ export interface StartExecutionProps { * The JSON input for the execution, same as that of StartExecution. * * @see https://docs.aws.amazon.com/step-functions/latest/apireference/API_StartExecution.html + * + * @default - No input */ readonly input?: { [key: string]: any }; @@ -18,6 +20,8 @@ export interface StartExecutionProps { * The name of the execution, same as that of StartExecution. * * @see https://docs.aws.amazon.com/step-functions/latest/apireference/API_StartExecution.html + * + * @default - None */ readonly name?: string; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json index 8ff438593eb38..466b62e54e6d2 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json @@ -129,62 +129,6 @@ "stability": "experimental", "awslint": { "exclude": [ - "docs-public-apis:@aws-cdk/aws-stepfunctions-tasks.S3LocationConfig.uri", - "docs-public-apis:@aws-cdk/aws-stepfunctions-tasks.AlgorithmSpecification", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.AlgorithmSpecification.metricDefinitions", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.AlgorithmSpecification.trainingImage", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.Channel.compressionType", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.Channel.contentType", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.Channel.inputMode", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.Channel.recordWrapperType", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.Channel.shuffleConfig", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.CommonEcsRunTaskProps.containerOverrides", - "docs-public-apis:@aws-cdk/aws-stepfunctions-tasks.ContainerOverride", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.ContainerOverride.cpu", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.ContainerOverride.environment", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.ContainerOverride.memoryLimit", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.ContainerOverride.memoryReservation", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.EcsRunTaskBaseProps.parameters", - "docs-public-apis:@aws-cdk/aws-stepfunctions-tasks.ISageMakerTask", - "docs-public-apis:@aws-cdk/aws-stepfunctions-tasks.OutputDataConfig", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.OutputDataConfig.encryptionKey", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.PublishToTopicProps.messagePerSubscriptionType", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.PublishToTopicProps.subject", - "docs-public-apis:@aws-cdk/aws-stepfunctions-tasks.ResourceConfig", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.ResourceConfig.volumeEncryptionKey", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.RunLambdaTaskProps.payload", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.S3DataSource.attributeNames", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.S3DataSource.s3DataDistributionType", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.S3DataSource.s3DataType", - "docs-public-apis:@aws-cdk/aws-stepfunctions-tasks.S3LocationConfig", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.AlgorithmSpecification.algorithmName", - "docs-public-apis:@aws-cdk/aws-stepfunctions-tasks.SagemakerTrainTaskProps", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.SagemakerTrainTaskProps.hyperparameters", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.SagemakerTrainTaskProps.resourceConfig", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.SagemakerTrainTaskProps.stoppingCondition", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.SagemakerTrainTaskProps.tags", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.SagemakerTrainTaskProps.vpcConfig", - "docs-public-apis:@aws-cdk/aws-stepfunctions-tasks.SagemakerTransformProps", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.SagemakerTransformProps.batchStrategy", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.SagemakerTransformProps.environment", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.SagemakerTransformProps.maxConcurrentTransforms", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.SagemakerTransformProps.maxPayloadInMB", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.SagemakerTransformProps.role", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.SagemakerTransformProps.tags", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.SagemakerTransformProps.transformResources", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.StartExecutionProps.input", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.StartExecutionProps.name", - "docs-public-apis:@aws-cdk/aws-stepfunctions-tasks.StoppingCondition", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.StoppingCondition.maxRuntime", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.TransformInput.compressionType", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.TransformInput.contentType", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.TransformInput.splitType", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.TransformOutput.accept", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.TransformOutput.assembleWith", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.TransformOutput.encryptionKey", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.TransformResources.volumeKmsKeyId", - "docs-public-apis:@aws-cdk/aws-stepfunctions-tasks.VpcConfig", - "props-default-doc:@aws-cdk/aws-stepfunctions-tasks.VpcConfig.subnets" ] } } diff --git a/packages/@aws-cdk/cdk-assets-schema/README.md b/packages/@aws-cdk/cdk-assets-schema/README.md index d9cce2f0049b9..80db1a478bfc7 100644 --- a/packages/@aws-cdk/cdk-assets-schema/README.md +++ b/packages/@aws-cdk/cdk-assets-schema/README.md @@ -1,5 +1,4 @@ # cdk-assets-schema - --- diff --git a/packages/@aws-cdk/cdk-assets-schema/lib/aws-destination.ts b/packages/@aws-cdk/cdk-assets-schema/lib/aws-destination.ts index 1f3688f5dec3a..e4b00ed4d308d 100644 --- a/packages/@aws-cdk/cdk-assets-schema/lib/aws-destination.ts +++ b/packages/@aws-cdk/cdk-assets-schema/lib/aws-destination.ts @@ -22,5 +22,24 @@ export interface AwsDestination { * @default - No ExternalId will be supplied */ readonly assumeRoleExternalId?: string; +} +/** + * Placeholders which can be used in the destinations + */ +export class Placeholders { + /** + * Insert this into the destination fields to be replaced with the current region + */ + public static readonly CURRENT_REGION = '${AWS::Region}'; + + /** + * Insert this into the destination fields to be replaced with the current account + */ + public static readonly CURRENT_ACCOUNT = '${AWS::AccountId}'; + + /** + * Insert this into the destination fields to be replaced with the current partition + */ + public static readonly CURRENT_PARTITION = '${AWS::Partition}'; } \ No newline at end of file diff --git a/packages/@aws-cdk/cdk-assets-schema/lib/docker-image-asset.ts b/packages/@aws-cdk/cdk-assets-schema/lib/docker-image-asset.ts index 24bffa056d2dd..dd56653288dc0 100644 --- a/packages/@aws-cdk/cdk-assets-schema/lib/docker-image-asset.ts +++ b/packages/@aws-cdk/cdk-assets-schema/lib/docker-image-asset.ts @@ -61,15 +61,4 @@ export interface DockerImageDestination extends AwsDestination { * Tag of the image to publish */ readonly imageTag: string; - - /** - * Full Docker tag coordinates (registry and repository and tag) - * - * Example: - * - * ``` - * 1234.dkr.ecr.REGION.amazonaws.com/REPO:TAG - * ``` - */ - readonly imageUri: string; } diff --git a/packages/@aws-cdk/cdk-assets-schema/lib/validate.ts b/packages/@aws-cdk/cdk-assets-schema/lib/validate.ts index 221ffc8524216..2e8279534a833 100644 --- a/packages/@aws-cdk/cdk-assets-schema/lib/validate.ts +++ b/packages/@aws-cdk/cdk-assets-schema/lib/validate.ts @@ -97,9 +97,8 @@ function isDockerImageAsset(entry: object): DockerImageAsset { expectKey(destination, 'assumeRoleExternalId', isString, true); expectKey(destination, 'repositoryName', isString); expectKey(destination, 'imageTag', isString); - expectKey(destination, 'imageUri', isString); return destination; })); return entry; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/cdk-assets-schema/test/validate.test.ts b/packages/@aws-cdk/cdk-assets-schema/test/validate.test.ts index 5beae92ce626b..88df3f9dea82f 100644 --- a/packages/@aws-cdk/cdk-assets-schema/test/validate.test.ts +++ b/packages/@aws-cdk/cdk-assets-schema/test/validate.test.ts @@ -14,7 +14,6 @@ test('Correctly validate Docker image asset', () => { region: 'us-north-20', repositoryName: 'REPO', imageTag: 'TAG', - imageUri: 'URI', }, }, }, @@ -79,4 +78,4 @@ test('Throw on invalid file asset', () => { }, }); }).toThrow(/Expected a string, got '3'/); -}); \ No newline at end of file +}); diff --git a/packages/@aws-cdk/core/lib/duration.ts b/packages/@aws-cdk/core/lib/duration.ts index 8801d6a771a1a..69152a27b5d28 100644 --- a/packages/@aws-cdk/core/lib/duration.ts +++ b/packages/@aws-cdk/core/lib/duration.ts @@ -10,6 +10,8 @@ import { Token } from "./token"; */ export class Duration { /** + * Create a Duration representing an amount of milliseconds + * * @param amount the amount of Milliseconds the `Duration` will represent. * @returns a new `Duration` representing `amount` ms. */ @@ -18,6 +20,8 @@ export class Duration { } /** + * Create a Duration representing an amount of seconds + * * @param amount the amount of Seconds the `Duration` will represent. * @returns a new `Duration` representing `amount` Seconds. */ @@ -26,6 +30,8 @@ export class Duration { } /** + * Create a Duration representing an amount of minutes + * * @param amount the amount of Minutes the `Duration` will represent. * @returns a new `Duration` representing `amount` Minutes. */ @@ -34,6 +40,8 @@ export class Duration { } /** + * Create a Duration representing an amount of hours + * * @param amount the amount of Hours the `Duration` will represent. * @returns a new `Duration` representing `amount` Hours. */ @@ -42,6 +50,8 @@ export class Duration { } /** + * Create a Duration representing an amount of days + * * @param amount the amount of Days the `Duration` will represent. * @returns a new `Duration` representing `amount` Days. */ @@ -50,8 +60,9 @@ export class Duration { } /** - * Parse a period formatted according to the ISO 8601 standard (see https://www.iso.org/fr/standard/70907.html). + * Parse a period formatted according to the ISO 8601 standard * + * @see https://www.iso.org/fr/standard/70907.html * @param duration an ISO-formtted duration to be parsed. * @returns the parsed `Duration`. */ @@ -64,11 +75,11 @@ export class Duration { if (!days && !hours && !minutes && !seconds) { throw new Error(`Not a valid ISO duration: ${duration}`); } - return Duration.seconds( - _toInt(seconds) - + (_toInt(minutes) * TimeUnit.Minutes.inSeconds) - + (_toInt(hours) * TimeUnit.Hours.inSeconds) - + (_toInt(days) * TimeUnit.Days.inSeconds) + return Duration.millis( + _toInt(seconds) * TimeUnit.Seconds.inMillis + + (_toInt(minutes) * TimeUnit.Minutes.inMillis) + + (_toInt(hours) * TimeUnit.Hours.inMillis) + + (_toInt(days) * TimeUnit.Days.inMillis) ); function _toInt(str: string): number { @@ -90,6 +101,8 @@ export class Duration { } /** + * Return the total number of milliseconds in this Duration + * * @returns the value of this `Duration` expressed in Milliseconds. */ public toMilliseconds(opts: TimeConversionOptions = {}): number { @@ -97,6 +110,8 @@ export class Duration { } /** + * Return the total number of seconds in this Duration + * * @returns the value of this `Duration` expressed in Seconds. */ public toSeconds(opts: TimeConversionOptions = {}): number { @@ -104,6 +119,8 @@ export class Duration { } /** + * Return the total number of minutes in this Duration + * * @returns the value of this `Duration` expressed in Minutes. */ public toMinutes(opts: TimeConversionOptions = {}): number { @@ -111,6 +128,8 @@ export class Duration { } /** + * Return the total number of hours in this Duration + * * @returns the value of this `Duration` expressed in Hours. */ public toHours(opts: TimeConversionOptions = {}): number { @@ -118,6 +137,8 @@ export class Duration { } /** + * Return the total number of days in this Duration + * * @returns the value of this `Duration` expressed in Days. */ public toDays(opts: TimeConversionOptions = {}): number { @@ -125,9 +146,12 @@ export class Duration { } /** - * @returns an ISO 8601 representation of this period (see https://www.iso.org/fr/standard/70907.html). + * Return an ISO 8601 representation of this period + * + * @returns a string starting with 'PT' describing the period + * @see https://www.iso.org/fr/standard/70907.html */ - public toISOString(): string { + public toIsoString(): string { if (this.amount === 0) { return 'PT0S'; } switch (this.unit) { case TimeUnit.Seconds: @@ -143,6 +167,52 @@ export class Duration { } } + /** + * Return an ISO 8601 representation of this period + * + * @returns a string starting with 'PT' describing the period + * @see https://www.iso.org/fr/standard/70907.html + * @deprecated Use `toIsoString()` instead. + */ + public toISOString(): string { + return this.toIsoString(); + } + + /** + * Turn this duration into a human-readable string + */ + public toHumanString(): string { + if (this.amount === 0) { return fmtUnit(0, this.unit); } + if (Token.isUnresolved(this.amount)) { return ` ${this.unit.label}`; } + + let millis = convert(this.amount, this.unit, TimeUnit.Milliseconds, { integral: false }); + const parts = new Array(); + + for (const unit of [TimeUnit.Days, TimeUnit.Hours, TimeUnit.Hours, TimeUnit.Minutes, TimeUnit.Seconds]) { + const wholeCount = Math.floor(convert(millis, TimeUnit.Milliseconds, unit, { integral: false })); + if (wholeCount > 0) { + parts.push(fmtUnit(wholeCount, unit)); + millis -= wholeCount * unit.inMillis; + } + } + + // Remainder in millis + if (millis > 0) { + parts.push(fmtUnit(millis, TimeUnit.Milliseconds)); + } + + // 2 significant parts, that's totally enough for humans + return parts.slice(0, 2).join(' '); + + function fmtUnit(amount: number, unit: TimeUnit) { + if (amount === 1) { + // All of the labels end in 's' + return `${amount} ${unit.label.substring(0, unit.label.length - 1)}`; + } + return `${amount} ${unit.label}`; + } + } + /** * Returns a string representation of this `Duration` that is also a Token that cannot be successfully resolved. This * protects users against inadvertently stringifying a `Duration` object, when they should have called one of the @@ -183,13 +253,16 @@ export interface TimeConversionOptions { } class TimeUnit { - public static readonly Milliseconds = new TimeUnit('millis', 0.001); - public static readonly Seconds = new TimeUnit('seconds', 1); - public static readonly Minutes = new TimeUnit('minutes', 60); - public static readonly Hours = new TimeUnit('hours', 3_600); - public static readonly Days = new TimeUnit('days', 86_400); + public static readonly Milliseconds = new TimeUnit('millis', 1); + public static readonly Seconds = new TimeUnit('seconds', 1_000); + public static readonly Minutes = new TimeUnit('minutes', 60_000); + public static readonly Hours = new TimeUnit('hours', 3_600_000); + public static readonly Days = new TimeUnit('days', 86_400_000); - private constructor(public readonly label: string, public readonly inSeconds: number) { + private constructor(public readonly label: string, public readonly inMillis: number) { + // MAX_SAFE_INTEGER is 2^53, so by representing our duration in millis (the lowest + // common unit) the highest duration we can represent is + // 2^53 / 86*10^6 ~= 104 * 10^6 days (about 100 million days). } public toString() { @@ -198,8 +271,8 @@ class TimeUnit { } function convert(amount: number, fromUnit: TimeUnit, toUnit: TimeUnit, { integral = true }: TimeConversionOptions) { - if (fromUnit.inSeconds === toUnit.inSeconds) { return amount; } - const multiplier = fromUnit.inSeconds / toUnit.inSeconds; + if (fromUnit.inMillis === toUnit.inMillis) { return amount; } + const multiplier = fromUnit.inMillis / toUnit.inMillis; if (Token.isUnresolved(amount)) { throw new Error(`Unable to perform time unit conversion on un-resolved token ${amount}.`); diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index cff11f68a3298..3e32e60453cd7 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -57,17 +57,6 @@ "docs-public-apis:@aws-cdk/core.ConstructNode.root", "docs-public-apis:@aws-cdk/core.ContextProvider.getKey", "docs-public-apis:@aws-cdk/core.ContextProvider.getValue", - "docs-public-apis:@aws-cdk/core.Duration.days", - "docs-public-apis:@aws-cdk/core.Duration.hours", - "docs-public-apis:@aws-cdk/core.Duration.millis", - "docs-public-apis:@aws-cdk/core.Duration.minutes", - "docs-public-apis:@aws-cdk/core.Duration.seconds", - "docs-public-apis:@aws-cdk/core.Duration.toDays", - "docs-public-apis:@aws-cdk/core.Duration.toHours", - "docs-public-apis:@aws-cdk/core.Duration.toISOString", - "docs-public-apis:@aws-cdk/core.Duration.toMilliseconds", - "docs-public-apis:@aws-cdk/core.Duration.toMinutes", - "docs-public-apis:@aws-cdk/core.Duration.toSeconds", "docs-public-apis:@aws-cdk/core.Lazy.anyValue", "docs-public-apis:@aws-cdk/core.Lazy.listValue", "docs-public-apis:@aws-cdk/core.Lazy.numberValue", diff --git a/packages/@aws-cdk/core/test/test.duration.ts b/packages/@aws-cdk/core/test/test.duration.ts index 41a7a6cc110c7..81e0822b55a77 100644 --- a/packages/@aws-cdk/core/test/test.duration.ts +++ b/packages/@aws-cdk/core/test/test.duration.ts @@ -1,5 +1,5 @@ import * as nodeunit from 'nodeunit'; -import { Duration, Stack, Token } from '../lib'; +import { Duration, Lazy, Stack, Token } from '../lib'; export = nodeunit.testCase({ 'negative amount'(test: nodeunit.Test) { @@ -93,6 +93,22 @@ export = nodeunit.testCase({ test.done(); }, + 'toIsoString'(test: nodeunit.Test) { + test.equal(Duration.seconds(0).toIsoString(), 'PT0S'); + test.equal(Duration.minutes(0).toIsoString(), 'PT0S'); + test.equal(Duration.hours(0).toIsoString(), 'PT0S'); + test.equal(Duration.days(0).toIsoString(), 'PT0S'); + + test.equal(Duration.seconds(5).toIsoString(), 'PT5S'); + test.equal(Duration.minutes(5).toIsoString(), 'PT5M'); + test.equal(Duration.hours(5).toIsoString(), 'PT5H'); + test.equal(Duration.days(5).toIsoString(), 'PT5D'); + + test.equal(Duration.seconds(1 + 60 * (1 + 60 * (1 + 24))).toIsoString(), 'PT1D1H1M1S'); + + test.done(); + }, + 'parse'(test: nodeunit.Test) { test.equal(Duration.parse('PT0S').toSeconds(), 0); test.equal(Duration.parse('PT0M').toSeconds(), 0); @@ -107,7 +123,26 @@ export = nodeunit.testCase({ test.equal(Duration.parse('PT1D1H1M1S').toSeconds(), 1 + 60 * (1 + 60 * (1 + 24))); test.done(); - } + }, + + 'to human string'(test: nodeunit.Test) { + test.equal(Duration.minutes(0).toHumanString(), '0 minutes'); + test.equal(Duration.minutes(Lazy.numberValue({ produce: () => 5 })).toHumanString(), ' minutes'); + + test.equal(Duration.minutes(10).toHumanString(), '10 minutes'); + test.equal(Duration.minutes(1).toHumanString(), '1 minute'); + + test.equal(Duration.minutes(62).toHumanString(), '1 hour 2 minutes'); + + test.equal(Duration.seconds(3666).toHumanString(), '1 hour 1 minute'); + + test.equal(Duration.millis(3000).toHumanString(), '3 seconds'); + test.equal(Duration.millis(3666).toHumanString(), '3 seconds 666 millis'); + + test.equal(Duration.millis(3.6).toHumanString(), '3.6 millis'); + + test.done(); + }, }); function floatEqual(test: nodeunit.Test, actual: number, expected: number) { diff --git a/packages/@aws-cdk/custom-resources/README.md b/packages/@aws-cdk/custom-resources/README.md index 13cd4a4002fe3..e75d619a7875e 100644 --- a/packages/@aws-cdk/custom-resources/README.md +++ b/packages/@aws-cdk/custom-resources/README.md @@ -380,6 +380,19 @@ Since a successful resource provisioning might or might not produce outputs, thi In both the cases, you will get a synth time error if you attempt to use it in conjunction with `ignoreErrorCodesMatching`. +### Customizing the Lambda function implementing the custom resource +Use the `role`, `timeout` and `logRetention` properties to customize the Lambda function implementing the custom +resource: + +```ts +new AwsCustomResource(this, 'Customized', { + // other props here + role: myRole, // must be assumable by the `lambda.amazonaws.com` service principal + timeout: cdk.Duration.minutes(10) // defaults to 2 minutes + logRetention: logs.RetentionDays.ONE_WEEK // defaults to never delete logs +}) +``` + ### Examples #### Verify a domain with SES diff --git a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts index 881954c056e97..e0ea2b0d06199 100644 --- a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts +++ b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts @@ -1,6 +1,7 @@ import { CustomResource, CustomResourceProvider } from '@aws-cdk/aws-cloudformation'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; +import * as logs from '@aws-cdk/aws-logs'; import * as cdk from '@aws-cdk/core'; import * as fs from 'fs'; import * as path from 'path'; @@ -235,6 +236,14 @@ export interface AwsCustomResourceProps { * @default Duration.minutes(2) */ readonly timeout?: cdk.Duration + + /** + * The number of days log events of the Lambda function implementing + * this custom resource are kept in CloudWatch Logs. + * + * @default logs.RetentionDays.INFINITE + */ + readonly logRetention?: logs.RetentionDays; } /** @@ -292,6 +301,7 @@ export class AwsCustomResource extends cdk.Construct implements iam.IGrantable { lambdaPurpose: 'AWS', timeout: props.timeout || cdk.Duration.minutes(2), role: props.role, + logRetention: props.logRetention, }); this.grantPrincipal = provider.grantPrincipal; diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index 7c93b67d0f5c0..9074a5a103459 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -73,8 +73,8 @@ "@types/aws-lambda": "^8.10.39", "@types/fs-extra": "^8.1.0", "@types/sinon": "^7.5.2", - "aws-sdk": "^2.637.0", - "aws-sdk-mock": "^5.0.0", + "aws-sdk": "^2.638.0", + "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -87,6 +87,7 @@ "@aws-cdk/aws-cloudformation": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-stepfunctions": "0.0.0", "@aws-cdk/aws-stepfunctions-tasks": "0.0.0", @@ -97,6 +98,7 @@ "@aws-cdk/aws-cloudformation": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-stepfunctions": "0.0.0", "@aws-cdk/aws-stepfunctions-tasks": "0.0.0", diff --git a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts index 27e3b89311703..4f5dcc5ec34d0 100644 --- a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts +++ b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts @@ -1,5 +1,6 @@ import '@aws-cdk/assert/jest'; import * as iam from '@aws-cdk/aws-iam'; +import * as logs from '@aws-cdk/aws-logs'; import * as cdk from '@aws-cdk/core'; import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from '../../lib'; @@ -488,3 +489,35 @@ test('getDataString', () => { } }); }); + +test('can specify log retention', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new AwsCustomResource(stack, 'AwsSdk', { + onCreate: { + service: 'service', + action: 'action', + physicalResourceId: PhysicalResourceId.of('id') + }, + logRetention: logs.RetentionDays.ONE_WEEK, + policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: AwsCustomResourcePolicy.ANY_RESOURCE }) + }); + + // THEN + expect(stack).toHaveResource('Custom::LogRetention', { + LogGroupName: { + 'Fn::Join': [ + '', + [ + '/aws/lambda/', + { + Ref: 'AWS679f53fac002430cb0da5b7982bd22872D164C4C' + } + ] + ] + }, + RetentionInDays: 7 + }); +}); diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 075a3e60c97ef..672d676b0d455 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -6,7 +6,8 @@ import * as colors from 'colors/safe'; import * as path from 'path'; import * as yargs from 'yargs'; -import { bootstrapEnvironment, BootstrapEnvironmentProps, SDK } from '../lib'; +import { bootstrapEnvironment, BootstrapEnvironmentProps } from '../lib'; +import { SdkProvider } from '../lib/api/aws-auth'; import { bootstrapEnvironment2 } from '../lib/api/bootstrap/bootstrap-environment2'; import { environmentsFromDescriptors, globEnvironmentsFromStacks } from '../lib/api/cxapp/environments'; import { execProgram } from '../lib/api/cxapp/exec'; @@ -111,11 +112,13 @@ async function initCommandLine() { debug('CDK toolkit version:', version.DISPLAY_VERSION); debug('Command line arguments:', argv); - const aws = new SDK({ + const aws = await SdkProvider.withAwsCliCompatibleDefaults({ profile: argv.profile, - proxyAddress: argv.proxy, - caBundlePath: argv['ca-bundle-path'], ec2creds: argv.ec2creds, + httpOptions: { + proxyAddress: argv.proxy, + caBundlePath: argv['ca-bundle-path'], + } }); const configuration = new Configuration(argv); diff --git a/packages/aws-cdk/lib/api/util/account-cache.ts b/packages/aws-cdk/lib/api/aws-auth/account-cache.ts similarity index 77% rename from packages/aws-cdk/lib/api/util/account-cache.ts rename to packages/aws-cdk/lib/api/aws-auth/account-cache.ts index 9dbc80ec8970f..4f3e7f2c1c414 100644 --- a/packages/aws-cdk/lib/api/util/account-cache.ts +++ b/packages/aws-cdk/lib/api/aws-auth/account-cache.ts @@ -2,6 +2,7 @@ import * as fs from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; import { debug } from '../../logging'; +import { Account } from './sdk-provider'; /** * Disk cache which maps access key IDs to account IDs. @@ -21,7 +22,7 @@ export class AccountAccessKeyCache { * @param filePath Path to the cache file */ constructor(filePath?: string) { - this.cacheFile = filePath || path.join(os.homedir(), '.cdk', 'cache', 'accounts.json'); + this.cacheFile = filePath || path.join(os.homedir(), '.cdk', 'cache', 'accounts_partitions.json'); } /** @@ -38,31 +39,32 @@ export class AccountAccessKeyCache { * @param accessKeyId * @param resolver */ - public async fetch(accessKeyId: string, resolver: () => Promise) { + public async fetch(accessKeyId: string, resolver: () => Promise) { // try to get account ID based on this access key ID from disk. const cached = await this.get(accessKeyId); if (cached) { - debug(`Retrieved account ID ${cached} from disk cache`); + + debug(`Retrieved account ID ${cached.accountId} from disk cache`); return cached; } // if it's not in the cache, resolve and put in cache. - const accountId = await resolver(); - if (accountId) { - await this.put(accessKeyId, accountId); + const account = await resolver(); + if (account) { + await this.put(accessKeyId, account); } - return accountId; + return account; } /** Get the account ID from an access key or undefined if not in cache */ - public async get(accessKeyId: string): Promise { + public async get(accessKeyId: string): Promise { const map = await this.loadMap(); return map[accessKeyId]; } /** Put a mapping betweenn access key and account ID */ - public async put(accessKeyId: string, accountId: string) { + public async put(accessKeyId: string, account: Account) { let map = await this.loadMap(); // nuke cache if it's too big. @@ -70,11 +72,11 @@ export class AccountAccessKeyCache { map = { }; } - map[accessKeyId] = accountId; + map[accessKeyId] = account; await this.saveMap(map); } - private async loadMap(): Promise<{ [accessKeyId: string]: string }> { + private async loadMap(): Promise<{ [accessKeyId: string]: Account }> { if (!(await fs.pathExists(this.cacheFile))) { return { }; } @@ -82,7 +84,7 @@ export class AccountAccessKeyCache { return await fs.readJson(this.cacheFile); } - private async saveMap(map: { [accessKeyId: string]: string }) { + private async saveMap(map: { [accessKeyId: string]: Account }) { if (!(await fs.pathExists(this.cacheFile))) { await fs.mkdirs(path.dirname(this.cacheFile)); } diff --git a/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts b/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts new file mode 100644 index 0000000000000..2ad921ae5ed5c --- /dev/null +++ b/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts @@ -0,0 +1,199 @@ +import * as AWS from 'aws-sdk'; +import * as child_process from 'child_process'; +import * as fs from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import * as util from 'util'; +import { debug } from '../../logging'; +import { SharedIniFile } from "./sdk_ini_file"; + +/** + * Behaviors to match AWS CLI + * + * See these links: + * + * https://docs.aws.amazon.com/cli/latest/topic/config-vars.html + * https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html + */ +export class AwsCliCompatible { + /** + * Build an AWS CLI-compatible credential chain provider + * + * This is similar to the default credential provider chain created by the SDK + * except: + * + * 1. Accepts profile argument in the constructor (the SDK must have it prepopulated + * in the environment). + * 2. Conditionally checks EC2 credentials, because checking for EC2 + * credentials on a non-EC2 machine may lead to long delays (in the best case) + * or an exception (in the worst case). + * 3. Respects $AWS_SHARED_CREDENTIALS_FILE. + * 4. Respects $AWS_DEFAULT_PROFILE in addition to $AWS_PROFILE. + */ + public static async credentialChain(profile: string | undefined, ec2creds: boolean | undefined, containerCreds: boolean | undefined) { + await forceSdkToReadConfigIfPresent(); + + profile = profile || process.env.AWS_PROFILE || process.env.AWS_DEFAULT_PROFILE || 'default'; + + const sources = [ + () => new AWS.EnvironmentCredentials('AWS'), + () => new AWS.EnvironmentCredentials('AMAZON'), + ]; + + if (await fs.pathExists(credentialsFileName())) { + sources.push(() => new AWS.SharedIniFileCredentials({ profile, filename: credentialsFileName() })); + } + + if (await fs.pathExists(configFileName())) { + sources.push(() => new AWS.SharedIniFileCredentials({ profile, filename: credentialsFileName() })); + } + + if (containerCreds ?? hasEcsCredentials()) { + sources.push(() => new AWS.ECSCredentials()); + } else if (ec2creds ?? await hasEc2Credentials()) { + // else if: don't get EC2 creds if we should have gotten ECS creds--ECS instances also + // run on EC2 boxes but the creds represent something different. Same behavior as + // upstream code. + sources.push(() => new AWS.EC2MetadataCredentials()); + } + + return new AWS.CredentialProviderChain(sources); + } + + /** + * Return the default region in a CLI-compatible way + * + * Mostly copied from node_loader.js, but with the following differences to make it + * AWS CLI compatible: + * + * 1. Takes a profile name as an argument (instead of forcing it to be taken from $AWS_PROFILE). + * This requires having made a copy of the SDK's `SharedIniFile` (the original + * does not take an argument). + * 2. $AWS_DEFAULT_PROFILE and $AWS_DEFAULT_REGION are also respected. + * + * Lambda and CodeBuild set the $AWS_REGION variable. + * + * FIXME: EC2 instances require querying the metadata service to determine the current region. + */ + public static async region(profile: string | undefined): Promise { + profile = profile || process.env.AWS_PROFILE || process.env.AWS_DEFAULT_PROFILE || 'default'; + + // Defaults inside constructor + const toCheck = [ + { filename: credentialsFileName(), profile }, + { isConfig: true, filename: configFileName(), profile }, + { isConfig: true, filename: configFileName(), profile: 'default' }, + ]; + + let region = process.env.AWS_REGION || process.env.AMAZON_REGION || + process.env.AWS_DEFAULT_REGION || process.env.AMAZON_DEFAULT_REGION; + + while (!region && toCheck.length > 0) { + const options = toCheck.shift()!; + if (await fs.pathExists(options.filename)) { + const configFile = new SharedIniFile(options); + const section = await configFile.getProfile(options.profile); + region = section?.region; + } + } + + if (!region) { + const usedProfile = !profile ? '' : ` (profile: "${profile}")`; + region = 'us-east-1'; // This is what the AWS CLI does + debug(`Unable to determine AWS region from environment or AWS configuration${usedProfile}, defaulting to '${region}'`); + } + + return region; + } +} + +/** + * Return whether it looks like we'll have ECS credentials available + */ +function hasEcsCredentials(): boolean { + return (AWS.ECSCredentials.prototype as any).isConfiguredForEcsCredentials(); +} + +/** + * Return whether we're on an EC2 instance + */ +async function hasEc2Credentials() { + debug("Determining whether we're on an EC2 instance."); + + let instance = false; + if (process.platform === 'win32') { + // https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/identify_ec2_instances.html + const result = await util.promisify(child_process.exec)('wmic path win32_computersystemproduct get uuid', { encoding: 'utf-8' }); + // output looks like + // UUID + // EC2AE145-D1DC-13B2-94ED-01234ABCDEF + const lines = result.stdout.toString().split('\n'); + instance = lines.some(x => matchesRegex(/^ec2/i, x)); + } else { + // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/identify_ec2_instances.html + const files: Array<[string, RegExp]> = [ + // This recognizes the Xen hypervisor based instances (pre-5th gen) + ['/sys/hypervisor/uuid', /^ec2/i], + + // This recognizes the new Hypervisor (5th-gen instances and higher) + // Can't use the advertised file '/sys/devices/virtual/dmi/id/product_uuid' because it requires root to read. + // Instead, sys_vendor contains something like 'Amazon EC2'. + ['/sys/devices/virtual/dmi/id/sys_vendor', /ec2/i], + ]; + for (const [file, re] of files) { + if (matchesRegex(re, readIfPossible(file))) { + instance = true; + break; + } + } + } + + debug(instance ? 'Looks like EC2 instance.' : 'Does not look like EC2 instance.'); + return instance; +} + +function homeDir() { + return process.env.HOME || process.env.USERPROFILE + || (process.env.HOMEPATH ? ((process.env.HOMEDRIVE || 'C:/') + process.env.HOMEPATH) : null) || os.homedir(); +} + +function credentialsFileName() { + return process.env.AWS_SHARED_CREDENTIALS_FILE || path.join(homeDir(), '.aws', 'credentials'); +} + +function configFileName() { + return process.env.AWS_CONFIG_FILE || path.join(homeDir(), '.aws', 'config'); +} + +/** + * Force the JS SDK to honor the ~/.aws/config file (and various settings therein) + * + * For example, ther is just *NO* way to do AssumeRole credentials as long as AWS_SDK_LOAD_CONFIG is not set, + * or read credentials from that file. + * + * The SDK crashes if the variable is set but the file does not exist, so conditionally set it. + */ +async function forceSdkToReadConfigIfPresent() { + if (await fs.pathExists(configFileName())) { + process.env.AWS_SDK_LOAD_CONFIG = '1'; + } +} + +function matchesRegex(re: RegExp, s: string | undefined) { + return s !== undefined && re.exec(s) !== null; +} + +/** + * Read a file if it exists, or return undefined + * + * Not async because it is used in the constructor + */ +function readIfPossible(filename: string): string | undefined { + try { + if (!fs.pathExistsSync(filename)) { return undefined; } + return fs.readFileSync(filename, { encoding: 'utf-8' }); + } catch (e) { + debug(e); + return undefined; + } +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/aws-auth/credential-plugins.ts b/packages/aws-cdk/lib/api/aws-auth/credential-plugins.ts new file mode 100644 index 0000000000000..8c5fa66f02285 --- /dev/null +++ b/packages/aws-cdk/lib/api/aws-auth/credential-plugins.ts @@ -0,0 +1,54 @@ +import { debug } from "../../logging"; +import { PluginHost } from "../../plugin"; +import { CredentialProviderSource, Mode } from "./credentials"; + +/** + * Cache for credential providers. + * + * Given an account and an operating mode (read or write) will return an + * appropriate credential provider for credentials for the given account. The + * credential provider will be cached so that multiple AWS clients for the same + * environment will not make multiple network calls to obtain credentials. + * + * Will use default credentials if they are for the right account; otherwise, + * all loaded credential provider plugins will be tried to obtain credentials + * for the given account. + */ +export class CredentialPlugins { + private readonly cache: {[key: string]: AWS.Credentials | undefined} = {}; + + public async fetchCredentialsFor(awsAccountId: string, mode: Mode): Promise { + const key = `${awsAccountId}-${mode}`; + if (!(key in this.cache)) { + this.cache[key] = await this.lookupCredentials(awsAccountId, mode); + } + return this.cache[key]; + } + + public get availablePluginNames(): string[] { + return PluginHost.instance.credentialProviderSources.map(s => s.name); + } + + private async lookupCredentials(awsAccountId: string, mode: Mode): Promise { + const triedSources: CredentialProviderSource[] = []; + // Otherwise, inspect the various credential sources we have + for (const source of PluginHost.instance.credentialProviderSources) { + if (!(await source.isAvailable())) { + debug('Credentials source %s is not available, ignoring it.', source.name); + continue; + } + triedSources.push(source); + if (!(await source.canProvideCredentials(awsAccountId))) { continue; } + debug(`Using ${source.name} credentials for account ${awsAccountId}`); + const providerOrCreds = await source.getProvider(awsAccountId, mode); + + // Backwards compatibility: if the plugin returns a ProviderChain, resolve that chain. + // Otherwise it must have returned credentials. + if ((providerOrCreds as any).resolvePromise) { + return await (providerOrCreds as any).resolvePromise(); + } + return providerOrCreds; + } + return undefined; + } +} diff --git a/packages/aws-cdk/lib/api/aws-auth/index.ts b/packages/aws-cdk/lib/api/aws-auth/index.ts new file mode 100644 index 0000000000000..cade9b2eada26 --- /dev/null +++ b/packages/aws-cdk/lib/api/aws-auth/index.ts @@ -0,0 +1,3 @@ +export * from './sdk'; +export * from './sdk-provider'; +export * from './credentials'; \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts b/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts new file mode 100644 index 0000000000000..b8e92a5f4d48f --- /dev/null +++ b/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts @@ -0,0 +1,356 @@ +import * as cxapi from '@aws-cdk/cx-api'; +import * as AWS from 'aws-sdk'; +import { ConfigurationOptions } from 'aws-sdk/lib/config'; +import * as fs from 'fs-extra'; +import * as https from 'https'; +import * as os from 'os'; +import * as path from 'path'; +import { debug } from '../../logging'; +import { cached } from '../../util/functions'; +import { CredentialPlugins } from '../aws-auth/credential-plugins'; +import { Mode } from "../aws-auth/credentials"; +import { AccountAccessKeyCache } from './account-cache'; +import { AwsCliCompatible } from './awscli-compatible'; +import { ISDK, SDK } from './sdk'; + +/** + * Options for the default SDK provider + */ +export interface SdkProviderOptions { + /** + * Profile to read from ~/.aws + * + * @default - No profile + */ + readonly profile?: string; + + /** + * Whether we should check for EC2 credentials + * + * @default - Autodetect + */ + readonly ec2creds?: boolean; + + /** + * Whether we should check for container credentials + * + * @default - Autodetect + */ + readonly containerCreds?: boolean; + + /** + * HTTP options for SDK + */ + readonly httpOptions?: SdkHttpOptions; +} + +/** + * Options for individual SDKs + */ +export interface SdkHttpOptions { + /** + * Proxy address to use + * + * @default No proxy + */ + readonly proxyAddress?: string; + + /** + * A path to a certificate bundle that contains a cert to be trusted. + * + * @default No certificate bundle + */ + readonly caBundlePath?: string; + + /** + * The custom user agent to use. + * + * @default - / + */ + readonly userAgent?: string; +} + +const CACHED_ACCOUNT = Symbol(); +const CACHED_DEFAULT_CREDENTIALS = Symbol(); + +/** + * Creates instances of the AWS SDK appropriate for a given account/region + * + * If an environment is given and the current credentials are NOT for the indicated + * account, will also search the set of credential plugin providers. + * + * If no environment is given, the default credentials will always be used. + */ +export class SdkProvider { + /** + * Create a new SdkProvider which gets its defaults in a way that haves like the AWS CLI does + * + * The AWS SDK for JS behaves slightly differently from the AWS CLI in a number of ways; see the + * class `AwsCliCompatible` for the details. + */ + public static async withAwsCliCompatibleDefaults(options: SdkProviderOptions = {}) { + const chain = await AwsCliCompatible.credentialChain(options.profile, options.ec2creds, options.containerCreds); + const region = await AwsCliCompatible.region(options.profile); + + return new SdkProvider(chain, region, options.httpOptions); + } + + private readonly accountCache = new AccountAccessKeyCache(); + private readonly plugins = new CredentialPlugins(); + private readonly httpOptions: ConfigurationOptions; + + public constructor( + private readonly defaultChain: AWS.CredentialProviderChain, + /** + * Default region + */ + public readonly defaultRegion: string, + httpOptions: SdkHttpOptions = {}) { + this.httpOptions = defaultHttpOptions(httpOptions); + } + + /** + * Return an SDK which can do operations in the given environment + * + * The `region` and `accountId` parameters are interpreted as in `resolveEnvironment()` (which is to + * say, `undefined` doesn't do what you expect). + */ + public async forEnvironment(accountId: string | undefined, region: string | undefined, mode: Mode): Promise { + const env = await this.resolveEnvironment(accountId, region); + const creds = await this.obtainCredentials(env.account, mode); + return new SDK(creds, env.region, this.httpOptions); + } + + /** + * Return an SDK which uses assumed role credentials + * + * The base credentials used to retrieve the assumed role credentials will be the + * current credentials (no plugin lookup will be done!). + * + * If `region` is undefined, the default value will be used. + */ + public async withAssumedRole(roleArn: string, externalId: string | undefined, region: string | undefined) { + debug(`Assuming role '${roleArn}'`); + region = region ?? this.defaultRegion; + + const creds = new AWS.ChainableTemporaryCredentials({ + params: { + RoleArn: roleArn, + ...externalId ? { ExternalId: externalId } : {}, + RoleSessionName: `aws-cdk-${os.userInfo().username}`, + }, + stsConfig: { + region, + ...this.httpOptions, + }, + masterCredentials: await this.defaultCredentials(), + }); + + return new SDK(creds, region, this.httpOptions); + } + + /** + * Resolve the environment for a stack + * + * `undefined` actually means `undefined`, and is NOT changed to default values! Only the magic values UNKNOWN_REGION + * and UNKNOWN_ACCOUNT will be replaced with looked-up values! + */ + public async resolveEnvironment(accountId: string | undefined, region: string | undefined) { + region = region !== cxapi.UNKNOWN_REGION ? region : this.defaultRegion; + accountId = accountId !== cxapi.UNKNOWN_ACCOUNT ? accountId : (await this.defaultAccount())?.accountId; + + if (!region) { + throw new Error(`AWS region must be configured either when you configure your CDK stack or through the environment`); + } + + if (!accountId) { + throw new Error(`Unable to resolve AWS account to use. It must be either configured when you define your CDK or through the environment`); + } + + const environment: cxapi.Environment = { + region, account: accountId, name: cxapi.EnvironmentUtils.format(accountId, region) + }; + + return environment; + } + + /** + * Use the default credentials to lookup our account number using STS. + * + * Uses a cache to avoid STS calls if we don't need 'em. + */ + public defaultAccount(): Promise { + return cached(this, CACHED_ACCOUNT, async () => { + try { + const creds = await this.defaultCredentials(); + + const accessKeyId = creds.accessKeyId; + if (!accessKeyId) { + throw new Error('Unable to resolve AWS credentials (setup with "aws configure")'); + } + + const account = await this.accountCache.fetch(creds.accessKeyId, async () => { + // if we don't have one, resolve from STS and store in cache. + debug('Looking up default account ID from STS'); + const result = await new AWS.STS({ ...this.httpOptions, credentials: creds, region: this.defaultRegion }).getCallerIdentity().promise(); + const accountId = result.Account; + const partition = result.Arn!.split(':')[1]; + if (!accountId) { + debug('STS didn\'t return an account ID'); + return undefined; + } + debug('Default account ID:', accountId); + return { accountId, partition }; + }); + + return account; + } catch (e) { + debug('Unable to determine the default AWS account (did you configure "aws configure"?):', e); + return undefined; + } + }); + } + + /** + * Get credentials for the given account ID in the given mode + * + * Use the current credentials if the destination account matches the current credentials' account. + * Otherwise try all credential plugins. + */ + protected async obtainCredentials(accountId: string, mode: Mode): Promise { + // First try 'current' credentials + const defaultAccountId = (await this.defaultAccount())?.accountId; + if (defaultAccountId === accountId) { + return this.defaultCredentials(); + } + + // Then try the plugins + const pluginCreds = await this.plugins.fetchCredentialsFor(accountId, mode); + if (pluginCreds) { + return pluginCreds; + } + + // No luck, format a useful error message + const error = [`Need to perform AWS calls for account ${accountId}`]; + error.push(defaultAccountId ? `but the current credentials are for ${defaultAccountId}` : `but no credentials have been configured`); + if (this.plugins.availablePluginNames.length > 0) { + error.push(`and none of these plugins found any: ${this.plugins.availablePluginNames.join(', ')}`); + } + + throw new Error(`${error.join(', ')}.`); + } + + /** + * Resolve the default chain to the first set of credentials that is available + */ + private defaultCredentials(): Promise { + return cached(this, CACHED_DEFAULT_CREDENTIALS, () => { + debug('Resolving default credentials'); + return this.defaultChain.resolvePromise(); + }); + } +} + +/** + * An AWS account + * + * An AWS account always exists in only one partition. Usually we don't care about + * the partition, but when we need to form ARNs we do. + */ +export interface Account { + /** + * The account number + */ + readonly accountId: string; + + /** + * The partition ('aws' or 'aws-cn' or otherwise) + */ + readonly partition: string; +} + +/** + * Get HTTP options for the SDK + * + * Read from user input or environment variables. + */ +function defaultHttpOptions(options: SdkHttpOptions) { + const config: ConfigurationOptions = {}; + config.httpOptions = {}; + + let userAgent = options.userAgent; + if (userAgent == null) { + // Find the package.json from the main toolkit + const pkg = JSON.parse(readIfPossible(path.join(__dirname, '..', '..', '..', 'package.json')) ?? '{}'); + userAgent = `${pkg.name}/${pkg.version}`; + } + config.customUserAgent = userAgent; + + const proxyAddress = options.proxyAddress || httpsProxyFromEnvironment(); + const caBundlePath = options.caBundlePath || caBundlePathFromEnvironment(); + + if (proxyAddress && caBundlePath) { + throw new Error(`At the moment, cannot specify Proxy (${proxyAddress}) and CA Bundle (${caBundlePath}) at the same time. See https://github.com/aws/aws-cdk/issues/5804`); + // Maybe it's possible after all, but I've been staring at + // https://github.com/TooTallNate/node-proxy-agent/blob/master/index.js#L79 + // a while now trying to figure out what to pass in so that the underlying Agent + // object will get the 'ca' argument. It's not trivial and I don't want to risk it. + } + + if (proxyAddress) { // Ignore empty string on purpose + // https://aws.amazon.com/blogs/developer/using-the-aws-sdk-for-javascript-from-behind-a-proxy/ + debug('Using proxy server: %s', proxyAddress); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const ProxyAgent: any = require('proxy-agent'); + config.httpOptions.agent = new ProxyAgent(proxyAddress); + } + if (caBundlePath) { + debug('Using CA bundle path: %s', caBundlePath); + config.httpOptions.agent = new https.Agent({ + ca: readIfPossible(caBundlePath) + }); + } + + return config; +} + +/** + * Find and return the configured HTTPS proxy address + */ +function httpsProxyFromEnvironment(): string | undefined { + if (process.env.https_proxy) { + return process.env.https_proxy; + } + if (process.env.HTTPS_PROXY) { + return process.env.HTTPS_PROXY; + } + return undefined; +} + +/** + * Find and return a CA certificate bundle path to be passed into the SDK. + */ +function caBundlePathFromEnvironment(): string | undefined { + if (process.env.aws_ca_bundle) { + return process.env.aws_ca_bundle; + } + if (process.env.AWS_CA_BUNDLE) { + return process.env.AWS_CA_BUNDLE; + } + return undefined; +} + +/** + * Read a file if it exists, or return undefined + * + * Not async because it is used in the constructor + */ +function readIfPossible(filename: string): string | undefined { + try { + if (!fs.pathExistsSync(filename)) { return undefined; } + return fs.readFileSync(filename, { encoding: 'utf-8' }); + } catch (e) { + debug(e); + return undefined; + } +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts new file mode 100644 index 0000000000000..cd178d735acec --- /dev/null +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -0,0 +1,69 @@ +import * as AWS from 'aws-sdk'; +import { ConfigurationOptions } from 'aws-sdk/lib/config'; + +/** @experimental */ +export interface ISDK { + cloudFormation(): AWS.CloudFormation; + + ec2(): AWS.EC2; + + ssm(): AWS.SSM; + + s3(): AWS.S3; + + route53(): AWS.Route53; + + ecr(): AWS.ECR; +} + +/** + * Base functionality of SDK without credential fetching + */ +export class SDK implements ISDK { + private readonly config: ConfigurationOptions; + + /** + * Default retry options for SDK clients + * + * Biggest bottleneck is CloudFormation, with a 1tps call rate. We want to be + * a little more tenacious than the defaults, and with a little more breathing + * room between calls (defaults are {retries=3, base=100}). + * + * I've left this running in a tight loop for an hour and the throttle errors + * haven't escaped the retry mechanism. + */ + private readonly retryOptions = { maxRetries: 6, retryDelayOptions: { base: 300 }}; + + constructor(credentials: AWS.Credentials, region: string, httpOptions: ConfigurationOptions = {}) { + this.config = { + ...httpOptions, + ...this.retryOptions, + credentials, + region, + }; + } + + public cloudFormation(): AWS.CloudFormation { + return new AWS.CloudFormation(this.config); + } + + public ec2(): AWS.EC2 { + return new AWS.EC2(this.config); + } + + public ssm(): AWS.SSM { + return new AWS.SSM(this.config); + } + + public s3(): AWS.S3 { + return new AWS.S3(this.config); + } + + public route53(): AWS.Route53 { + return new AWS.Route53(this.config); + } + + public ecr(): AWS.ECR { + return new AWS.ECR(this.config); + } +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk_ini_file.ts b/packages/aws-cdk/lib/api/aws-auth/sdk_ini_file.ts new file mode 100644 index 0000000000000..40845d00b8a15 --- /dev/null +++ b/packages/aws-cdk/lib/api/aws-auth/sdk_ini_file.ts @@ -0,0 +1,59 @@ +/** + * A reimplementation of JS AWS SDK's SharedIniFile class + * + * We need that class to parse the ~/.aws/config file to determine the correct + * region at runtime, but unfortunately it is private upstream. + */ + +import * as AWS from 'aws-sdk'; +import * as fs from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; + +export interface SharedIniFileOptions { + isConfig?: boolean; + filename?: string; +} + +export class SharedIniFile { + private readonly isConfig: boolean; + private readonly filename: string; + private parsedContents?: { [key: string]: { [key: string]: string } }; + + constructor(options?: SharedIniFileOptions) { + options = options || {}; + this.isConfig = options.isConfig === true; + this.filename = options.filename || this.getDefaultFilepath(); + } + + public async getProfile(profile: string) { + await this.ensureFileLoaded(); + + const profileIndex = profile !== (AWS as any).util.defaultProfile && this.isConfig ? + 'profile ' + profile : profile; + + return this.parsedContents![profileIndex]; + } + + private getDefaultFilepath(): string { + return path.join( + os.homedir(), + '.aws', + this.isConfig ? 'config' : 'credentials' + ); + } + + private async ensureFileLoaded() { + if (this.parsedContents) { + return; + } + + if (!await fs.pathExists(this.filename)) { + this.parsedContents = {}; + return; + } + + const contents: string = (await fs.readFile(this.filename)).toString(); + this.parsedContents = (AWS as any).util.ini.parse(contents); + } +} diff --git a/packages/aws-cdk/lib/api/bootstrap-environment.ts b/packages/aws-cdk/lib/api/bootstrap-environment.ts index e1272e8e6ddc7..72c25eb140a50 100644 --- a/packages/aws-cdk/lib/api/bootstrap-environment.ts +++ b/packages/aws-cdk/lib/api/bootstrap-environment.ts @@ -2,15 +2,17 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; +import { SdkProvider } from './aws-auth'; import {Tag} from "./cxapp/stacks"; import { deployStack, DeployStackResult } from './deploy-stack'; -import { ISDK } from './util/sdk'; // tslint:disable:max-line-length /** @experimental */ export const BUCKET_NAME_OUTPUT = 'BucketName'; /** @experimental */ +export const REPOSITORY_NAME_OUTPUT = 'RepositoryName'; +/** @experimental */ export const BUCKET_DOMAIN_NAME_OUTPUT = 'BucketDomainName'; export interface BootstrapEnvironmentProps { @@ -57,7 +59,7 @@ export interface BootstrapEnvironmentProps { } /** @experimental */ -export async function bootstrapEnvironment(environment: cxapi.Environment, aws: ISDK, toolkitStackName: string, roleArn: string | undefined, props: BootstrapEnvironmentProps = {}): Promise { +export async function bootstrapEnvironment(environment: cxapi.Environment, aws: SdkProvider, toolkitStackName: string, roleArn: string | undefined, props: BootstrapEnvironmentProps = {}): Promise { if (props.trustedAccounts?.length) { throw new Error('--trust can only be passed for the new bootstrap experience!'); } diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment2.ts b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment2.ts index 596450cd16939..8d4b9b5159716 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment2.ts +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment2.ts @@ -2,9 +2,10 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; -import { BootstrapEnvironmentProps, deployStack, DeployStackResult, ISDK } from '..'; +import { BootstrapEnvironmentProps, deployStack, DeployStackResult } from '..'; +import { SdkProvider } from '../aws-auth'; -export async function bootstrapEnvironment2(environment: cxapi.Environment, sdk: ISDK, +export async function bootstrapEnvironment2(environment: cxapi.Environment, sdk: SdkProvider, toolkitStackName: string, roleArn: string | undefined, props: BootstrapEnvironmentProps = {}): Promise { if (props.trustedAccounts?.length && !props.cloudFormationExecutionPolicies?.length) { diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.json b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.json index 5e523b98a90ca..2c074641ef119 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.json +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.json @@ -273,13 +273,20 @@ }, { "Action": [ - "ecr:PutImage", "ecr:InitiateLayerUpload", - "ecr:UploadLayerPart", "ecr:CompleteLayerUpload" + "ecr:PutImage", "ecr:InitiateLayerUpload", + "ecr:UploadLayerPart", "ecr:CompleteLayerUpload", + "ecr:BatchCheckLayerAvailability", + "ecr:DescribeRepositories", "ecr:DescribeImages" ], "Resource": { "Fn::Sub": "${ContainerAssetsRepository.Arn}" }, "Effect": "Allow" + }, + { + "Action": ["ecr:GetAuthorizationToken"], + "Resource": "*", + "Effect": "Allow" } ], "Version": "2012-10-17" diff --git a/packages/aws-cdk/lib/api/cxapp/environments.ts b/packages/aws-cdk/lib/api/cxapp/environments.ts index a4c2aae0376b2..c34c066e010ce 100644 --- a/packages/aws-cdk/lib/api/cxapp/environments.ts +++ b/packages/aws-cdk/lib/api/cxapp/environments.ts @@ -1,9 +1,9 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as minimatch from 'minimatch'; -import { ISDK } from '../util/sdk'; +import { SdkProvider } from '../aws-auth'; import { AppStacks } from './stacks'; -export async function globEnvironmentsFromStacks(appStacks: AppStacks, environmentGlobs: string[], sdk: ISDK): Promise { +export async function globEnvironmentsFromStacks(appStacks: AppStacks, environmentGlobs: string[], sdk: SdkProvider): Promise { if (environmentGlobs.length === 0) { environmentGlobs = [ '**' ]; // default to ALL } @@ -12,7 +12,7 @@ export async function globEnvironmentsFromStacks(appStacks: AppStacks, environme const availableEnvironments = new Array(); for (const stack of stacks) { - const actual = await parseEnvironment(sdk, stack.environment); + const actual = await sdk.resolveEnvironment(stack.environment.account, stack.environment.region); availableEnvironments.push(actual); } @@ -26,20 +26,6 @@ export async function globEnvironmentsFromStacks(appStacks: AppStacks, environme return environments; } -async function parseEnvironment(sdk: ISDK, env: cxapi.Environment): Promise { - const account = env.account === cxapi.UNKNOWN_ACCOUNT ? await sdk.defaultAccount() : env.account; - const region = env.region === cxapi.UNKNOWN_REGION ? await sdk.defaultRegion() : env.region; - - if (!account || !region) { - throw new Error(`Unable to determine default account and/or region`); - } - - return { - account, region, - name: cxapi.EnvironmentUtils.format(account, region) - }; -} - /** * Given a set of "/" strings, construct environments for them */ diff --git a/packages/aws-cdk/lib/api/cxapp/exec.ts b/packages/aws-cdk/lib/api/cxapp/exec.ts index 8b9432140aba9..4de8c73509c01 100644 --- a/packages/aws-cdk/lib/api/cxapp/exec.ts +++ b/packages/aws-cdk/lib/api/cxapp/exec.ts @@ -5,10 +5,10 @@ import * as path from 'path'; import { debug } from '../../logging'; import { Configuration, PROJECT_CONFIG, USER_DEFAULTS } from '../../settings'; import { versionNumber } from '../../version'; -import { ISDK } from '../util/sdk'; +import { SdkProvider } from '../aws-auth'; /** Invokes the cloud executable and returns JSON output */ -export async function execProgram(aws: ISDK, config: Configuration): Promise { +export async function execProgram(aws: SdkProvider, config: Configuration): Promise { const env: { [key: string]: string } = { }; const context = config.context.all; @@ -131,12 +131,15 @@ export async function execProgram(aws: ISDK, config: Configuration): Promise Promise; +type Synthesizer = (aws: SdkProvider, config: Configuration) => Promise; export interface AppStacksProps { /** @@ -43,7 +43,7 @@ export interface AppStacksProps { /** * AWS object (used by synthesizer and contextprovider) */ - aws: ISDK; + aws: SdkProvider; /** * Callback invoked to synthesize the actual stacks diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 0a51851080483..51af071a5948a 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -3,15 +3,18 @@ import * as aws from 'aws-sdk'; import * as colors from 'colors/safe'; import * as uuid from 'uuid'; import { Tag } from "../api/cxapp/stacks"; -import { prepareAssets } from '../assets'; +import { addMetadataAssetsToManifest } from '../assets'; import { debug, error, print } from '../logging'; import { deserializeStructure, toYAML } from '../serialize'; +import { AssetManifestBuilder } from '../util/asset-manifest-builder'; +import { publishAssets } from '../util/asset-publishing'; +import { contentHash } from '../util/content-hash'; +import { SdkProvider } from './aws-auth'; import { Mode } from './aws-auth/credentials'; import { ToolkitInfo } from './toolkit-info'; import { changeSetHasNoChanges, describeStack, stackExists, stackFailedCreating, waitForChangeSet, waitForStack } from './util/cloudformation'; import { StackActivityMonitor } from './util/cloudformation/stack-activity-monitor'; import { StackStatus } from './util/cloudformation/stack-status'; -import { ISDK } from './util/sdk'; type TemplateBodyParameter = { TemplateBody?: string @@ -29,7 +32,7 @@ export interface DeployStackResult { /** @experimental */ export interface DeployStackOptions { stack: cxapi.CloudFormationStackArtifact; - sdk: ISDK; + sdk: SdkProvider; toolkitInfo?: ToolkitInfo; roleArn?: string; notificationArns?: string[]; @@ -66,34 +69,40 @@ const LARGE_TEMPLATE_SIZE_KB = 50; /** @experimental */ export async function deployStack(options: DeployStackOptions): Promise { - if (!options.stack.environment) { - throw new Error(`The stack ${options.stack.displayName} does not have an environment`); + const stack = options.stack; + + if (!stack.environment) { + throw new Error(`The stack ${stack.displayName} does not have an environment`); } - const cfn = await options.sdk.cloudFormation(options.stack.environment.account, options.stack.environment.region, Mode.ForWriting); - const deployName = options.deployName || options.stack.stackName; + // Translate symbolic/unknown environment references to concrete environment references + const stackEnv = await options.sdk.resolveEnvironment(stack.environment.account, stack.environment.region); + + const cfn = (await options.sdk.forEnvironment(stackEnv.account, stackEnv.region, Mode.ForWriting)).cloudFormation(); + const deployName = options.deployName || stack.stackName; if (!options.force) { + // bail out if the current template is exactly the same as the one we are about to deploy + // in cdk-land, this means nothing changed because assets (and therefore nested stacks) are immutable. debug(`checking if we can skip this stack based on the currently deployed template and tags (use --force to override)`); const deployed = await getDeployedStack(cfn, deployName); const tagsIdentical = compareTags(deployed?.tags ?? [], options.tags ?? []); - if (deployed && JSON.stringify(options.stack.template) === JSON.stringify(deployed.template) && tagsIdentical) { + if (deployed && JSON.stringify(stack.template) === JSON.stringify(deployed.template) && tagsIdentical) { debug(`${deployName}: no change in template and tags, skipping (use --force to override)`); return { noOp: true, outputs: await getStackOutputs(cfn, deployName), stackArn: deployed.stackId, - stackArtifact: options.stack + stackArtifact: stack }; } else { debug(`${deployName}: template changed, deploying...`); } } - // bail out if the current template is exactly the same as the one we are about to deploy - // in cdk-land, this means nothing changed because assets (and therefore nested stacks) are immutable. + const assets = new AssetManifestBuilder(); - const params = await prepareAssets(options.stack, options.toolkitInfo, options.reuseAssets); + const params = await addMetadataAssetsToManifest(stack, assets, options.toolkitInfo, options.reuseAssets); // add passed CloudFormation parameters for (const [paramName, paramValue] of Object.entries((options.parameters || {}))) { @@ -107,7 +116,7 @@ export async function deployStack(options: DeployStackOptions): Promise { +async function makeBodyParameter( + stack: cxapi.CloudFormationStackArtifact, + assetManifest: AssetManifestBuilder, + toolkitInfo?: ToolkitInfo): Promise { const templateJson = toYAML(stack.template); - if (toolkitInfo) { - const s3KeyPrefix = `cdk/${stack.id}/`; - const s3KeySuffix = '.yml'; - const { key } = await toolkitInfo.uploadIfChanged(templateJson, { - s3KeyPrefix, s3KeySuffix, contentType: 'application/x-yaml' - }); - const templateURL = `${toolkitInfo.bucketUrl}/${key}`; - debug('Stored template in S3 at:', templateURL); - return { TemplateURL: templateURL }; - } else if (templateJson.length > LARGE_TEMPLATE_SIZE_KB * 1024) { + + if (templateJson.length <= LARGE_TEMPLATE_SIZE_KB * 1024) { + return { TemplateBody: templateJson }; + } + + if (!toolkitInfo) { error( `The template for stack "${stack.displayName}" is ${Math.round(templateJson.length / 1024)}KiB. ` + `Templates larger than ${LARGE_TEMPLATE_SIZE_KB}KiB must be uploaded to S3.\n` + @@ -204,15 +218,27 @@ async function makeBodyParameter(stack: cxapi.CloudFormationStackArtifact, toolk colors.blue(`\t$ cdk bootstrap ${stack.environment!.name}\n`)); throw new Error(`Template too large to deploy ("cdk bootstrap" is required)`); - } else { - return { TemplateBody: templateJson }; } + + const templateHash = contentHash(templateJson); + const key = `cdk/${stack.id}/${templateHash}.yml`; + const templateURL = `${toolkitInfo.bucketUrl}/${key}`; + + assetManifest.addFileAsset(templateHash, { + path: stack.templateFile, + }, { + bucketName: toolkitInfo.bucketName, + objectKey: key, + }); + + debug('Storing template in S3 at:', templateURL); + return { TemplateURL: templateURL }; } /** @experimental */ export interface DestroyStackOptions { stack: cxapi.CloudFormationStackArtifact; - sdk: ISDK; + sdk: SdkProvider; roleArn?: string; deployName?: string; quiet?: boolean; @@ -225,7 +251,8 @@ export async function destroyStack(options: DestroyStackOptions) { } const deployName = options.deployName || options.stack.stackName; - const cfn = await options.sdk.cloudFormation(options.stack.environment.account, options.stack.environment.region, Mode.ForWriting); + const { account, region } = options.stack.environment; + const cfn = (await options.sdk.forEnvironment(account, region, Mode.ForWriting)).cloudFormation(); if (!await stackExists(cfn, deployName)) { return; } @@ -305,4 +332,4 @@ function compareTags(a: Tag[], b: Tag[]): boolean { } return true; -} +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/deployment-target.ts b/packages/aws-cdk/lib/api/deployment-target.ts index bf72c7f40b285..46c74ab61c3bc 100644 --- a/packages/aws-cdk/lib/api/deployment-target.ts +++ b/packages/aws-cdk/lib/api/deployment-target.ts @@ -1,10 +1,9 @@ import { CloudFormationStackArtifact } from '@aws-cdk/cx-api'; import { Tag } from "../api/cxapp/stacks"; import { debug } from '../logging'; -import { Mode } from './aws-auth/credentials'; +import { Mode, SdkProvider } from './aws-auth'; import { deployStack, DeployStackResult, readCurrentTemplate } from './deploy-stack'; import { loadToolkitInfo } from './toolkit-info'; -import { ISDK } from './util/sdk'; export const DEFAULT_TOOLKIT_STACK_NAME = 'CDKToolkit'; @@ -45,14 +44,14 @@ export interface DeployStackOptions { } export interface ProvisionerProps { - aws: ISDK; + aws: SdkProvider; } /** * Default provisioner (applies to CloudFormation). */ export class CloudFormationDeploymentTarget implements IDeploymentTarget { - private readonly aws: ISDK; + private readonly aws: SdkProvider; constructor(props: ProvisionerProps) { this.aws = props.aws; @@ -60,7 +59,7 @@ export class CloudFormationDeploymentTarget implements IDeploymentTarget { public async readCurrentTemplate(stack: CloudFormationStackArtifact): Promise