diff --git a/.mergify.yml b/.mergify.yml index 3e03c9a7533cf..8e19f26cd586a 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -6,7 +6,7 @@ pull_request_rules: label: add: [ contribution/core ] conditions: - - author~=^(eladb|RomainMuller|garnaat|nija-at|shivlaks|skinny85|rix0rrr|NGL321|Jerry-AWS|SomayaB|MrArnoldPalmer|NetaNir|iliapolo)$ + - author~=^(eladb|RomainMuller|garnaat|nija-at|shivlaks|skinny85|rix0rrr|NGL321|Jerry-AWS|SomayaB|MrArnoldPalmer|NetaNir|iliapolo|njlynch)$ - -label~="contribution/core" - name: automatic merge actions: @@ -66,20 +66,7 @@ pull_request_rules: conditions: - author!=dependabot[bot] - author!=dependabot-preview[bot] - # List out all the people whose work is okay to provisionally approve - - author!=eladb - - author!=RomainMuller - - author!=garnaat - - author!=nija-at - - author!=shivlaks - - author!=skinny85 - - author!=rix0rrr - - author!=NGL321 - - author!=Jerry-AWS - - author!=SomayaB - - author!=MrArnoldPalmer - - author!=NetaNir - - author!=iliapolo + - label!=contribution/core - base=master - -merged - -closed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 810267a9ab428..edaef65689c87 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -184,6 +184,17 @@ 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. +For Gitpod users only! The best way to supply CDK with your AWS credentials is to add them as +[persisting environment variables](https://www.gitpod.io/docs/environment-variables). +Adding them works as follows via terminal: + +```shell +eval $(gp env -e AWS_ACCESS_KEY_ID=XXXXXXXXX) +eval $(gp env -e AWS_SECRET_ACCESS_KEY=YYYYYYY) +eval $(gp env -e AWS_DEFAULT_REGION=ZZZZZZZZ) +eval $(gp env -e) +``` + 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. diff --git a/package.json b/package.json index 199c117aa6641..e1387a4881b48 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,12 @@ "devDependencies": { "conventional-changelog-cli": "^2.0.34", "fs-extra": "^9.0.1", + "graceful-fs": "^4.2.4", "jsii-diff": "^1.6.0", "jsii-pacmak": "^1.6.0", "jsii-rosetta": "^1.6.0", - "lerna": "^3.22.0", + "lerna": "^3.22.1", "standard-version": "^8.0.0", - "graceful-fs": "^4.2.4", "typescript": "~3.8.3" }, "resolutions-comment": "should be removed or reviewed when nodeunit dependency is dropped or adjusted", diff --git a/packages/@aws-cdk/app-delivery/package.json b/packages/@aws-cdk/app-delivery/package.json index db77409db42d4..a844fe67cfd03 100644 --- a/packages/@aws-cdk/app-delivery/package.json +++ b/packages/@aws-cdk/app-delivery/package.json @@ -57,7 +57,7 @@ "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "fast-check": "^1.24.2", + "fast-check": "^1.25.0", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk/assert/lib/assertions/have-resource.ts b/packages/@aws-cdk/assert/lib/assertions/have-resource.ts index 3676f06352068..c44a4b176e6fd 100644 --- a/packages/@aws-cdk/assert/lib/assertions/have-resource.ts +++ b/packages/@aws-cdk/assert/lib/assertions/have-resource.ts @@ -313,7 +313,7 @@ export function arrayWith(...elements: any[]): PropertyMatcher { const ret = (value: any, inspection: InspectionFailure): boolean => { if (!Array.isArray(value)) { - return failMatcher(inspection, `Expect an object but got '${typeof value}'`); + return failMatcher(inspection, `Expect an array but got '${typeof value}'`); } for (const element of elements) { @@ -412,4 +412,4 @@ function isCallable(x: any): x is ((...args: any[]) => any) { function isObject(x: any): x is object { // Because `typeof null === 'object'`. return x && typeof x === 'object'; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-amplify/README.md b/packages/@aws-cdk/aws-amplify/README.md index f6e9b17e71627..9a6ec9b6e48f0 100644 --- a/packages/@aws-cdk/aws-amplify/README.md +++ b/packages/@aws-cdk/aws-amplify/README.md @@ -52,6 +52,17 @@ const amplifyApp = new amplify.App(this, 'MyApp', { }); ``` +To connect your `App` to GitLab, use the `GitLabSourceCodeProvider`: +```ts +const amplifyApp = new amplify.App(this, 'MyApp', { + sourceCodeProvider: new amplify.GitLabSourceCodeProvider({ + owner: '', + repository: '', + oauthToken: cdk.SecretValue.secretsManager('my-gitlab-token') + }) +}); +``` + To connect your `App` to CodeCommit, use the `CodeCommitSourceCodeProvider`: ```ts const repository = new codecommit.Repository(this, 'Repo', { diff --git a/packages/@aws-cdk/aws-amplify/lib/app.ts b/packages/@aws-cdk/aws-amplify/lib/app.ts index 483bbbb58d6c7..640c9684625df 100644 --- a/packages/@aws-cdk/aws-amplify/lib/app.ts +++ b/packages/@aws-cdk/aws-amplify/lib/app.ts @@ -372,6 +372,11 @@ export enum RedirectStatus { * Not found (404) */ NOT_FOUND = '404', + + /** + * Not found rewrite (404) + */ + NOT_FOUND_REWRITE = '404-200', } /** diff --git a/packages/@aws-cdk/aws-amplify/lib/source-code-providers.ts b/packages/@aws-cdk/aws-amplify/lib/source-code-providers.ts index 1b280d49e6170..8736c76ff7649 100644 --- a/packages/@aws-cdk/aws-amplify/lib/source-code-providers.ts +++ b/packages/@aws-cdk/aws-amplify/lib/source-code-providers.ts @@ -36,6 +36,40 @@ export class GitHubSourceCodeProvider implements ISourceCodeProvider { } } +/** + * Properties for a GitLab source code provider + */ +export interface GitLabSourceCodeProviderProps { + /** + * The user or organization owning the repository + */ + readonly owner: string; + + /** + * The name of the repository + */ + readonly repository: string; + + /** + * A personal access token with the `repo` scope + */ + readonly oauthToken: SecretValue; +} + +/** + * GitLab source code provider + */ +export class GitLabSourceCodeProvider implements ISourceCodeProvider { + constructor(private readonly props: GitLabSourceCodeProviderProps) { } + + public bind(_app: App): SourceCodeProviderConfig { + return { + repository: `https://gitlab.com/${this.props.owner}/${this.props.repository}`, + oauthToken: this.props.oauthToken, + }; + } +} + /** * Properties for a CodeCommit source code provider */ diff --git a/packages/@aws-cdk/aws-amplify/test/app.test.ts b/packages/@aws-cdk/aws-amplify/test/app.test.ts index b5c9a3b3a7942..5af765cae6d75 100644 --- a/packages/@aws-cdk/aws-amplify/test/app.test.ts +++ b/packages/@aws-cdk/aws-amplify/test/app.test.ts @@ -61,6 +61,58 @@ test('create an app connected to a GitHub repository', () => { }); }); +test('create an app connected to a GitLab repository', () => { + // WHEN + new amplify.App(stack, 'App', { + sourceCodeProvider: new amplify.GitLabSourceCodeProvider({ + owner: 'aws', + repository: 'aws-cdk', + oauthToken: SecretValue.plainText('secret'), + }), + buildSpec: codebuild.BuildSpec.fromObject({ + version: '1.0', + frontend: { + phases: { + build: { + commands: [ + 'npm run build', + ], + }, + }, + }, + }), + }); + + // THEN + expect(stack).toHaveResource('AWS::Amplify::App', { + Name: 'App', + BuildSpec: '{\n \"version\": \"1.0\",\n \"frontend\": {\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"npm run build\"\n ]\n }\n }\n }\n}', + IAMServiceRole: { + 'Fn::GetAtt': [ + 'AppRole1AF9B530', + 'Arn', + ], + }, + OauthToken: 'secret', + Repository: 'https://gitlab.com/aws/aws-cdk', + }); + + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'amplify.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); +}); + test('create an app connected to a CodeCommit repository', () => { // WHEN new amplify.App(stack, 'App', { diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 3be9cea704d17..6045d52aa95d3 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -19,6 +19,7 @@ running on AWS Lambda, or any web application. ## Table of Contents - [Defining APIs](#defining-apis) + - [Breaking up Methods and Resources across Stacks](#breaking-up-methods-and-resources-across-stacks) - [AWS Lambda-backed APIs](#aws-lambda-backed-apis) - [Integration Targets](#integration-targets) - [Working with models](#working-with-models) @@ -99,6 +100,18 @@ item.addMethod('GET'); // GET /items/{item} item.addMethod('DELETE', new apigateway.HttpIntegration('http://amazon.com')); ``` +### Breaking up Methods and Resources across Stacks + +It is fairly common for REST APIs with a large number of Resources and Methods to hit the [CloudFormation +limit](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html) of 200 resources per +stack. + +To help with this, Resources and Methods for the same REST API can be re-organized across multiple stacks. A common +way to do this is to have a stack per Resource or groups of Resources, but this is not the only possible way. +The following example uses sets up two Resources '/pets' and '/books' in separate stacks using nested stacks: + +[Resources grouped into nested stacks](test/integ.restapi-import.lit.ts) + ## Integration Targets Methods are associated with backend integrations, which are invoked when this @@ -956,8 +969,20 @@ The following code creates a REST API using an external OpenAPI definition JSON const api = new apigateway.SpecRestApi(this, 'books-api', { apiDefinition: apigateway.ApiDefinition.fromAsset('path-to-file.json') }); + +const booksResource = api.root.addResource('books') +booksResource.addMethod('GET', ...); ``` +It is possible to use the `addResource()` API to define additional API Gateway Resources. + +**Note:** Deployment will fail if a Resource of the same name is already defined in the Open API specification. + +**Note:** Any default properties configured, such as `defaultIntegration`, `defaultMethodOptions`, etc. will only be +applied to Resources and Methods defined in the CDK, and not the ones defined in the spec. Use the [API Gateway +extensions to OpenAPI](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions.html) +to configure these. + There are a number of limitations in using OpenAPI definitions in API Gateway. Read the [Amazon API Gateway important notes for REST APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html#api-gateway-known-issues-rest-apis) for more details. @@ -965,8 +990,6 @@ for more details. **Note:** When starting off with an OpenAPI definition using `SpecRestApi`, it is not possible to configure some properties that can be configured directly in the OpenAPI specification file. This is to prevent people duplication of these properties and potential confusion. -Further, it is currently also not possible to configure Methods and Resources in addition to the ones in the -specification file. ## APIGateway v2 diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizer.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizer.ts index a51f30e14514c..ea414a1c43584 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizer.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizer.ts @@ -1,6 +1,6 @@ import { Construct, Resource, ResourceProps } from '@aws-cdk/core'; import { AuthorizationType } from './method'; -import { RestApi } from './restapi'; +import { IRestApi } from './restapi'; const AUTHORIZER_SYMBOL = Symbol.for('@aws-cdk/aws-apigateway.Authorizer'); @@ -28,7 +28,7 @@ export abstract class Authorizer extends Resource implements IAuthorizer { * Called when the authorizer is used from a specific REST API. * @internal */ - public abstract _attachToApi(restApi: RestApi): void; + public abstract _attachToApi(restApi: IRestApi): void; } /** diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts index 9215c28de1e61..f79d675af1e7f 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts @@ -3,7 +3,7 @@ import * as lambda from '@aws-cdk/aws-lambda'; import { Construct, Duration, Lazy, Stack } from '@aws-cdk/core'; import { CfnAuthorizer } from '../apigateway.generated'; import { Authorizer, IAuthorizer } from '../authorizer'; -import { RestApi } from '../restapi'; +import { IRestApi } from '../restapi'; /** * Base properties for all lambda authorizers @@ -83,7 +83,7 @@ abstract class LambdaAuthorizer extends Authorizer implements IAuthorizer { * Attaches this authorizer to a specific REST API. * @internal */ - public _attachToApi(restApi: RestApi) { + public _attachToApi(restApi: IRestApi) { if (this.restApiId && this.restApiId !== restApi.restApiId) { throw new Error('Cannot attach authorizer to two different rest APIs'); } diff --git a/packages/@aws-cdk/aws-apigateway/lib/method.ts b/packages/@aws-cdk/aws-apigateway/lib/method.ts index 58c504ab4aa8a..a7af625a4d121 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/method.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -7,7 +7,7 @@ import { MethodResponse } from './methodresponse'; import { IModel } from './model'; import { IRequestValidator, RequestValidatorOptions } from './requestvalidator'; import { IResource } from './resource'; -import { RestApi } from './restapi'; +import { IRestApi, RestApi, RestApiBase } from './restapi'; import { validateHttpMethod } from './util'; export interface MethodOptions { @@ -159,13 +159,16 @@ export class Method extends Resource { public readonly httpMethod: string; public readonly resource: IResource; - public readonly restApi: RestApi; + /** + * The API Gateway RestApi associated with this method. + */ + public readonly api: IRestApi; constructor(scope: Construct, id: string, props: MethodProps) { super(scope, id); this.resource = props.resource; - this.restApi = props.resource.restApi; + this.api = props.resource.api; this.httpMethod = props.httpMethod.toUpperCase(); validateHttpMethod(this.httpMethod); @@ -186,12 +189,12 @@ export class Method extends Resource { } if (Authorizer.isAuthorizer(authorizer)) { - authorizer._attachToApi(this.restApi); + authorizer._attachToApi(this.api); } const methodProps: CfnMethodProps = { resourceId: props.resource.resourceId, - restApiId: this.restApi.restApiId, + restApiId: this.api.restApiId, httpMethod: this.httpMethod, operationName: options.operationName || defaultMethodOptions.operationName, apiKeyRequired: options.apiKeyRequired || defaultMethodOptions.apiKeyRequired, @@ -209,15 +212,25 @@ export class Method extends Resource { this.methodId = resource.ref; - props.resource.restApi._attachMethod(this); + if (RestApiBase._isRestApiBase(props.resource.api)) { + props.resource.api._attachMethod(this); + } - const deployment = props.resource.restApi.latestDeployment; + const deployment = props.resource.api.latestDeployment; if (deployment) { deployment.node.addDependency(resource); deployment.addToLogicalId({ method: methodProps }); } } + /** + * The RestApi associated with this Method + * @deprecated - Throws an error if this Resource is not associated with an instance of `RestApi`. Use `api` instead. + */ + public get restApi(): RestApi { + return this.resource.restApi; + } + /** * Returns an execute-api ARN for this method: * diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index 2d916780bf3e2..102a17a5cdc27 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -4,7 +4,7 @@ import { Cors, CorsOptions } from './cors'; import { Integration } from './integration'; import { MockIntegration } from './integrations'; import { Method, MethodOptions } from './method'; -import { RestApi } from './restapi'; +import { IRestApi, RestApi } from './restapi'; export interface IResource extends IResourceBase { /** @@ -12,6 +12,13 @@ export interface IResource extends IResourceBase { */ readonly parentResource?: IResource; + /** + * The rest API that this resource is part of. + * + * @deprecated - Throws an error if this Resource is not associated with an instance of `RestApi`. Use `api` instead. + */ + readonly restApi: RestApi; + /** * The rest API that this resource is part of. * @@ -20,7 +27,7 @@ export interface IResource extends IResourceBase { * hash to determine the ID of the deployment. This allows us to automatically update * the deployment when the model of the REST API changes. */ - readonly restApi: RestApi; + readonly api: IRestApi; /** * The ID of the resource. @@ -154,7 +161,11 @@ export interface ResourceProps extends ResourceOptions { export abstract class ResourceBase extends ResourceConstruct implements IResource { public abstract readonly parentResource?: IResource; + /** + * @deprecated - Throws an error if this Resource is not associated with an instance of `RestApi`. Use `api` instead. + */ public abstract readonly restApi: RestApi; + public abstract readonly api: IRestApi; public abstract readonly resourceId: string; public abstract readonly path: string; public abstract readonly defaultIntegration?: Integration; @@ -353,6 +364,9 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc return resource.resourceForPath(parts.join('/')); } + /** + * @deprecated - Throws error in some use cases that have been enabled since this deprecation notice. Use `RestApi.urlForPath()` instead. + */ public get url(): string { return this.restApi.urlForPath(this.path); } @@ -360,7 +374,7 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc export class Resource extends ResourceBase { public readonly parentResource?: IResource; - public readonly restApi: RestApi; + public readonly api: IRestApi; public readonly resourceId: string; public readonly path: string; @@ -380,21 +394,21 @@ export class Resource extends ResourceBase { } const resourceProps: CfnResourceProps = { - restApiId: props.parent.restApi.restApiId, + restApiId: props.parent.api.restApiId, parentId: props.parent.resourceId, pathPart: props.pathPart, }; const resource = new CfnResource(this, 'Resource', resourceProps); this.resourceId = resource.ref; - this.restApi = props.parent.restApi; + this.api = props.parent.api; // render resource path (special case for root) this.path = props.parent.path; if (!this.path.endsWith('/')) { this.path += '/'; } this.path += props.pathPart; - const deployment = props.parent.restApi.latestDeployment; + const deployment = props.parent.api.latestDeployment; if (deployment) { deployment.node.addDependency(resource); deployment.addToLogicalId({ resource: resourceProps }); @@ -413,6 +427,17 @@ export class Resource extends ResourceBase { this.addCorsPreflight(this.defaultCorsPreflightOptions); } } + + /** + * The RestApi associated with this Resource + * @deprecated - Throws an error if this Resource is not associated with an instance of `RestApi`. Use `api` instead. + */ + public get restApi(): RestApi { + if (!this.parentResource) { + throw new Error('parentResource was unexpectedly not defined'); + } + return this.parentResource.restApi; + } } export interface ProxyResourceOptions extends ResourceOptions { diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 5a43b562ff279..4d08a0b01ce36 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -16,12 +16,36 @@ import { IResource, ResourceBase, ResourceOptions } from './resource'; import { Stage, StageOptions } from './stage'; import { UsagePlan, UsagePlanProps } from './usage-plan'; +const RESTAPI_SYMBOL = Symbol.for('@aws-cdk/aws-apigateway.RestApiBase'); + export interface IRestApi extends IResourceBase { /** * The ID of this API Gateway RestApi. * @attribute */ readonly restApiId: string; + + /** + * The resource ID of the root resource. + * @attribute + */ + readonly restApiRootResourceId: string; + + /** + * API Gateway deployment that represents the latest changes of the API. + * This resource will be automatically updated every time the REST API model changes. + * `undefined` when no deployment is configured. + */ + readonly latestDeployment?: Deployment; + + /** + * Represents the root resource ("/") of this API. Use it to define the API model: + * + * api.root.addMethod('ANY', redirectToHomePage); // "ANY /" + * api.root.addResource('friends').addMethod('GET', getFriendsHandler); // "GET /friends" + * + */ + readonly root: IResource; } /** @@ -197,7 +221,36 @@ export interface SpecRestApiProps extends RestApiOptions { readonly apiDefinition: ApiDefinition; } -abstract class RestApiBase extends Resource implements IRestApi { +/** + * Base implementation that are common to various implementations of IRestApi + */ +export abstract class RestApiBase extends Resource implements IRestApi { + + /** + * Checks if the given object is an instance of RestApiBase. + * @internal + */ + public static _isRestApiBase(x: any): x is RestApiBase { + return x !== null && typeof(x) === 'object' && RESTAPI_SYMBOL in x; + } + + /** + * API Gateway deployment that represents the latest changes of the API. + * This resource will be automatically updated every time the REST API model changes. + * This will be undefined if `deploy` is false. + */ + public get latestDeployment() { + return this._latestDeployment; + } + + /** + * The first domain name mapped to this API, if defined through the `domainName` + * configuration prop, or added via `addDomainName` + */ + public get domainName() { + return this._domainName; + } + /** * The ID of this API Gateway RestApi. */ @@ -210,6 +263,12 @@ abstract class RestApiBase extends Resource implements IRestApi { */ public abstract readonly restApiRootResourceId: string; + /** + * Represents the root resource of this API endpoint ('/'). + * Resources and Methods are added to this resource. + */ + public abstract readonly root: IResource; + /** * API Gateway stage that points to the latest deployment (if defined). * @@ -225,6 +284,8 @@ abstract class RestApiBase extends Resource implements IRestApi { super(scope, id, { physicalName: props.restApiName || id, }); + + Object.defineProperty(this, RESTAPI_SYMBOL, { value: true }); } /** @@ -240,15 +301,6 @@ abstract class RestApiBase extends Resource implements IRestApi { return this.deploymentStage.urlForPath(path); } - /** - * API Gateway deployment that represents the latest changes of the API. - * This resource will be automatically updated every time the REST API model changes. - * This will be undefined if `deploy` is false. - */ - public get latestDeployment() { - return this._latestDeployment; - } - /** * Defines an API Gateway domain name and maps it to this API. * @param id The construct id @@ -272,14 +324,6 @@ abstract class RestApiBase extends Resource implements IRestApi { return new UsagePlan(this, id, props); } - /** - * The first domain name mapped to this API, if defined through the `domainName` - * configuration prop, or added via `addDomainName` - */ - public get domainName() { - return this._domainName; - } - /** * Gets the "execute-api" ARN * @returns The "execute-api" ARN. @@ -316,6 +360,16 @@ abstract class RestApiBase extends Resource implements IRestApi { }); } + /** + * Internal API used by `Method` to keep an inventory of methods at the API + * level for validation purposes. + * + * @internal + */ + public _attachMethod(method: Method) { + ignore(method); + } + protected configureCloudWatchRole(apiResource: CfnRestApi) { const role = new iam.Role(this, 'CloudWatchRole', { assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), @@ -384,6 +438,8 @@ export class SpecRestApi extends RestApiBase { */ public readonly restApiRootResourceId: string; + public readonly root: IResource; + constructor(scope: Construct, id: string, props: SpecRestApiProps) { super(scope, id, props); const apiDefConfig = props.apiDefinition.bind(this); @@ -398,6 +454,7 @@ export class SpecRestApi extends RestApiBase { this.node.defaultChild = resource; this.restApiId = resource.ref; this.restApiRootResourceId = resource.attrRootResourceId; + this.root = new RootResource(this, props, this.restApiRootResourceId); this.configureDeployment(props); if (props.domainName) { @@ -411,6 +468,21 @@ export class SpecRestApi extends RestApiBase { } } +/** + * Attributes that can be specified when importing a RestApi + */ +export interface RestApiAttributes { + /** + * The ID of the API Gateway RestApi. + */ + readonly restApiId: string; + + /** + * The resource ID of the root resource. + */ + readonly rootResourceId: string; +} + /** * Represents a REST API in Amazon API Gateway. * @@ -419,34 +491,44 @@ export class SpecRestApi extends RestApiBase { * By default, the API will automatically be deployed and accessible from a * public endpoint. */ -export class RestApi extends RestApiBase implements IRestApi { +export class RestApi extends RestApiBase { + /** + * Import an existing RestApi. + */ public static fromRestApiId(scope: Construct, id: string, restApiId: string): IRestApi { class Import extends Resource implements IRestApi { public readonly restApiId = restApiId; + + public get root(): IResource { + throw new Error('root is not configured when imported using `fromRestApiId()`. Use `fromRestApiAttributes()` API instead.'); + } + + public get restApiRootResourceId(): string { + throw new Error('restApiRootResourceId is not configured when imported using `fromRestApiId()`. Use `fromRestApiAttributes()` API instead.'); + } } return new Import(scope, id); } /** - * The ID of this API Gateway RestApi. + * Import an existing RestApi that can be configured with additional Methods and Resources. + * @experimental */ + public static fromRestApiAttributes(scope: Construct, id: string, attrs: RestApiAttributes): IRestApi { + class Import extends RestApiBase { + public readonly restApiId = attrs.restApiId; + public readonly restApiRootResourceId = attrs.rootResourceId; + public readonly root: IResource = new RootResource(this, {}, this.restApiRootResourceId); + } + + return new Import(scope, id); + } + public readonly restApiId: string; - /** - * Represents the root resource ("/") of this API. Use it to define the API model: - * - * api.root.addMethod('ANY', redirectToHomePage); // "ANY /" - * api.root.addResource('friends').addMethod('GET', getFriendsHandler); // "GET /friends" - * - */ public readonly root: IResource; - /** - * The resource ID of the root resource. - * - * @attribute - */ public readonly restApiRootResourceId: string; /** @@ -613,26 +695,47 @@ export enum EndpointType { class RootResource extends ResourceBase { public readonly parentResource?: IResource; - public readonly restApi: RestApi; + public readonly api: RestApiBase; public readonly resourceId: string; public readonly path: string; public readonly defaultIntegration?: Integration | undefined; public readonly defaultMethodOptions?: MethodOptions | undefined; public readonly defaultCorsPreflightOptions?: CorsOptions | undefined; - constructor(api: RestApi, props: RestApiProps, resourceId: string) { + private readonly _restApi?: RestApi; + + constructor(api: RestApiBase, props: ResourceOptions, resourceId: string) { super(api, 'Default'); this.parentResource = undefined; this.defaultIntegration = props.defaultIntegration; this.defaultMethodOptions = props.defaultMethodOptions; this.defaultCorsPreflightOptions = props.defaultCorsPreflightOptions; - this.restApi = api; + this.api = api; this.resourceId = resourceId; this.path = '/'; + if (api instanceof RestApi) { + this._restApi = api; + } + if (this.defaultCorsPreflightOptions) { this.addCorsPreflight(this.defaultCorsPreflightOptions); } } + + /** + * Get the RestApi associated with this Resource. + * @deprecated - Throws an error if this Resource is not associated with an instance of `RestApi`. Use `api` instead. + */ + public get restApi(): RestApi { + if (!this._restApi) { + throw new Error('RestApi is not available on Resource not connected to an instance of RestApi. Use `api` instead'); + } + return this._restApi; + } } + +function ignore(_x: any) { + return; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index c3003c458ea57..6816f6cc02ab7 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -176,14 +176,12 @@ "docs-public-apis:@aws-cdk/aws-apigateway.Method.httpMethod", "docs-public-apis:@aws-cdk/aws-apigateway.Method.methodId", "docs-public-apis:@aws-cdk/aws-apigateway.Method.resource", - "docs-public-apis:@aws-cdk/aws-apigateway.Method.restApi", "docs-public-apis:@aws-cdk/aws-apigateway.Model", "docs-public-apis:@aws-cdk/aws-apigateway.Model.fromModelName", "docs-public-apis:@aws-cdk/aws-apigateway.RequestValidator", "docs-public-apis:@aws-cdk/aws-apigateway.RequestValidator.fromRequestValidatorId", "docs-public-apis:@aws-cdk/aws-apigateway.Resource", "docs-public-apis:@aws-cdk/aws-apigateway.ResourceBase", - "docs-public-apis:@aws-cdk/aws-apigateway.RestApi.fromRestApiId", "docs-public-apis:@aws-cdk/aws-apigateway.RestApi.arnForExecuteApi", "docs-public-apis:@aws-cdk/aws-apigateway.Stage", "docs-public-apis:@aws-cdk/aws-apigateway.Stage.restApi", diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json index bcf74c12601fa..8946e415c6874 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json @@ -44,14 +44,63 @@ "Name": "my-api" } }, - "myapiDeployment92F2CB49eb6b0027bfbdb20b09988607569e06bd": { + "myapibooks51D54548": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "myapi4C7BF186", + "RootResourceId" + ] + }, + "PathPart": "books", + "RestApiId": { + "Ref": "myapi4C7BF186" + } + } + }, + "myapibooksGETD6B2F597": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "ResourceId": { + "Ref": "myapibooks51D54548" + }, + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationResponses": [ + { + "StatusCode": "200" + } + ], + "PassthroughBehavior": "NEVER", + "RequestTemplates": { + "application/json": "{ \"statusCode\": 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "StatusCode": "200" + } + ] + } + }, + "myapiDeployment92F2CB49fe116fef7f552ff0fc433c9aa3930d2f": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { "Ref": "myapi4C7BF186" }, "Description": "Automatically created by the RestApi construct" - } + }, + "DependsOn": [ + "myapibooksGETD6B2F597", + "myapibooks51D54548" + ] }, "myapiDeploymentStageprod298F01AF": { "Type": "AWS::ApiGateway::Stage", @@ -60,7 +109,7 @@ "Ref": "myapi4C7BF186" }, "DeploymentId": { - "Ref": "myapiDeployment92F2CB49eb6b0027bfbdb20b09988607569e06bd" + "Ref": "myapiDeployment92F2CB49fe116fef7f552ff0fc433c9aa3930d2f" }, "StageName": "prod" } @@ -163,6 +212,32 @@ ] ] } + }, + "BooksURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "myapi4C7BF186" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "myapiDeploymentStageprod298F01AF" + }, + "/books" + ] + ] + } } }, "Parameters": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.ts b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.ts index 1b8531ccad8d5..63e6343f4de26 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.ts @@ -4,7 +4,8 @@ import * as apigateway from '../lib'; /* * Stack verification steps: - * * `curl -i ` should return HTTP code 200 + * * `curl -s -o /dev/null -w "%{http_code}" ` should return HTTP code 200 + * * `curl -s -o /dev/null -w "%{http_code}" ` should return HTTP code 200 */ const app = new cdk.App(); @@ -14,8 +15,24 @@ const api = new apigateway.SpecRestApi(stack, 'my-api', { apiDefinition: apigateway.ApiDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')), }); +api.root.addResource('books').addMethod('GET', new apigateway.MockIntegration({ + integrationResponses: [{ + statusCode: '200', + }], + passthroughBehavior: apigateway.PassthroughBehavior.NEVER, + requestTemplates: { + 'application/json': '{ "statusCode": 200 }', + }, +}), { + methodResponses: [ { statusCode: '200' } ], +}); + new cdk.CfnOutput(stack, 'PetsURL', { value: api.urlForPath('/pets'), }); +new cdk.CfnOutput(stack, 'BooksURL', { + value: api.urlForPath('/books'), +}); + app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi-import.lit.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi-import.lit.expected.json new file mode 100644 index 0000000000000..349ae37ce27c8 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi-import.lit.expected.json @@ -0,0 +1,334 @@ +{ + "Resources": { + "RestApi0C43BF4B": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "RestApi" + } + }, + "RestApiCloudWatchRoleE3ED6605": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "RestApiAccount7C83CF5A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "RestApiCloudWatchRoleE3ED6605", + "Arn" + ] + } + }, + "DependsOn": [ + "RestApi0C43BF4B" + ] + }, + "RestApiANYA7C1DC94": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "RestApi0C43BF4B", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "AuthorizationType": "NONE", + "Integration": { + "Type": "MOCK" + } + } + }, + "integrestapiimportPetsStackNestedStackintegrestapiimportPetsStackNestedStackResource2B31898B": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParametersc6464ef3a9925cfe5c28d912ee7fc0952eb5135b281419c8d450a3aa8825e1efS3BucketFE7B8A1B" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersc6464ef3a9925cfe5c28d912ee7fc0952eb5135b281419c8d450a3aa8825e1efS3VersionKeyB80604FE" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersc6464ef3a9925cfe5c28d912ee7fc0952eb5135b281419c8d450a3aa8825e1efS3VersionKeyB80604FE" + } + ] + } + ] + } + ] + ] + }, + "Parameters": { + "referencetointegrestapiimportRootStackRestApi2647DA4CRootResourceId": { + "Fn::GetAtt": [ + "RestApi0C43BF4B", + "RootResourceId" + ] + }, + "referencetointegrestapiimportRootStackRestApi2647DA4CRef": { + "Ref": "RestApi0C43BF4B" + } + } + } + }, + "integrestapiimportBooksStackNestedStackintegrestapiimportBooksStackNestedStackResource395C2C9B": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParameters480caddfb9aa669df64905982e75c672d967ce9d9ed261ee8c73f6bdcaf97141S3Bucket74F8A623" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters480caddfb9aa669df64905982e75c672d967ce9d9ed261ee8c73f6bdcaf97141S3VersionKeyC855AC3B" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters480caddfb9aa669df64905982e75c672d967ce9d9ed261ee8c73f6bdcaf97141S3VersionKeyC855AC3B" + } + ] + } + ] + } + ] + ] + }, + "Parameters": { + "referencetointegrestapiimportRootStackRestApi2647DA4CRootResourceId": { + "Fn::GetAtt": [ + "RestApi0C43BF4B", + "RootResourceId" + ] + }, + "referencetointegrestapiimportRootStackRestApi2647DA4CRef": { + "Ref": "RestApi0C43BF4B" + } + } + } + }, + "integrestapiimportDeployStackNestedStackintegrestapiimportDeployStackNestedStackResource0D0EE737": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParameters04407a85c5bf6d4da110e25ee35b1f67903f760cd7835965518b0f7ad37e86abS3BucketADE4C6AE" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters04407a85c5bf6d4da110e25ee35b1f67903f760cd7835965518b0f7ad37e86abS3VersionKeyF36B0062" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters04407a85c5bf6d4da110e25ee35b1f67903f760cd7835965518b0f7ad37e86abS3VersionKeyF36B0062" + } + ] + } + ] + } + ] + ] + }, + "Parameters": { + "referencetointegrestapiimportRootStackRestApi2647DA4CRef": { + "Ref": "RestApi0C43BF4B" + } + } + }, + "DependsOn": [ + "integrestapiimportBooksStackNestedStackintegrestapiimportBooksStackNestedStackResource395C2C9B", + "integrestapiimportPetsStackNestedStackintegrestapiimportPetsStackNestedStackResource2B31898B" + ] + } + }, + "Outputs": { + "PetsURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "RestApi0C43BF4B" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com/prod/pets" + ] + ] + } + }, + "BooksURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "RestApi0C43BF4B" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com/prod/books" + ] + ] + } + } + }, + "Parameters": { + "AssetParametersc6464ef3a9925cfe5c28d912ee7fc0952eb5135b281419c8d450a3aa8825e1efS3BucketFE7B8A1B": { + "Type": "String", + "Description": "S3 bucket for asset \"c6464ef3a9925cfe5c28d912ee7fc0952eb5135b281419c8d450a3aa8825e1ef\"" + }, + "AssetParametersc6464ef3a9925cfe5c28d912ee7fc0952eb5135b281419c8d450a3aa8825e1efS3VersionKeyB80604FE": { + "Type": "String", + "Description": "S3 key for asset version \"c6464ef3a9925cfe5c28d912ee7fc0952eb5135b281419c8d450a3aa8825e1ef\"" + }, + "AssetParametersc6464ef3a9925cfe5c28d912ee7fc0952eb5135b281419c8d450a3aa8825e1efArtifactHashED1A6259": { + "Type": "String", + "Description": "Artifact hash for asset \"c6464ef3a9925cfe5c28d912ee7fc0952eb5135b281419c8d450a3aa8825e1ef\"" + }, + "AssetParameters480caddfb9aa669df64905982e75c672d967ce9d9ed261ee8c73f6bdcaf97141S3Bucket74F8A623": { + "Type": "String", + "Description": "S3 bucket for asset \"480caddfb9aa669df64905982e75c672d967ce9d9ed261ee8c73f6bdcaf97141\"" + }, + "AssetParameters480caddfb9aa669df64905982e75c672d967ce9d9ed261ee8c73f6bdcaf97141S3VersionKeyC855AC3B": { + "Type": "String", + "Description": "S3 key for asset version \"480caddfb9aa669df64905982e75c672d967ce9d9ed261ee8c73f6bdcaf97141\"" + }, + "AssetParameters480caddfb9aa669df64905982e75c672d967ce9d9ed261ee8c73f6bdcaf97141ArtifactHash1198374C": { + "Type": "String", + "Description": "Artifact hash for asset \"480caddfb9aa669df64905982e75c672d967ce9d9ed261ee8c73f6bdcaf97141\"" + }, + "AssetParameters04407a85c5bf6d4da110e25ee35b1f67903f760cd7835965518b0f7ad37e86abS3BucketADE4C6AE": { + "Type": "String", + "Description": "S3 bucket for asset \"04407a85c5bf6d4da110e25ee35b1f67903f760cd7835965518b0f7ad37e86ab\"" + }, + "AssetParameters04407a85c5bf6d4da110e25ee35b1f67903f760cd7835965518b0f7ad37e86abS3VersionKeyF36B0062": { + "Type": "String", + "Description": "S3 key for asset version \"04407a85c5bf6d4da110e25ee35b1f67903f760cd7835965518b0f7ad37e86ab\"" + }, + "AssetParameters04407a85c5bf6d4da110e25ee35b1f67903f760cd7835965518b0f7ad37e86abArtifactHash6DD5E125": { + "Type": "String", + "Description": "Artifact hash for asset \"04407a85c5bf6d4da110e25ee35b1f67903f760cd7835965518b0f7ad37e86ab\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi-import.lit.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi-import.lit.ts new file mode 100644 index 0000000000000..bea2be6c5b05f --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi-import.lit.ts @@ -0,0 +1,124 @@ +import { App, CfnOutput, Construct, NestedStack, NestedStackProps, Stack } from '@aws-cdk/core'; +import { Deployment, Method, MockIntegration, PassthroughBehavior, RestApi, Stage } from '../lib'; + +/** + * This file showcases how to split up a RestApi's Resources and Methods across nested stacks. + * + * The root stack 'RootStack' first defines a RestApi. + * Two nested stacks BooksStack and PetsStack, create corresponding Resources '/books' and '/pets'. + * They are then deployed to a 'prod' Stage via a third nested stack - DeployStack. + * + * To verify this worked, go to the APIGateway + */ + +class RootStack extends Stack { + constructor(scope: Construct) { + super(scope, 'integ-restapi-import-RootStack'); + + const restApi = new RestApi(this, 'RestApi', { + deploy: false, + }); + restApi.root.addMethod('ANY'); + + const petsStack = new PetsStack(this, { + restApiId: restApi.restApiId, + rootResourceId: restApi.restApiRootResourceId, + }); + const booksStack = new BooksStack(this, { + restApiId: restApi.restApiId, + rootResourceId: restApi.restApiRootResourceId, + }); + new DeployStack(this, { + restApiId: restApi.restApiId, + methods: [ ...petsStack.methods, ...booksStack.methods ], + }); + + new CfnOutput(this, 'PetsURL', { + value: `https://${restApi.restApiId}.execute-api.${this.region}.amazonaws.com/prod/pets`, + }); + + new CfnOutput(this, 'BooksURL', { + value: `https://${restApi.restApiId}.execute-api.${this.region}.amazonaws.com/prod/books`, + }); + } +} + +interface ResourceNestedStackProps extends NestedStackProps { + readonly restApiId: string; + + readonly rootResourceId: string; +} + +class PetsStack extends NestedStack { + public readonly methods: Method[] = []; + + constructor(scope: Construct, props: ResourceNestedStackProps) { + super(scope, 'integ-restapi-import-PetsStack', props); + + const api = RestApi.fromRestApiAttributes(this, 'RestApi', { + restApiId: props.restApiId, + rootResourceId: props.rootResourceId, + }); + + const method = api.root.addResource('pets').addMethod('GET', new MockIntegration({ + integrationResponses: [{ + statusCode: '200', + }], + passthroughBehavior: PassthroughBehavior.NEVER, + requestTemplates: { + 'application/json': '{ "statusCode": 200 }', + }, + }), { + methodResponses: [ { statusCode: '200' } ], + }); + + this.methods.push(method); + } +} + +class BooksStack extends NestedStack { + public readonly methods: Method[] = []; + + constructor(scope: Construct, props: ResourceNestedStackProps) { + super(scope, 'integ-restapi-import-BooksStack', props); + + const api = RestApi.fromRestApiAttributes(this, 'RestApi', { + restApiId: props.restApiId, + rootResourceId: props.rootResourceId, + }); + + const method = api.root.addResource('books').addMethod('GET', new MockIntegration({ + integrationResponses: [{ + statusCode: '200', + }], + passthroughBehavior: PassthroughBehavior.NEVER, + requestTemplates: { + 'application/json': '{ "statusCode": 200 }', + }, + }), { + methodResponses: [ { statusCode: '200' } ], + }); + + this.methods.push(method); + } +} + +interface DeployStackProps extends NestedStackProps { + readonly restApiId: string; + + readonly methods?: Method[]; +} + +class DeployStack extends NestedStack { + constructor(scope: Construct, props: DeployStackProps) { + super(scope, 'integ-restapi-import-DeployStack', props); + + const deployment = new Deployment(this, 'Deployment', { + api: RestApi.fromRestApiId(this, 'RestApi', props.restApiId), + }); + (props.methods ?? []).forEach((method) => deployment.node.addDependency(method)); + new Stage(this, 'Stage', { deployment }); + } +} + +new RootStack(new App()); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/test.authorizer.ts b/packages/@aws-cdk/aws-apigateway/test/test.authorizer.ts index ce5a5279228e3..89b6905fddb4e 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.authorizer.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.authorizer.ts @@ -1,12 +1,12 @@ import { Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; -import { Authorizer, RestApi } from '../lib'; +import { Authorizer, IRestApi } from '../lib'; export = { 'isAuthorizer correctly detects an instance of type Authorizer'(test: Test) { class MyAuthorizer extends Authorizer { public readonly authorizerId = 'test-authorizer-id'; - public _attachToApi(_: RestApi): void { + public _attachToApi(_: IRestApi): void { // do nothing } } diff --git a/packages/@aws-cdk/aws-apigateway/test/test.method.ts b/packages/@aws-cdk/aws-apigateway/test/test.method.ts index e4383ecf768ac..bb0d28b976094 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.method.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.method.ts @@ -928,4 +928,38 @@ export = { test.done(); }, + + '"restApi" and "api" properties return the RestApi correctly'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const api = new apigw.RestApi(stack, 'test-api'); + const method = api.root.addResource('pets').addMethod('GET'); + + // THEN + test.ok(method.restApi); + test.ok(method.api); + test.deepEqual(stack.resolve(method.api.restApiId), stack.resolve(method.restApi.restApiId)); + + test.done(); + }, + + '"restApi" throws an error on imported while "api" returns correctly'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const api = apigw.RestApi.fromRestApiAttributes(stack, 'test-api', { + restApiId: 'test-rest-api-id', + rootResourceId: 'test-root-resource-id', + }); + const method = api.root.addResource('pets').addMethod('GET'); + + // THEN + test.throws(() => method.restApi, /not available on Resource not connected to an instance of RestApi/); + test.ok(method.api); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts index d512b924cfe98..4df5bd3fd2755 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts @@ -335,18 +335,6 @@ export = { test.done(); }, - 'fromRestApiId'(test: Test) { - // GIVEN - const stack = new Stack(); - - // WHEN - const imported = apigw.RestApi.fromRestApiId(stack, 'imported-api', 'api-rxt4498f'); - - // THEN - test.deepEqual(stack.resolve(imported.restApiId), 'api-rxt4498f'); - test.done(); - }, - '"url" and "urlForPath" return the URL endpoints of the deployed API'(test: Test) { // GIVEN const stack = new Stack(); @@ -933,4 +921,102 @@ export = { test.done(); }, + + '"restApi" and "api" properties return the RestApi correctly'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.RestApi(stack, 'test-api'); + const method = api.root.addResource('pets').addMethod('GET'); + + // THEN + test.ok(method.restApi); + test.ok(method.api); + test.deepEqual(stack.resolve(method.api.restApiId), stack.resolve(method.restApi.restApiId)); + + test.done(); + }, + + '"restApi" throws an error on imported while "api" returns correctly'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = apigw.RestApi.fromRestApiAttributes(stack, 'test-api', { + restApiId: 'test-rest-api-id', + rootResourceId: 'test-root-resource-id', + }); + const method = api.root.addResource('pets').addMethod('GET'); + + // THEN + test.throws(() => method.restApi, /not available on Resource not connected to an instance of RestApi/); + test.ok(method.api); + + test.done(); + }, + + 'Import': { + 'fromRestApiId()'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const imported = apigw.RestApi.fromRestApiId(stack, 'imported-api', 'api-rxt4498f'); + + // THEN + test.deepEqual(stack.resolve(imported.restApiId), 'api-rxt4498f'); + test.done(); + }, + + 'fromRestApiAttributes()'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const imported = apigw.RestApi.fromRestApiAttributes(stack, 'imported-api', { + restApiId: 'test-restapi-id', + rootResourceId: 'test-root-resource-id', + }); + const resource = imported.root.addResource('pets'); + resource.addMethod('GET'); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Resource', { + PathPart: 'pets', + ParentId: stack.resolve(imported.restApiRootResourceId), + })); + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + ResourceId: stack.resolve(resource.resourceId), + })); + + test.done(); + }, + }, + + 'SpecRestApi': { + 'add Methods and Resources'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.SpecRestApi(stack, 'SpecRestApi', { + apiDefinition: apigw.ApiDefinition.fromInline({ foo: 'bar' }), + }); + + // WHEN + const resource = api.root.addResource('pets'); + resource.addMethod('GET'); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Resource', { + PathPart: 'pets', + ParentId: stack.resolve(api.restApiRootResourceId), + })); + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + ResourceId: stack.resolve(resource.resourceId), + })); + test.done(); + }, + }, }; diff --git a/packages/@aws-cdk/aws-applicationautoscaling/package.json b/packages/@aws-cdk/aws-applicationautoscaling/package.json index 5f4b909ff44b4..2799c6d520683 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/package.json +++ b/packages/@aws-cdk/aws-applicationautoscaling/package.json @@ -66,7 +66,7 @@ "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", - "fast-check": "^1.24.2", + "fast-check": "^1.25.0", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-appsync/README.md b/packages/@aws-cdk/aws-appsync/README.md index 5e978b796045f..0c0b27c8e6f92 100644 --- a/packages/@aws-cdk/aws-appsync/README.md +++ b/packages/@aws-cdk/aws-appsync/README.md @@ -54,6 +54,7 @@ type Mutation { saveCustomer(id: String!, customer: SaveCustomerInput!): Customer removeCustomer(id: String!): Customer saveCustomerWithFirstOrder(customer: SaveCustomerInput!, order: FirstOrderInput!, referral: String): Order + doPostOnAws: String! } ``` @@ -75,13 +76,16 @@ export class ApiStack extends Stack { }, authorizationConfig: { defaultAuthorization: { - userPool, - defaultAction: UserPoolDefaultAction.ALLOW, + authorizationType: AuthorizationType.USER_POOL, + userPoolConfig: { + userPool, + defaultAction: UserPoolDefaultAction.ALLOW + }, }, additionalAuthorizationModes: [ { - apiKeyDesc: 'My API Key', - }, + authorizationType: AuthorizationType.API_KEY, + } ], }, schemaDefinitionFile: './schema.graphql', @@ -154,6 +158,40 @@ export class ApiStack extends Stack { requestMappingTemplate: MappingTemplate.dynamoDbDeleteItem('id', 'id'), responseMappingTemplate: MappingTemplate.dynamoDbResultItem(), }); + + const httpDS = api.addHttpDataSource('http', 'The http data source', 'https://aws.amazon.com/'); + + httpDS.createResolver({ + typeName: 'Mutation', + fieldName: 'doPostOnAws', + requestMappingTemplate: MappingTemplate.fromString(`{ + "version": "2018-05-29", + "method": "POST", + # if full path is https://api.xxxxxxxxx.com/posts then resourcePath would be /posts + "resourcePath": "/path/123", + "params":{ + "body": $util.toJson($ctx.args), + "headers":{ + "Content-Type": "application/json", + "Authorization": "$ctx.request.headers.Authorization" + } + } + }`), + responseMappingTemplate: MappingTemplate.fromString(` + ## Raise a GraphQL field error in case of a datasource invocation error + #if($ctx.error) + $util.error($ctx.error.message, $ctx.error.type) + #end + ## if the response status code is not 200, then return an error. Else return the body ** + #if($ctx.result.statusCode == 200) + ## If response is 200, return the body. + $ctx.result.body + #else + ## If response is not 200, append the response to error block. + $utils.appendError($ctx.result.body, "$ctx.result.statusCode") + #end + `), + }); } } -``` \ No newline at end of file +``` diff --git a/packages/@aws-cdk/aws-appsync/jest.config.js b/packages/@aws-cdk/aws-appsync/jest.config.js index cd664e1d069e5..d9634b8ea0c5d 100644 --- a/packages/@aws-cdk/aws-appsync/jest.config.js +++ b/packages/@aws-cdk/aws-appsync/jest.config.js @@ -1,2 +1,10 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); -module.exports = baseConfig; +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + branches: 1, + statements: 1, + } + } +}; diff --git a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts index b1da44a40bee0..2d4f802f72048 100644 --- a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts +++ b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts @@ -1,15 +1,74 @@ import { IUserPool } from '@aws-cdk/aws-cognito'; import { Table } from '@aws-cdk/aws-dynamodb'; -import { IGrantable, IPrincipal, IRole, ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; +import { + IGrantable, + IPrincipal, + IRole, + ManagedPolicy, + Role, + ServicePrincipal, +} from '@aws-cdk/aws-iam'; import { IFunction } from '@aws-cdk/aws-lambda'; import { Construct, Duration, IResolvable } from '@aws-cdk/core'; import { readFileSync } from 'fs'; -import { CfnApiKey, CfnDataSource, CfnGraphQLApi, CfnGraphQLSchema, CfnResolver } from './appsync.generated'; +import { + CfnApiKey, + CfnDataSource, + CfnGraphQLApi, + CfnGraphQLSchema, + CfnResolver, +} from './appsync.generated'; /** - * Marker interface for the different authorization modes. + * enum with all possible values for AppSync authorization type */ -export interface AuthMode { } +export enum AuthorizationType { + /** + * API Key authorization type + */ + API_KEY = 'API_KEY', + /** + * AWS IAM authorization type. Can be used with Cognito Identity Pool federated credentials + */ + IAM = 'AWS_IAM', + /** + * Cognito User Pool authorization type + */ + USER_POOL = 'AMAZON_COGNITO_USER_POOLS', + /** + * OpenID Connect authorization type + */ + OIDC = 'OPENID_CONNECT', +} + +/** + * Interface to specify default or additional authorization(s) + */ +export interface AuthorizationMode { + /** + * One of possible four values AppSync supports + * + * @see https://docs.aws.amazon.com/appsync/latest/devguide/security.html + * + * @default - `AuthorizationType.API_KEY` + */ + readonly authorizationType: AuthorizationType; + /** + * If authorizationType is `AuthorizationType.USER_POOL`, this option is required. + * @default - none + */ + readonly userPoolConfig?: UserPoolConfig; + /** + * If authorizationType is `AuthorizationType.API_KEY`, this option can be configured. + * @default - check default values of `ApiKeyConfig` memebers + */ + readonly apiKeyConfig?: ApiKeyConfig; + /** + * If authorizationType is `AuthorizationType.OIDC`, this option is required. + * @default - none + */ + readonly openIdConnectConfig?: OpenIdConnectConfig; +} /** * enum with all possible values for Cognito user-pool default actions @@ -28,8 +87,7 @@ export enum UserPoolDefaultAction { /** * Configuration for Cognito user-pools in AppSync */ -export interface UserPoolConfig extends AuthMode { - +export interface UserPoolConfig { /** * The Cognito user pool to use as identity source */ @@ -48,18 +106,20 @@ export interface UserPoolConfig extends AuthMode { readonly defaultAction?: UserPoolDefaultAction; } -function isUserPoolConfig(obj: unknown): obj is UserPoolConfig { - return (obj as UserPoolConfig).userPool !== undefined; -} - /** * Configuration for API Key authorization in AppSync */ -export interface ApiKeyConfig extends AuthMode { +export interface ApiKeyConfig { /** - * Unique description of the API key + * Unique name of the API Key + * @default - 'DefaultAPIKey' */ - readonly apiKeyDesc: string; + readonly name?: string; + /** + * Description of API key + * @default - 'Default API Key created by CDK' + */ + readonly description?: string; /** * The time from creation time after which the API key expires, using RFC3339 representation. @@ -70,8 +130,33 @@ export interface ApiKeyConfig extends AuthMode { readonly expires?: string; } -function isApiKeyConfig(obj: unknown): obj is ApiKeyConfig { - return (obj as ApiKeyConfig).apiKeyDesc !== undefined; +/** + * Configuration for OpenID Connect authorization in AppSync + */ +export interface OpenIdConnectConfig { + /** + * The number of milliseconds an OIDC token is valid after being authenticated by OIDC provider. + * `auth_time` claim in OIDC token is required for this validation to work. + * @default - no validation + */ + readonly tokenExpiryFromAuth?: number; + /** + * The number of milliseconds an OIDC token is valid after being issued to a user. + * This validation uses `iat` claim of OIDC token. + * @default - no validation + */ + readonly tokenExpiryFromIssue?: number; + /** + * The client identifier of the Relying party at the OpenID identity provider. + * A regular expression can be specified so AppSync can validate against multiple client identifiers at a time. + * @example - 'ABCD|CDEF' where ABCD and CDEF are two different clientId + * @default - * (All) + */ + readonly clientId?: string; + /** + * The issuer for the OIDC configuration. The issuer returned by discovery must exactly match the value of `iss` in the OIDC token. + */ + readonly oidcProvider: string; } /** @@ -83,14 +168,14 @@ export interface AuthorizationConfig { * * @default - API Key authorization */ - readonly defaultAuthorization?: AuthMode; + readonly defaultAuthorization?: AuthorizationMode; /** * Additional authorization modes * * @default - No other modes */ - readonly additionalAuthorizationModes?: [AuthMode] + readonly additionalAuthorizationModes?: AuthorizationMode[]; } /** @@ -206,22 +291,56 @@ export class GraphQLApi extends Construct { constructor(scope: Construct, id: string, props: GraphQLApiProps) { super(scope, id); + this.validateAuthorizationProps(props); + const defaultAuthorizationType = + props.authorizationConfig?.defaultAuthorization?.authorizationType || + AuthorizationType.API_KEY; + let apiLogsRole; if (props.logConfig) { - apiLogsRole = new Role(this, 'ApiLogsRole', { assumedBy: new ServicePrincipal('appsync') }); - apiLogsRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSAppSyncPushToCloudWatchLogs')); + apiLogsRole = new Role(this, 'ApiLogsRole', { + assumedBy: new ServicePrincipal('appsync'), + }); + apiLogsRole.addManagedPolicy( + ManagedPolicy.fromAwsManagedPolicyName( + 'service-role/AWSAppSyncPushToCloudWatchLogs', + ), + ); } this.api = new CfnGraphQLApi(this, 'Resource', { name: props.name, - authenticationType: 'API_KEY', - ...props.logConfig && { + authenticationType: defaultAuthorizationType, + ...(props.logConfig && { logConfig: { cloudWatchLogsRoleArn: apiLogsRole ? apiLogsRole.roleArn : undefined, excludeVerboseContent: props.logConfig.excludeVerboseContent, - fieldLogLevel: props.logConfig.fieldLogLevel ? props.logConfig.fieldLogLevel.toString() : undefined, + fieldLogLevel: props.logConfig.fieldLogLevel + ? props.logConfig.fieldLogLevel.toString() + : undefined, }, - }, + }), + openIdConnectConfig: + props.authorizationConfig?.defaultAuthorization?.authorizationType === + AuthorizationType.OIDC + ? this.formatOpenIdConnectConfig( + props.authorizationConfig.defaultAuthorization + .openIdConnectConfig!, + ) + : undefined, + userPoolConfig: + props.authorizationConfig?.defaultAuthorization?.authorizationType === + AuthorizationType.USER_POOL + ? this.formatUserPoolConfig( + props.authorizationConfig.defaultAuthorization.userPoolConfig!, + ) + : undefined, + additionalAuthenticationProviders: props.authorizationConfig + ?.additionalAuthorizationModes!.length + ? this.formatAdditionalAuthorizationModes( + props.authorizationConfig!.additionalAuthorizationModes!, + ) + : undefined, }); this.apiId = this.api.attrApiId; @@ -229,8 +348,18 @@ export class GraphQLApi extends Construct { this.graphQlUrl = this.api.attrGraphQlUrl; this.name = this.api.name; - if (props.authorizationConfig) { - this.setupAuth(props.authorizationConfig); + if ( + defaultAuthorizationType === AuthorizationType.API_KEY || + props.authorizationConfig?.additionalAuthorizationModes?.findIndex( + (authMode) => authMode.authorizationType === AuthorizationType.API_KEY + ) !== -1 + ) { + const apiKeyConfig: ApiKeyConfig = props.authorizationConfig + ?.defaultAuthorization?.apiKeyConfig || { + name: 'DefaultAPIKey', + description: 'Default API Key created by CDK', + }; + this.createAPIKey(apiKeyConfig); } let definition; @@ -266,7 +395,11 @@ export class GraphQLApi extends Construct { * @param description The description of the data source * @param table The DynamoDB table backing this data source [disable-awslint:ref-via-interface] */ - public addDynamoDbDataSource(name: string, description: string, table: Table): DynamoDbDataSource { + public addDynamoDbDataSource( + name: string, + description: string, + table: Table, + ): DynamoDbDataSource { return new DynamoDbDataSource(this, `${name}DS`, { api: this, description, @@ -275,13 +408,32 @@ export class GraphQLApi extends Construct { }); } + /** + * add a new http data source to this API + * @param name The name of the data source + * @param description The description of the data source + * @param endpoint The http endpoint + */ + public addHttpDataSource(name: string, description: string, endpoint: string): HttpDataSource { + return new HttpDataSource(this, `${name}DS`, { + api: this, + description, + endpoint, + name, + }); + } + /** * add a new Lambda data source to this API * @param name The name of the data source * @param description The description of the data source * @param lambdaFunction The Lambda function to call to interact with this data source */ - public addLambdaDataSource(name: string, description: string, lambdaFunction: IFunction): LambdaDataSource { + public addLambdaDataSource( + name: string, + description: string, + lambdaFunction: IFunction, + ): LambdaDataSource { return new LambdaDataSource(this, `${name}DS`, { api: this, description, @@ -290,55 +442,132 @@ export class GraphQLApi extends Construct { }); } - private setupAuth(auth: AuthorizationConfig) { - if (isUserPoolConfig(auth.defaultAuthorization)) { - const { authenticationType, userPoolConfig } = this.userPoolDescFrom(auth.defaultAuthorization); - this.api.authenticationType = authenticationType; - this.api.userPoolConfig = userPoolConfig; - } else if (isApiKeyConfig(auth.defaultAuthorization)) { - this.api.authenticationType = this.apiKeyDesc(auth.defaultAuthorization).authenticationType; + private validateAuthorizationProps(props: GraphQLApiProps) { + const defaultAuthorizationType = + props.authorizationConfig?.defaultAuthorization?.authorizationType || + AuthorizationType.API_KEY; + + if ( + defaultAuthorizationType === AuthorizationType.OIDC && + !props.authorizationConfig?.defaultAuthorization?.openIdConnectConfig + ) { + throw new Error('Missing default OIDC Configuration'); } - this.api.additionalAuthenticationProviders = []; - for (const mode of (auth.additionalAuthorizationModes || [])) { - if (isUserPoolConfig(mode)) { - this.api.additionalAuthenticationProviders.push(this.userPoolDescFrom(mode)); - } else if (isApiKeyConfig(mode)) { - this.api.additionalAuthenticationProviders.push(this.apiKeyDesc(mode)); - } + if ( + defaultAuthorizationType === AuthorizationType.USER_POOL && + !props.authorizationConfig?.defaultAuthorization?.userPoolConfig + ) { + throw new Error('Missing default User Pool Configuration'); + } + + if (props.authorizationConfig?.additionalAuthorizationModes) { + props.authorizationConfig.additionalAuthorizationModes.forEach( + (authorizationMode) => { + if ( + authorizationMode.authorizationType === AuthorizationType.API_KEY && + defaultAuthorizationType === AuthorizationType.API_KEY + ) { + throw new Error( + "You can't duplicate API_KEY in additional authorization config. See https://docs.aws.amazon.com/appsync/latest/devguide/security.html", + ); + } + + if ( + authorizationMode.authorizationType === AuthorizationType.IAM && + defaultAuthorizationType === AuthorizationType.IAM + ) { + throw new Error( + "You can't duplicate IAM in additional authorization config. See https://docs.aws.amazon.com/appsync/latest/devguide/security.html", + ); + } + + if ( + authorizationMode.authorizationType === AuthorizationType.OIDC && + !authorizationMode.openIdConnectConfig + ) { + throw new Error( + 'Missing OIDC Configuration inside an additional authorization mode', + ); + } + + if ( + authorizationMode.authorizationType === + AuthorizationType.USER_POOL && + !authorizationMode.userPoolConfig + ) { + throw new Error( + 'Missing User Pool Configuration inside an additional authorization mode', + ); + } + }, + ); } } - private userPoolDescFrom(upConfig: UserPoolConfig): { authenticationType: string; userPoolConfig: CfnGraphQLApi.UserPoolConfigProperty } { + private formatOpenIdConnectConfig( + config: OpenIdConnectConfig, + ): CfnGraphQLApi.OpenIDConnectConfigProperty { return { - authenticationType: 'AMAZON_COGNITO_USER_POOLS', - userPoolConfig: { - appIdClientRegex: upConfig.appIdClientRegex, - userPoolId: upConfig.userPool.userPoolId, - awsRegion: upConfig.userPool.stack.region, - defaultAction: upConfig.defaultAction ? upConfig.defaultAction.toString() : 'ALLOW', - }, + authTtl: config.tokenExpiryFromAuth, + clientId: config.clientId, + iatTtl: config.tokenExpiryFromIssue, + issuer: config.oidcProvider, + }; + } + + private formatUserPoolConfig( + config: UserPoolConfig, + ): CfnGraphQLApi.UserPoolConfigProperty { + return { + userPoolId: config.userPool.userPoolId, + awsRegion: config.userPool.stack.region, + appIdClientRegex: config.appIdClientRegex, + defaultAction: config.defaultAction || 'ALLOW', }; } - private apiKeyDesc(akConfig: ApiKeyConfig): { authenticationType: string } { + private createAPIKey(config: ApiKeyConfig) { let expires: number | undefined; - if (akConfig.expires) { - expires = new Date(akConfig.expires).valueOf(); - const now = Date.now(); - const days = (d: number) => now + Duration.days(d).toMilliseconds(); + if (config.expires) { + expires = new Date(config.expires).valueOf(); + const days = (d: number) => + Date.now() + Duration.days(d).toMilliseconds(); if (expires < days(1) || expires > days(365)) { throw Error('API key expiration must be between 1 and 365 days.'); } expires = Math.round(expires / 1000); } - const key = new CfnApiKey(this, `${akConfig.apiKeyDesc || ''}ApiKey`, { + const key = new CfnApiKey(this, `${config.name || 'DefaultAPIKey'}ApiKey`, { expires, - description: akConfig.apiKeyDesc, + description: config.description || 'Default API Key created by CDK', apiId: this.apiId, }); this._apiKey = key.attrApiKey; - return { authenticationType: 'API_KEY' }; + } + + private formatAdditionalAuthorizationModes( + authModes: AuthorizationMode[], + ): CfnGraphQLApi.AdditionalAuthenticationProviderProperty[] { + return authModes.reduce< + CfnGraphQLApi.AdditionalAuthenticationProviderProperty[] + >( + (acc, authMode) => [ + ...acc, + { + authenticationType: authMode.authorizationType, + userPoolConfig: + authMode.authorizationType === AuthorizationType.USER_POOL + ? this.formatUserPoolConfig(authMode.userPoolConfig!) + : undefined, + openIdConnectConfig: + authMode.authorizationType === AuthorizationType.OIDC + ? this.formatOpenIdConnectConfig(authMode.openIdConnectConfig!) + : undefined, + }, + ], + [], + ); } } @@ -537,6 +766,30 @@ export class DynamoDbDataSource extends BackedDataSource { } } +/** + * Properties for an AppSync http datasource + */ +export interface HttpDataSourceProps extends BaseDataSourceProps { + /** + * The http endpoint + */ + readonly endpoint: string; +} + +/** + * An AppSync datasource backed by a http endpoint + */ +export class HttpDataSource extends BaseDataSource { + constructor(scope: Construct, id: string, props: HttpDataSourceProps) { + super(scope, id, props, { + httpConfig: { + endpoint: props.endpoint, + }, + type: 'HTTP', + }); + } +} + /** * Properties for an AppSync Lambda datasource */ @@ -772,7 +1025,7 @@ export class Assign { * Renders the assignment as a map element. */ public putInMap(map: string): string { - return `$util.qr($${map}.put("${this.attr}", "${this.arg}"))`; + return `$util.qr($${map}.put("${this.attr}", ${this.arg}))`; } } @@ -843,8 +1096,8 @@ export class PrimaryKey { assignments.push(this.skey.renderAsAssignment()); } return `"key" : { - ${assignments.join(',')} - }`; + ${assignments.join(',')} + }`; } } @@ -878,14 +1131,19 @@ export class AttributeValues { return new AttributeValuesStep(attr, this.container, this.assignments); } + /** + * Renders the variables required for `renderTemplate`. + */ + public renderVariables(): string { + return `#set($input = ${this.container}) + ${this.assignments.map(a => a.putInMap('input')).join('\n')}`; + } + /** * Renders the attribute value assingments to a VTL string. */ public renderTemplate(): string { - return ` - #set($input = ${this.container}) - ${this.assignments.map(a => a.putInMap('input')).join('\n')} - "attributeValues": $util.dynamodb.toMapValuesJson($input)`; + return '"attributeValues": $util.dynamodb.toMapValuesJson($input)'; } } @@ -1002,12 +1260,14 @@ export abstract class MappingTemplate { * @param values the assignment of Mutation values to the table attributes */ public static dynamoDbPutItem(key: PrimaryKey, values: AttributeValues): MappingTemplate { - return this.fromString(`{ - "version" : "2017-02-28", - "operation" : "PutItem", - ${key.renderTemplate()}, - ${values.renderTemplate()} - }`); + return this.fromString(` + ${values.renderVariables()} + { + "version": "2017-02-28", + "operation": "PutItem", + ${key.renderTemplate()}, + ${values.renderTemplate()} + }`); } /** diff --git a/packages/@aws-cdk/aws-appsync/package.json b/packages/@aws-cdk/aws-appsync/package.json index 9cc5bfa71f0e3..ed431e29e7367 100644 --- a/packages/@aws-cdk/aws-appsync/package.json +++ b/packages/@aws-cdk/aws-appsync/package.json @@ -67,6 +67,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", + "jest": "^25.5.4", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-dynamodb.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-dynamodb.test.ts new file mode 100644 index 0000000000000..37e1ecc0ea57b --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/appsync-dynamodb.test.ts @@ -0,0 +1,73 @@ +import '@aws-cdk/assert/jest'; +import { MappingTemplate, PrimaryKey, Values } from '../lib'; + +function joined(str: string): string { + return str.replace(/\s+/g, ''); +} + +describe('DynamoDB Mapping Templates', () => { + test('PutItem projecting all', () => { + const template = MappingTemplate.dynamoDbPutItem( + PrimaryKey.partition('id').is('id'), + Values.projecting(), + ); + + const rendered = joined(template.renderTemplate()); + + expect(rendered).toStrictEqual(joined(` + #set($input = $ctx.args) + { + "version" : "2017-02-28", + "operation" : "PutItem", + "key" : { + "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id) + }, + "attributeValues": $util.dynamodb.toMapValuesJson($input) + }`), + ); + }); + + test('PutItem with invididual attributes', () => { + const template = MappingTemplate.dynamoDbPutItem( + PrimaryKey.partition('id').is('id'), + Values.attribute('val').is('ctx.args.val'), + ); + + const rendered = joined(template.renderTemplate()); + + expect(rendered).toStrictEqual(joined(` + #set($input = {}) + $util.qr($input.put("val", ctx.args.val)) + { + "version" : "2017-02-28", + "operation" : "PutItem", + "key" : { + "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id) + }, + "attributeValues": $util.dynamodb.toMapValuesJson($input) + }`), + ); + }); + + test('PutItem with additional attributes', () => { + const template = MappingTemplate.dynamoDbPutItem( + PrimaryKey.partition('id').is('id'), + Values.projecting().attribute('val').is('ctx.args.val'), + ); + + const rendered = joined(template.renderTemplate()); + + expect(rendered).toStrictEqual(joined(` + #set($input = $ctx.args) + $util.qr($input.put("val", ctx.args.val)) + { + "version" : "2017-02-28", + "operation" : "PutItem", + "key" : { + "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id) + }, + "attributeValues": $util.dynamodb.toMapValuesJson($input) + }`), + ); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json b/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json index f51065c3287c2..e1d4a0ba17a4b 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json @@ -85,28 +85,22 @@ } } }, - "ApiMyAPIKeyApiKeyACDEE2CC": { + "ApiDefaultAPIKeyApiKey74F5313B": { "Type": "AWS::AppSync::ApiKey", "Properties": { "ApiId": { - "Fn::GetAtt": [ - "ApiF70053CD", - "ApiId" - ] + "Fn::GetAtt": ["ApiF70053CD", "ApiId"] }, - "Description": "My API Key" + "Description": "Default API Key created by CDK" } }, "ApiSchema510EECD7": { "Type": "AWS::AppSync::GraphQLSchema", "Properties": { "ApiId": { - "Fn::GetAtt": [ - "ApiF70053CD", - "ApiId" - ] + "Fn::GetAtt": ["ApiF70053CD", "ApiId"] }, - "Definition": "type ServiceVersion {\n version: String!\n}\n\ntype Customer {\n id: String!\n name: String!\n}\n\ninput SaveCustomerInput {\n name: String!\n}\n\ntype Order {\n customer: String!\n order: String!\n}\n\ntype Query {\n getServiceVersion: ServiceVersion\n getCustomers: [Customer]\n getCustomer(id: String): Customer\n getCustomerOrdersEq(customer: String): Order\n getCustomerOrdersLt(customer: String): Order\n getCustomerOrdersLe(customer: String): Order\n getCustomerOrdersGt(customer: String): Order\n getCustomerOrdersGe(customer: String): Order\n getCustomerOrdersFilter(customer: String, order: String): Order\n getCustomerOrdersBetween(customer: String, order1: String, order2: String): Order\n}\n\ninput FirstOrderInput {\n product: String!\n quantity: Int!\n}\n\ntype Mutation {\n addCustomer(customer: SaveCustomerInput!): Customer\n saveCustomer(id: String!, customer: SaveCustomerInput!): Customer\n removeCustomer(id: String!): Customer\n saveCustomerWithFirstOrder(customer: SaveCustomerInput!, order: FirstOrderInput!, referral: String): Order\n}" + "Definition": "type ServiceVersion {\n version: String!\n}\n\ntype Customer {\n id: String!\n name: String!\n}\n\ninput SaveCustomerInput {\n name: String!\n}\n\ntype Order {\n customer: String!\n order: String!\n}\n\ntype Query {\n getServiceVersion: ServiceVersion\n getCustomers: [Customer]\n getCustomer(id: String): Customer\n getCustomerOrdersEq(customer: String): Order\n getCustomerOrdersLt(customer: String): Order\n getCustomerOrdersLe(customer: String): Order\n getCustomerOrdersGt(customer: String): Order\n getCustomerOrdersGe(customer: String): Order\n getCustomerOrdersFilter(customer: String, order: String): Order\n getCustomerOrdersBetween(customer: String, order1: String, order2: String): Order\n}\n\ninput FirstOrderInput {\n product: String!\n quantity: Int!\n}\n\ntype Mutation {\n addCustomer(customer: SaveCustomerInput!): Customer\n saveCustomer(id: String!, customer: SaveCustomerInput!): Customer\n removeCustomer(id: String!): Customer\n saveCustomerWithFirstOrder(customer: SaveCustomerInput!, order: FirstOrderInput!, referral: String): Order\n doPostOnAws: String!\n}\n" } }, "ApiNoneDSB4E6495F": { @@ -286,7 +280,7 @@ "TypeName": "Mutation", "DataSourceName": "Customer", "Kind": "UNIT", - "RequestMappingTemplate": "{\n \"version\" : \"2017-02-28\",\n \"operation\" : \"PutItem\",\n \"key\" : {\n \"id\" : $util.dynamodb.toDynamoDBJson($util.autoId())\n },\n \n #set($input = $ctx.args.customer)\n \n \"attributeValues\": $util.dynamodb.toMapValuesJson($input)\n }", + "RequestMappingTemplate": "\n #set($input = $ctx.args.customer)\n \n {\n \"version\": \"2017-02-28\",\n \"operation\": \"PutItem\",\n \"key\" : {\n \"id\" : $util.dynamodb.toDynamoDBJson($util.autoId())\n },\n \"attributeValues\": $util.dynamodb.toMapValuesJson($input)\n }", "ResponseMappingTemplate": "$util.toJson($ctx.result)" }, "DependsOn": [ @@ -307,7 +301,7 @@ "TypeName": "Mutation", "DataSourceName": "Customer", "Kind": "UNIT", - "RequestMappingTemplate": "{\n \"version\" : \"2017-02-28\",\n \"operation\" : \"PutItem\",\n \"key\" : {\n \"id\" : $util.dynamodb.toDynamoDBJson($ctx.args.id)\n },\n \n #set($input = $ctx.args.customer)\n \n \"attributeValues\": $util.dynamodb.toMapValuesJson($input)\n }", + "RequestMappingTemplate": "\n #set($input = $ctx.args.customer)\n \n {\n \"version\": \"2017-02-28\",\n \"operation\": \"PutItem\",\n \"key\" : {\n \"id\" : $util.dynamodb.toDynamoDBJson($ctx.args.id)\n },\n \"attributeValues\": $util.dynamodb.toMapValuesJson($input)\n }", "ResponseMappingTemplate": "$util.toJson($ctx.result)" }, "DependsOn": [ @@ -328,7 +322,7 @@ "TypeName": "Mutation", "DataSourceName": "Customer", "Kind": "UNIT", - "RequestMappingTemplate": "{\n \"version\" : \"2017-02-28\",\n \"operation\" : \"PutItem\",\n \"key\" : {\n \"order\" : $util.dynamodb.toDynamoDBJson($util.autoId()),\"customer\" : $util.dynamodb.toDynamoDBJson($ctx.args.customer.id)\n },\n \n #set($input = $ctx.args.order)\n $util.qr($input.put(\"referral\", \"referral\"))\n \"attributeValues\": $util.dynamodb.toMapValuesJson($input)\n }", + "RequestMappingTemplate": "\n #set($input = $ctx.args.order)\n $util.qr($input.put(\"referral\", referral))\n {\n \"version\": \"2017-02-28\",\n \"operation\": \"PutItem\",\n \"key\" : {\n \"order\" : $util.dynamodb.toDynamoDBJson($util.autoId()),\"customer\" : $util.dynamodb.toDynamoDBJson($ctx.args.customer.id)\n },\n \"attributeValues\": $util.dynamodb.toMapValuesJson($input)\n }", "ResponseMappingTemplate": "$util.toJson($ctx.result)" }, "DependsOn": [ @@ -591,6 +585,67 @@ "ApiSchema510EECD7" ] }, + "ApihttpDSServiceRole8B5C9457": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "appsync.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ApihttpDS91F12990": { + "Type": "AWS::AppSync::DataSource", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "ApiF70053CD", + "ApiId" + ] + }, + "Name": "http", + "Type": "HTTP", + "Description": "The http data source", + "HttpConfig": { + "Endpoint": "https://aws.amazon.com/" + }, + "ServiceRoleArn": { + "Fn::GetAtt": [ + "ApihttpDSServiceRole8B5C9457", + "Arn" + ] + } + } + }, + "ApihttpDSMutationdoPostOnAwsResolverA9027953": { + "Type": "AWS::AppSync::Resolver", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "ApiF70053CD", + "ApiId" + ] + }, + "FieldName": "doPostOnAws", + "TypeName": "Mutation", + "DataSourceName": "http", + "Kind": "UNIT", + "RequestMappingTemplate": "{\n \"version\": \"2018-05-29\",\n \"method\": \"POST\",\n # if full path is https://api.xxxxxxxxx.com/posts then resourcePath would be /posts\n \"resourcePath\": \"/path/123\",\n \"params\":{\n \"body\": $util.toJson($ctx.args),\n \"headers\":{\n \"Content-Type\": \"application/json\",\n \"Authorization\": \"$ctx.request.headers.Authorization\"\n }\n }\n }", + "ResponseMappingTemplate": "\n ## Raise a GraphQL field error in case of a datasource invocation error\n #if($ctx.error)\n $util.error($ctx.error.message, $ctx.error.type)\n #end\n ## if the response status code is not 200, then return an error. Else return the body **\n #if($ctx.result.statusCode == 200)\n ## If response is 200, return the body.\n $ctx.result.body\n #else\n ## If response is not 200, append the response to error block.\n $utils.appendError($ctx.result.body, \"$ctx.result.statusCode\")\n #end\n " + }, + "DependsOn": [ + "ApihttpDS91F12990", + "ApiSchema510EECD7" + ] + }, "CustomerTable260DCC08": { "Type": "AWS::DynamoDB::Table", "Properties": { @@ -640,4 +695,4 @@ "DeletionPolicy": "Delete" } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts b/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts index a04b33bcdb000..fb2be9eeac531 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts @@ -2,7 +2,15 @@ import { UserPool } from '@aws-cdk/aws-cognito'; import { AttributeType, BillingMode, Table } from '@aws-cdk/aws-dynamodb'; import { App, RemovalPolicy, Stack } from '@aws-cdk/core'; import { join } from 'path'; -import { GraphQLApi, KeyCondition, MappingTemplate, PrimaryKey, UserPoolDefaultAction, Values } from '../lib'; +import { + AuthorizationType, + GraphQLApi, + KeyCondition, + MappingTemplate, + PrimaryKey, + UserPoolDefaultAction, + Values, +} from '../lib'; const app = new App(); const stack = new Stack(app, 'aws-appsync-integ'); @@ -16,14 +24,15 @@ const api = new GraphQLApi(stack, 'Api', { schemaDefinitionFile: join(__dirname, 'schema.graphql'), authorizationConfig: { defaultAuthorization: { - userPool, - defaultAction: UserPoolDefaultAction.ALLOW, + authorizationType: AuthorizationType.USER_POOL, + userPoolConfig: { + userPool, + defaultAction: UserPoolDefaultAction.ALLOW, + }, }, additionalAuthorizationModes: [ { - apiKeyDesc: 'My API Key', - // Can't specify a date because it will inevitably be in the past. - // expires: '2019-02-05T12:00:00Z', + authorizationType: AuthorizationType.API_KEY, }, ], }, @@ -139,4 +148,38 @@ orderDS.createResolver({ responseMappingTemplate: MappingTemplate.dynamoDbResultList(), }); -app.synth(); \ No newline at end of file +const httpDS = api.addHttpDataSource('http', 'The http data source', 'https://aws.amazon.com/'); + +httpDS.createResolver({ + typeName: 'Mutation', + fieldName: 'doPostOnAws', + requestMappingTemplate: MappingTemplate.fromString(`{ + "version": "2018-05-29", + "method": "POST", + # if full path is https://api.xxxxxxxxx.com/posts then resourcePath would be /posts + "resourcePath": "/path/123", + "params":{ + "body": $util.toJson($ctx.args), + "headers":{ + "Content-Type": "application/json", + "Authorization": "$ctx.request.headers.Authorization" + } + } + }`), + responseMappingTemplate: MappingTemplate.fromString(` + ## Raise a GraphQL field error in case of a datasource invocation error + #if($ctx.error) + $util.error($ctx.error.message, $ctx.error.type) + #end + ## if the response status code is not 200, then return an error. Else return the body ** + #if($ctx.result.statusCode == 200) + ## If response is 200, return the body. + $ctx.result.body + #else + ## If response is not 200, append the response to error block. + $utils.appendError($ctx.result.body, "$ctx.result.statusCode") + #end + `), +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-appsync/test/schema.graphql b/packages/@aws-cdk/aws-appsync/test/schema.graphql index 5f82e9279ccbc..24af9a154ec59 100644 --- a/packages/@aws-cdk/aws-appsync/test/schema.graphql +++ b/packages/@aws-cdk/aws-appsync/test/schema.graphql @@ -39,4 +39,5 @@ type Mutation { saveCustomer(id: String!, customer: SaveCustomerInput!): Customer removeCustomer(id: String!): Customer saveCustomerWithFirstOrder(customer: SaveCustomerInput!, order: FirstOrderInput!, referral: String): Order -} \ No newline at end of file + doPostOnAws: String! +} diff --git a/packages/@aws-cdk/aws-autoscaling-common/package.json b/packages/@aws-cdk/aws-autoscaling-common/package.json index 4c31c377051ca..3cfba9f722db7 100644 --- a/packages/@aws-cdk/aws-autoscaling-common/package.json +++ b/packages/@aws-cdk/aws-autoscaling-common/package.json @@ -62,7 +62,7 @@ "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "fast-check": "^1.24.2", + "fast-check": "^1.25.0", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-autoscaling/README.md b/packages/@aws-cdk/aws-autoscaling/README.md index 260d8d0e0b693..ce55c411671f7 100644 --- a/packages/@aws-cdk/aws-autoscaling/README.md +++ b/packages/@aws-cdk/aws-autoscaling/README.md @@ -224,6 +224,11 @@ To enable the max instance lifetime support, specify `maxInstanceLifetime` prope for the `AutoscalingGroup` resource. The value must be between 7 and 365 days(inclusive). To clear a previously set value, just leave this property undefinied. +### Instance Monitoring + +To disable detailed instance monitoring, specify `instanceMonitoring` property +for the `AutoscalingGroup` resource as `Monitoring.BASIC`. Otherwise detailed monitoring +will be enabled. ### Future work diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index 52ca56748b0f3..31483d53d40a6 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -21,6 +21,21 @@ import { BlockDevice, BlockDeviceVolume, EbsDeviceVolumeType } from './volume'; */ const NAME_TAG: string = 'Name'; +/** + * The monitoring mode for instances launched in an autoscaling group + */ +export enum Monitoring { + /** + * Generates metrics every 5 minutes + */ + BASIC, + + /** + * Generates metrics every minute + */ + DETAILED, +} + /** * Basic properties of an AutoScalingGroup, except the exact machines to run and where they should run * @@ -207,6 +222,18 @@ export interface CommonAutoScalingGroupProps { * @default none */ readonly maxInstanceLifetime?: Duration; + + /** + * Controls whether instances in this group are launched with detailed or basic monitoring. + * + * When detailed monitoring is enabled, Amazon CloudWatch generates metrics every minute and your account + * is charged a fee. When you disable detailed monitoring, CloudWatch generates metrics every 5 minutes. + * + * @see https://docs.aws.amazon.com/autoscaling/latest/userguide/as-instance-monitoring.html#enable-as-instance-metrics + * + * @default - Monitoring.DETAILED + */ + readonly instanceMonitoring?: Monitoring; } /** @@ -477,6 +504,7 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements imageId: imageConfig.imageId, keyName: props.keyName, instanceType: props.instanceType.toString(), + instanceMonitoring: (props.instanceMonitoring !== undefined ? (props.instanceMonitoring === Monitoring.DETAILED) : undefined), securityGroups: securityGroupsToken, iamInstanceProfile: iamProfile.ref, userData: userDataToken, diff --git a/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts index c17c90b17f06e..faea5fe7cd8f9 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts @@ -831,6 +831,45 @@ export = { test.done(); }, + 'can configure instance monitoring'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = mockVpc(stack); + + // WHEN + new autoscaling.AutoScalingGroup(stack, 'MyStack', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), + machineImage: new ec2.AmazonLinuxImage(), + vpc, + instanceMonitoring: autoscaling.Monitoring.BASIC, + }); + + // THEN + expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + InstanceMonitoring: false, + })); + test.done(); + }, + + 'instance monitoring defaults to absent'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = mockVpc(stack); + + // WHEN + new autoscaling.AutoScalingGroup(stack, 'MyStack', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), + machineImage: new ec2.AmazonLinuxImage(), + vpc, + }); + + // THEN + expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + InstanceMonitoring: ABSENT, + })); + test.done(); + }, + 'throws if ephemeral volumeIndex < 0'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts index 3b3f39d64eb4c..a7c18b6b87609 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts @@ -345,7 +345,7 @@ export class Trail extends Resource { * @default false */ public logAllLambdaDataEvents(options: AddEventSelectorOptions = {}) { - return this.addEventSelector(DataResourceType.LAMBDA_FUNCTION, [ 'arn:aws:lambda' ], options); + return this.addEventSelector(DataResourceType.LAMBDA_FUNCTION, [ `arn:${this.stack.partition}:lambda` ], options); } /** @@ -372,7 +372,7 @@ export class Trail extends Resource { * @default false */ public logAllS3DataEvents(options: AddEventSelectorOptions = {}) { - return this.addEventSelector(DataResourceType.S3_OBJECT, [ 'arn:aws:s3:::' ], options); + return this.addEventSelector(DataResourceType.S3_OBJECT, [ `arn:${this.stack.partition}:s3:::` ], options); } /** diff --git a/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts b/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts index 50c2b766bb4c3..b78f6a5f5a741 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts @@ -257,7 +257,20 @@ describe('cloudtrail', () => { { DataResources: [{ Type: 'AWS::S3::Object', - Values: [ 'arn:aws:s3:::' ], + Values: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::', + ], + ], + }, + ], }], IncludeManagementEvents: ABSENT, ReadWriteType: ABSENT, @@ -331,7 +344,20 @@ describe('cloudtrail', () => { { DataResources: [{ Type: 'AWS::S3::Object', - Values: [ 'arn:aws:s3:::' ], + Values: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::', + ], + ], + }, + ], }], IncludeManagementEvents: false, ReadWriteType: 'ReadOnly', @@ -391,7 +417,20 @@ describe('cloudtrail', () => { { DataResources: [{ Type: 'AWS::Lambda::Function', - Values: [ 'arn:aws:lambda' ], + Values: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':lambda', + ], + ], + }, + ], }], }, ], diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json index 9057e8c7ae31b..d3044cf9f313c 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json @@ -231,7 +231,7 @@ }, "/", { - "Ref": "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3Bucket5148F39F" + "Ref": "AssetParametersb73a9afb7e79ff941de53cc25f57889657aff1b3c3ad024656b62644e6b25ce2S3BucketC9264D73" }, "/", { @@ -241,7 +241,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3VersionKey0618C4C3" + "Ref": "AssetParametersb73a9afb7e79ff941de53cc25f57889657aff1b3c3ad024656b62644e6b25ce2S3VersionKey992034D6" } ] } @@ -254,7 +254,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3VersionKey0618C4C3" + "Ref": "AssetParametersb73a9afb7e79ff941de53cc25f57889657aff1b3c3ad024656b62644e6b25ce2S3VersionKey992034D6" } ] } @@ -270,11 +270,11 @@ "referencetocdkdynamodbglobal20191121AssetParameters012c6b101abc4ea1f510921af61a3e08e05f30f84d7b35c40ca4adb1ace60746S3VersionKey8D3D9B9ARef": { "Ref": "AssetParameters012c6b101abc4ea1f510921af61a3e08e05f30f84d7b35c40ca4adb1ace60746S3VersionKey1C286880" }, - "referencetocdkdynamodbglobal20191121AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket6627F4A7Ref": { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C" + "referencetocdkdynamodbglobal20191121AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3BucketF12BD931Ref": { + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket222C40AB" }, - "referencetocdkdynamodbglobal20191121AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyD04C038CRef": { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "referencetocdkdynamodbglobal20191121AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey4CB468E4Ref": { + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } } } @@ -293,29 +293,29 @@ "Type": "String", "Description": "Artifact hash for asset \"012c6b101abc4ea1f510921af61a3e08e05f30f84d7b35c40ca4adb1ace60746\"" }, - "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C": { + "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket222C40AB": { "Type": "String", - "Description": "S3 bucket for asset \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" + "Description": "S3 bucket for asset \"164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441\"" }, - "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB": { + "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802": { "Type": "String", - "Description": "S3 key for asset version \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" + "Description": "S3 key for asset version \"164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441\"" }, - "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1ArtifactHash251241BC": { + "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441ArtifactHash88D60D5A": { "Type": "String", - "Description": "Artifact hash for asset \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" + "Description": "Artifact hash for asset \"164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441\"" }, - "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3Bucket5148F39F": { + "AssetParametersb73a9afb7e79ff941de53cc25f57889657aff1b3c3ad024656b62644e6b25ce2S3BucketC9264D73": { "Type": "String", - "Description": "S3 bucket for asset \"ffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947\"" + "Description": "S3 bucket for asset \"b73a9afb7e79ff941de53cc25f57889657aff1b3c3ad024656b62644e6b25ce2\"" }, - "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3VersionKey0618C4C3": { + "AssetParametersb73a9afb7e79ff941de53cc25f57889657aff1b3c3ad024656b62644e6b25ce2S3VersionKey992034D6": { "Type": "String", - "Description": "S3 key for asset version \"ffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947\"" + "Description": "S3 key for asset version \"b73a9afb7e79ff941de53cc25f57889657aff1b3c3ad024656b62644e6b25ce2\"" }, - "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947ArtifactHashBF6B619B": { + "AssetParametersb73a9afb7e79ff941de53cc25f57889657aff1b3c3ad024656b62644e6b25ce2ArtifactHash580BBBA9": { "Type": "String", - "Description": "Artifact hash for asset \"ffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947\"" + "Description": "Artifact hash for asset \"b73a9afb7e79ff941de53cc25f57889657aff1b3c3ad024656b62644e6b25ce2\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts index 450cd9fc52e8a..a49ff09fe6634 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts @@ -297,6 +297,7 @@ export class InterfaceVpcEndpointAwsService implements IInterfaceVpcEndpointServ public static readonly STORAGE_GATEWAY = new InterfaceVpcEndpointAwsService('storagegateway'); public static readonly REKOGNITION = new InterfaceVpcEndpointAwsService('rekognition'); public static readonly REKOGNITION_FIPS = new InterfaceVpcEndpointAwsService('rekognition-fips'); + public static readonly STEP_FUNCTIONS = new InterfaceVpcEndpointAwsService('states'); /** * The name of the service. diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index 7bed5e57ba6f5..458f2d1b4bc2e 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -231,6 +231,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.STORAGE_GATEWAY", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.REKOGNITION", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.REKOGNITION_FIPS", + "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.STEP_FUNCTIONS", "docs-public-apis:@aws-cdk/aws-ec2.Port.toString", "docs-public-apis:@aws-cdk/aws-ec2.PrivateSubnet.fromPrivateSubnetAttributes", "docs-public-apis:@aws-cdk/aws-ec2.PublicSubnet.fromPublicSubnetAttributes", diff --git a/packages/@aws-cdk/aws-eks/README.md b/packages/@aws-cdk/aws-eks/README.md index d77259e7fb3fb..fb70871bfe8e5 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -386,6 +386,33 @@ A convenience method for mapping a role to the `system:masters` group is also av cluster.awsAuth.addMastersRole(role) ``` +### Cluster Security Group + +When you create an Amazon EKS cluster, a +[cluster security group](https://docs.aws.amazon.com/eks/latest/userguide/sec-group-reqs.html) +is automatically created as well. This security group is designed to allow +all traffic from the control plane and managed node groups to flow freely +between each other. + +The ID for that security group can be retrieved after creating the cluster. + +```ts +const clusterSecurityGroupId = cluster.clusterSecurityGroupId; +``` + +### Cluster Encryption Configuration + +When you create an Amazon EKS cluster, envelope encryption of +Kubernetes secrets using the AWS Key Management Service (AWS KMS) can be enabled. The documentation +on [creating a cluster](https://docs.aws.amazon.com/eks/latest/userguide/create-cluster.html) +can provide more details about the customer master key (CMK) that can be used for the encryption. + +The Amazon Resource Name (ARN) for that CMK can be retrieved. + +```ts +const clusterEncryptionConfigKeyArn = cluster.clusterEncryptionConfigKeyArn; +``` + ### Node ssh Access If you want to be able to SSH into your worker nodes, you must already @@ -486,7 +513,8 @@ cluster.addChart('NginxIngress', { }); ``` -Helm charts will be installed and updated using `helm upgrade --install`. +Helm charts will be installed and updated using `helm upgrade --install`, where a few parameters +are being passed down (such as `repo`, `values`, `version`, `namespace`, `wait`, `timeout`, etc). This means that if the chart is added to CDK with the same release name, it will try to update the chart in the cluster. The chart will exists as CloudFormation resource. diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts index 8733463cce31b..f20ddd85c5704 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts @@ -131,11 +131,21 @@ export class ClusterResourceHandler extends ResourceHandler { } if (updates.updateLogging || updates.updateAccess) { - const updateResponse = await this.eks.updateClusterConfig({ + const config: aws.EKS.UpdateClusterConfigRequest = { name: this.clusterName, logging: this.newProps.logging, - resourcesVpcConfig: this.newProps.resourcesVpcConfig, - }); + }; + if (updates.updateAccess) { + // Updating the cluster with securityGroupIds and subnetIds (as specified in the warning here: + // https://awscli.amazonaws.com/v2/documentation/api/latest/reference/eks/update-cluster-config.html) + // will fail, therefore we take only the access fields explicitly + config.resourcesVpcConfig = { + endpointPrivateAccess: this.newProps.resourcesVpcConfig.endpointPrivateAccess, + endpointPublicAccess: this.newProps.resourcesVpcConfig.endpointPublicAccess, + publicAccessCidrs: this.newProps.resourcesVpcConfig.publicAccessCidrs, + }; + } + const updateResponse = await this.eks.updateClusterConfig(config); return { EksUpdateId: updateResponse.update?.id }; } @@ -197,9 +207,20 @@ export class ClusterResourceHandler extends ResourceHandler { Name: cluster.name, Endpoint: cluster.endpoint, Arn: cluster.arn, - CertificateAuthorityData: cluster.certificateAuthority?.data, - OpenIdConnectIssuerUrl: cluster.identity?.oidc?.issuer, - OpenIdConnectIssuer: cluster.identity?.oidc?.issuer?.substring(8), // Strips off https:// from the issuer url + + // IMPORTANT: CFN expects that attributes will *always* have values, + // so return an empty string in case the value is not defined. + // Otherwise, CFN will throw with `Vendor response doesn't contain + // XXXX key`. + + CertificateAuthorityData: cluster.certificateAuthority?.data ?? '', + ClusterSecurityGroupId: cluster.resourcesVpcConfig?.clusterSecurityGroupId ?? '', + OpenIdConnectIssuerUrl: cluster.identity?.oidc?.issuer ?? '', + OpenIdConnectIssuer: cluster.identity?.oidc?.issuer?.substring(8) ?? '', // Strips off https:// from the issuer url + + // We can safely return the first item from encryption configuration array, because it has a limit of 1 item + // https://docs.aws.amazon.com/eks/latest/APIReference/API_CreateCluster.html#AmazonEKS-CreateCluster-request-encryptionConfig + EncryptionConfigKeyArn: cluster.encryptionConfig?.shift()?.provider?.keyArn ?? '', }, }; } diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts index 52557776c97e8..18dfaa4716752 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts @@ -18,6 +18,8 @@ export class ClusterResource extends Construct { public readonly attrEndpoint: string; public readonly attrArn: string; public readonly attrCertificateAuthorityData: string; + public readonly attrClusterSecurityGroupId: string; + public readonly attrEncryptionConfigKeyArn: string; public readonly attrOpenIdConnectIssuerUrl: string; public readonly attrOpenIdConnectIssuer: string; public readonly ref: string; @@ -117,6 +119,13 @@ export class ClusterResource extends Construct { properties: { Config: props, AssumeRoleArn: this.creationRole.roleArn, + + // IMPORTANT: increment this number when you add new attributes to the + // resource. Otherwise, CloudFormation will error with "Vendor response + // doesn't contain XXX key in object" (see #8276) by incrementing this + // number, you will effectively cause a "no-op update" to the cluster + // which will return the new set of attribute. + AttributesRevision: 2, }, }); @@ -126,6 +135,8 @@ export class ClusterResource extends Construct { this.attrEndpoint = Token.asString(resource.getAtt('Endpoint')); this.attrArn = Token.asString(resource.getAtt('Arn')); this.attrCertificateAuthorityData = Token.asString(resource.getAtt('CertificateAuthorityData')); + this.attrClusterSecurityGroupId = Token.asString(resource.getAtt('ClusterSecurityGroupId')); + this.attrEncryptionConfigKeyArn = Token.asString(resource.getAtt('EncryptionConfigKeyArn')); this.attrOpenIdConnectIssuerUrl = Token.asString(resource.getAtt('OpenIdConnectIssuerUrl')); this.attrOpenIdConnectIssuer = Token.asString(resource.getAtt('OpenIdConnectIssuer')); } diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index d1fb2bf60352b..6bdd9526044ee 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -52,6 +52,18 @@ export interface ICluster extends IResource, ec2.IConnectable { * @attribute */ readonly clusterCertificateAuthorityData: string; + + /** + * The cluster security group that was created by Amazon EKS for the cluster. + * @attribute + */ + readonly clusterSecurityGroupId: string; + + /** + * Amazon Resource Name (ARN) or alias of the customer master key (CMK). + * @attribute + */ + readonly clusterEncryptionConfigKeyArn: string; } /** @@ -84,6 +96,16 @@ export interface ClusterAttributes { */ readonly clusterCertificateAuthorityData: string; + /** + * The cluster security group that was created by Amazon EKS for the cluster. + */ + readonly clusterSecurityGroupId: string; + + /** + * Amazon Resource Name (ARN) or alias of the customer master key (CMK). + */ + readonly clusterEncryptionConfigKeyArn: string; + /** * The security groups associated with this cluster. */ @@ -299,6 +321,16 @@ export class Cluster extends Resource implements ICluster { */ public readonly clusterCertificateAuthorityData: string; + /** + * The cluster security group that was created by Amazon EKS for the cluster. + */ + public readonly clusterSecurityGroupId: string; + + /** + * Amazon Resource Name (ARN) or alias of the customer master key (CMK). + */ + public readonly clusterEncryptionConfigKeyArn: string; + /** * Manages connection rules (Security Group Rules) for the cluster * @@ -331,6 +363,12 @@ export class Cluster extends Resource implements ICluster { */ public readonly defaultNodegroup?: Nodegroup; + /** + * If the cluster has one (or more) FargateProfiles associated, this array + * will hold a reference to each. + */ + private readonly _fargateProfiles: FargateProfile[] = []; + /** * If this cluster is kubectl-enabled, returns the `ClusterResource` object * that manages it. If this cluster is not kubectl-enabled (i.e. uses the @@ -414,6 +452,8 @@ export class Cluster extends Resource implements ICluster { this.clusterEndpoint = resource.attrEndpoint; this.clusterCertificateAuthorityData = resource.attrCertificateAuthorityData; + this.clusterSecurityGroupId = resource.attrClusterSecurityGroupId; + this.clusterEncryptionConfigKeyArn = resource.attrEncryptionConfigKeyArn; const updateConfigCommandPrefix = `aws eks update-kubeconfig --name ${this.clusterName}`; const getTokenCommandPrefix = `aws eks get-token --cluster-name ${this.clusterName}`; @@ -757,6 +797,18 @@ export class Cluster extends Resource implements ICluster { return this.stack.node.tryFindChild(uid) as KubectlProvider || new KubectlProvider(this.stack, uid); } + /** + * Internal API used by `FargateProfile` to keep inventory of Fargate profiles associated with + * this cluster, for the sake of ensuring the profiles are created sequentially. + * + * @returns the list of FargateProfiles attached to this cluster, including the one just attached. + * @internal + */ + public _attachFargateProfile(fargateProfile: FargateProfile): FargateProfile[] { + this._fargateProfiles.push(fargateProfile); + return this._fargateProfiles; + } + /** * Installs the AWS spot instance interrupt handler on the cluster if it's not * already added. @@ -990,6 +1042,8 @@ export interface AutoScalingGroupOptions { class ImportedCluster extends Resource implements ICluster { public readonly vpc: ec2.IVpc; public readonly clusterCertificateAuthorityData: string; + public readonly clusterSecurityGroupId: string; + public readonly clusterEncryptionConfigKeyArn: string; public readonly clusterName: string; public readonly clusterArn: string; public readonly clusterEndpoint: string; @@ -1003,6 +1057,8 @@ class ImportedCluster extends Resource implements ICluster { this.clusterEndpoint = props.clusterEndpoint; this.clusterArn = props.clusterArn; this.clusterCertificateAuthorityData = props.clusterCertificateAuthorityData; + this.clusterSecurityGroupId = props.clusterSecurityGroupId; + this.clusterEncryptionConfigKeyArn = props.clusterEncryptionConfigKeyArn; let i = 1; for (const sgProps of props.securityGroups) { diff --git a/packages/@aws-cdk/aws-eks/lib/fargate-profile.ts b/packages/@aws-cdk/aws-eks/lib/fargate-profile.ts index b9b45bb1d8ebe..bd245f8c9a4b5 100644 --- a/packages/@aws-cdk/aws-eks/lib/fargate-profile.ts +++ b/packages/@aws-cdk/aws-eks/lib/fargate-profile.ts @@ -180,6 +180,14 @@ export class FargateProfile extends Construct implements ITaggable { this.fargateProfileArn = resource.getAttString('fargateProfileArn'); this.fargateProfileName = resource.ref; + // Fargate profiles must be created sequentially. If other profile(s) already + // exist on the same cluster, create a dependency to force sequential creation. + const clusterFargateProfiles = props.cluster._attachFargateProfile(this); + if (clusterFargateProfiles.length > 1) { + const previousProfile = clusterFargateProfiles[clusterFargateProfiles.length - 2]; + resource.node.addDependency(previousProfile); + } + // map the fargate pod execution role to the relevant groups in rbac // see https://github.com/aws/aws-cdk/issues/7981 props.cluster.awsAuth.addRoleMapping(role, { diff --git a/packages/@aws-cdk/aws-eks/lib/helm-chart.ts b/packages/@aws-cdk/aws-eks/lib/helm-chart.ts index 59cbc0f3e7aa0..f3c6141f5cd0a 100644 --- a/packages/@aws-cdk/aws-eks/lib/helm-chart.ts +++ b/packages/@aws-cdk/aws-eks/lib/helm-chart.ts @@ -1,4 +1,4 @@ -import { Construct, CustomResource, Stack } from '@aws-cdk/core'; +import { Construct, CustomResource, Duration, Stack } from '@aws-cdk/core'; import { Cluster } from './cluster'; /** @@ -47,6 +47,12 @@ export interface HelmChartOptions { * @default - Helm will not wait before marking release as successful */ readonly wait?: boolean; + + /** + * Amount of time to wait for any individual Kubernetes operation. Maximum 15 minutes. + * @default Duration.minutes(5) + */ + readonly timeout?: Duration; } /** @@ -68,7 +74,7 @@ export interface HelmChartProps extends HelmChartOptions { */ export class HelmChart extends Construct { /** - * The CloudFormation reosurce type. + * The CloudFormation resource type. */ public static readonly RESOURCE_TYPE = 'Custom::AWSCDK-EKS-HelmChart'; @@ -79,6 +85,11 @@ export class HelmChart extends Construct { const provider = props.cluster._kubectlProvider; + const timeout = props.timeout?.toSeconds(); + if (timeout && timeout > 900) { + throw new Error('Helm chart timeout cannot be higher than 15 minutes.'); + } + new CustomResource(this, 'Resource', { serviceToken: provider.serviceToken, resourceType: HelmChart.RESOURCE_TYPE, @@ -89,6 +100,7 @@ export class HelmChart extends Construct { Chart: props.chart, Version: props.version, Wait: props.wait || false, + Timeout: timeout, Values: (props.values ? stack.toJsonString(props.values) : undefined), Namespace: props.namespace || 'default', Repository: props.repository, diff --git a/packages/@aws-cdk/aws-eks/lib/kubectl-handler/helm/__init__.py b/packages/@aws-cdk/aws-eks/lib/kubectl-handler/helm/__init__.py index 05d0fbdaba614..57ea65a2fa3b7 100644 --- a/packages/@aws-cdk/aws-eks/lib/kubectl-handler/helm/__init__.py +++ b/packages/@aws-cdk/aws-eks/lib/kubectl-handler/helm/__init__.py @@ -25,6 +25,7 @@ def helm_handler(event, context): chart = props['Chart'] version = props.get('Version', None) wait = props.get('Wait', False) + timeout = props.get('Timeout', None) namespace = props.get('Namespace', None) repository = props.get('Repository', None) values_text = props.get('Values', None) @@ -45,14 +46,14 @@ def helm_handler(event, context): f.write(json.dumps(values, indent=2)) if request_type == 'Create' or request_type == 'Update': - helm('upgrade', release, chart, repository, values_file, namespace, version) + helm('upgrade', release, chart, repository, values_file, namespace, version, wait, timeout) elif request_type == "Delete": try: - helm('uninstall', release, namespace=namespace) + helm('uninstall', release, namespace=namespace, timeout=timeout) except Exception as e: logger.info("delete error: %s" % e) -def helm(verb, release, chart = None, repo = None, file = None, namespace = None, version = None, wait = False): +def helm(verb, release, chart = None, repo = None, file = None, namespace = None, version = None, wait = False, timeout = None): import subprocess cmnd = ['helm', verb, release] @@ -70,6 +71,8 @@ def helm(verb, release, chart = None, repo = None, file = None, namespace = None cmnd.extend(['--namespace', namespace]) if wait: cmnd.append('--wait') + if not timeout is None: + cmnd.extend(['--timeout', timeout]) cmnd.extend(['--kubeconfig', kubeconfig]) retry = 3 diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index 3fc137fce439f..9cb02bc5759d1 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -98,13 +98,7 @@ }, "awslint": { "exclude": [ - "resource-attribute:@aws-cdk/aws-eks.FargateCluster.clusterSecurityGroupId", - "resource-attribute:@aws-cdk/aws-eks.FargateCluster.clusterEncryptionConfigKeyArn", - "resource-attribute:@aws-cdk/aws-eks.Cluster.clusterSecurityGroupId", - "resource-attribute:@aws-cdk/aws-eks.Cluster.clusterEncryptionConfigKeyArn", - "props-no-arn-refs:@aws-cdk/aws-eks.ClusterProps.outputMastersRoleArn", - "resource-attribute:@aws-cdk/aws-eks.Cluster.clusterSecurityGroupId", - "resource-attribute:@aws-cdk/aws-eks.Cluster.clusterSecurityGroupId" + "props-no-arn-refs:@aws-cdk/aws-eks.ClusterProps.outputMastersRoleArn" ] }, "stability": "experimental", 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 7a24571d092ff..3196329daeaf2 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 @@ -888,7 +888,8 @@ "ClusterCreationRole360249B6", "Arn" ] - } + }, + "AttributesRevision": 2 }, "DependsOn": [ "ClusterCreationRoleDefaultPolicyE8BDFC7B", @@ -2355,7 +2356,7 @@ }, "/", { - "Ref": "AssetParameters7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6S3BucketB18DC500" + "Ref": "AssetParametersfdca05152ae8546b816681ea8e00f45f12df47f6988add4facaf1f8972995b7aS3BucketDC230AE0" }, "/", { @@ -2365,7 +2366,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6S3VersionKeyBE7DFF7A" + "Ref": "AssetParametersfdca05152ae8546b816681ea8e00f45f12df47f6988add4facaf1f8972995b7aS3VersionKey2373CDCB" } ] } @@ -2378,7 +2379,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6S3VersionKeyBE7DFF7A" + "Ref": "AssetParametersfdca05152ae8546b816681ea8e00f45f12df47f6988add4facaf1f8972995b7aS3VersionKey2373CDCB" } ] } @@ -2388,17 +2389,17 @@ ] }, "Parameters": { - "referencetoawscdkeksclustertestAssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896S3Bucket35BE45A3Ref": { - "Ref": "AssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896S3Bucket221B7FEE" + "referencetoawscdkeksclustertestAssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cS3Bucket60058D6ARef": { + "Ref": "AssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cS3Bucket7F8D74FE" }, - "referencetoawscdkeksclustertestAssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896S3VersionKey60905A80Ref": { - "Ref": "AssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896S3VersionKeyA8C9A018" + "referencetoawscdkeksclustertestAssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cS3VersionKey42E00C5ARef": { + "Ref": "AssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cS3VersionKey1DF2734D" }, - "referencetoawscdkeksclustertestAssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3BucketC7CBF350Ref": { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C" + "referencetoawscdkeksclustertestAssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket740C4561Ref": { + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket222C40AB" }, - "referencetoawscdkeksclustertestAssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKey7E2BE411Ref": { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "referencetoawscdkeksclustertestAssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey3B484C19Ref": { + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } } } @@ -2416,7 +2417,7 @@ }, "/", { - "Ref": "AssetParameters36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbcS3Bucket2D824DEF" + "Ref": "AssetParameters07ba0071b1bf179d8ece256a71220acd174ccb76b36702c2b9b9ecb8004cb608S3Bucket721C96A9" }, "/", { @@ -2426,7 +2427,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbcS3VersionKey45D8E8E4" + "Ref": "AssetParameters07ba0071b1bf179d8ece256a71220acd174ccb76b36702c2b9b9ecb8004cb608S3VersionKey52A1C30C" } ] } @@ -2439,7 +2440,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbcS3VersionKey45D8E8E4" + "Ref": "AssetParameters07ba0071b1bf179d8ece256a71220acd174ccb76b36702c2b9b9ecb8004cb608S3VersionKey52A1C30C" } ] } @@ -2449,17 +2450,17 @@ ] }, "Parameters": { - "referencetoawscdkeksclustertestAssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbS3Bucket6A8A7186Ref": { - "Ref": "AssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbS3Bucket0C3A00C2" + "referencetoawscdkeksclustertestAssetParametersca6f286e0d135e22cfefc133659e2f2fe139a4b46b8eef5b8e197606625c9af9S3Bucket973804E9Ref": { + "Ref": "AssetParametersca6f286e0d135e22cfefc133659e2f2fe139a4b46b8eef5b8e197606625c9af9S3BucketC1533EC8" }, - "referencetoawscdkeksclustertestAssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbS3VersionKeyA18C5C39Ref": { - "Ref": "AssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbS3VersionKeyBED95764" + "referencetoawscdkeksclustertestAssetParametersca6f286e0d135e22cfefc133659e2f2fe139a4b46b8eef5b8e197606625c9af9S3VersionKey2F733777Ref": { + "Ref": "AssetParametersca6f286e0d135e22cfefc133659e2f2fe139a4b46b8eef5b8e197606625c9af9S3VersionKey2C834492" }, - "referencetoawscdkeksclustertestAssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3BucketC7CBF350Ref": { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C" + "referencetoawscdkeksclustertestAssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket740C4561Ref": { + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket222C40AB" }, - "referencetoawscdkeksclustertestAssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKey7E2BE411Ref": { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "referencetoawscdkeksclustertestAssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey3B484C19Ref": { + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } } } @@ -2707,6 +2708,22 @@ ] } }, + "ClusterSecurityGroupId": { + "Value": { + "Fn::GetAtt": [ + "Cluster9EE0221C", + "ClusterSecurityGroupId" + ] + } + }, + "ClusterEncryptionConfigKeyArn": { + "Value": { + "Fn::GetAtt": [ + "Cluster9EE0221C", + "EncryptionConfigKeyArn" + ] + } + }, "ClusterName": { "Value": { "Ref": "Cluster9EE0221C" @@ -2714,41 +2731,41 @@ } }, "Parameters": { - "AssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896S3Bucket221B7FEE": { + "AssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cS3Bucket7F8D74FE": { "Type": "String", - "Description": "S3 bucket for asset \"01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896\"" + "Description": "S3 bucket for asset \"95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402c\"" }, - "AssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896S3VersionKeyA8C9A018": { + "AssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cS3VersionKey1DF2734D": { "Type": "String", - "Description": "S3 key for asset version \"01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896\"" + "Description": "S3 key for asset version \"95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402c\"" }, - "AssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896ArtifactHashED8C0EF9": { + "AssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cArtifactHash38FFB16E": { "Type": "String", - "Description": "Artifact hash for asset \"01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896\"" + "Description": "Artifact hash for asset \"95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402c\"" }, - "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C": { + "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket222C40AB": { "Type": "String", - "Description": "S3 bucket for asset \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" + "Description": "S3 bucket for asset \"164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441\"" }, - "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB": { + "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802": { "Type": "String", - "Description": "S3 key for asset version \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" + "Description": "S3 key for asset version \"164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441\"" }, - "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1ArtifactHash251241BC": { + "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441ArtifactHash88D60D5A": { "Type": "String", - "Description": "Artifact hash for asset \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" + "Description": "Artifact hash for asset \"164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441\"" }, - "AssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbS3Bucket0C3A00C2": { + "AssetParametersca6f286e0d135e22cfefc133659e2f2fe139a4b46b8eef5b8e197606625c9af9S3BucketC1533EC8": { "Type": "String", - "Description": "S3 bucket for asset \"a6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afb\"" + "Description": "S3 bucket for asset \"ca6f286e0d135e22cfefc133659e2f2fe139a4b46b8eef5b8e197606625c9af9\"" }, - "AssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbS3VersionKeyBED95764": { + "AssetParametersca6f286e0d135e22cfefc133659e2f2fe139a4b46b8eef5b8e197606625c9af9S3VersionKey2C834492": { "Type": "String", - "Description": "S3 key for asset version \"a6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afb\"" + "Description": "S3 key for asset version \"ca6f286e0d135e22cfefc133659e2f2fe139a4b46b8eef5b8e197606625c9af9\"" }, - "AssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbArtifactHashBF08C2D7": { + "AssetParametersca6f286e0d135e22cfefc133659e2f2fe139a4b46b8eef5b8e197606625c9af9ArtifactHash51A7CDC3": { "Type": "String", - "Description": "Artifact hash for asset \"a6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afb\"" + "Description": "Artifact hash for asset \"ca6f286e0d135e22cfefc133659e2f2fe139a4b46b8eef5b8e197606625c9af9\"" }, "AssetParameterse02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57S3BucketEF5DD638": { "Type": "String", @@ -2774,29 +2791,29 @@ "Type": "String", "Description": "Artifact hash for asset \"4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319\"" }, - "AssetParameters7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6S3BucketB18DC500": { + "AssetParametersfdca05152ae8546b816681ea8e00f45f12df47f6988add4facaf1f8972995b7aS3BucketDC230AE0": { "Type": "String", - "Description": "S3 bucket for asset \"7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6\"" + "Description": "S3 bucket for asset \"fdca05152ae8546b816681ea8e00f45f12df47f6988add4facaf1f8972995b7a\"" }, - "AssetParameters7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6S3VersionKeyBE7DFF7A": { + "AssetParametersfdca05152ae8546b816681ea8e00f45f12df47f6988add4facaf1f8972995b7aS3VersionKey2373CDCB": { "Type": "String", - "Description": "S3 key for asset version \"7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6\"" + "Description": "S3 key for asset version \"fdca05152ae8546b816681ea8e00f45f12df47f6988add4facaf1f8972995b7a\"" }, - "AssetParameters7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6ArtifactHash5F906FBC": { + "AssetParametersfdca05152ae8546b816681ea8e00f45f12df47f6988add4facaf1f8972995b7aArtifactHashEF3EDEF7": { "Type": "String", - "Description": "Artifact hash for asset \"7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6\"" + "Description": "Artifact hash for asset \"fdca05152ae8546b816681ea8e00f45f12df47f6988add4facaf1f8972995b7a\"" }, - "AssetParameters36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbcS3Bucket2D824DEF": { + "AssetParameters07ba0071b1bf179d8ece256a71220acd174ccb76b36702c2b9b9ecb8004cb608S3Bucket721C96A9": { "Type": "String", - "Description": "S3 bucket for asset \"36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbc\"" + "Description": "S3 bucket for asset \"07ba0071b1bf179d8ece256a71220acd174ccb76b36702c2b9b9ecb8004cb608\"" }, - "AssetParameters36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbcS3VersionKey45D8E8E4": { + "AssetParameters07ba0071b1bf179d8ece256a71220acd174ccb76b36702c2b9b9ecb8004cb608S3VersionKey52A1C30C": { "Type": "String", - "Description": "S3 key for asset version \"36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbc\"" + "Description": "S3 key for asset version \"07ba0071b1bf179d8ece256a71220acd174ccb76b36702c2b9b9ecb8004cb608\"" }, - "AssetParameters36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbcArtifactHash83AE269A": { + "AssetParameters07ba0071b1bf179d8ece256a71220acd174ccb76b36702c2b9b9ecb8004cb608ArtifactHash134C23B4": { "Type": "String", - "Description": "Artifact hash for asset \"36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbc\"" + "Description": "Artifact hash for asset \"07ba0071b1bf179d8ece256a71220acd174ccb76b36702c2b9b9ecb8004cb608\"" }, "SsmParameterValueawsserviceeksoptimizedami116amazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { "Type": "AWS::SSM::Parameter::Value", diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts index f6e883f773140..ff6d62e74c20f 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts @@ -72,6 +72,8 @@ class EksClusterStack extends TestStack { new CfnOutput(this, 'ClusterEndpoint', { value: cluster.clusterEndpoint }); new CfnOutput(this, 'ClusterArn', { value: cluster.clusterArn }); new CfnOutput(this, 'ClusterCertificateAuthorityData', { value: cluster.clusterCertificateAuthorityData }); + new CfnOutput(this, 'ClusterSecurityGroupId', { value: cluster.clusterSecurityGroupId }); + new CfnOutput(this, 'ClusterEncryptionConfigKeyArn', { value: cluster.clusterEncryptionConfigKeyArn }); new CfnOutput(this, 'ClusterName', { value: cluster.clusterName }); } } diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts b/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts index 29dcfac4e89b6..e762d6c7abbd3 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts @@ -99,8 +99,10 @@ export = { Endpoint: 'http://endpoint', Arn: 'arn:cluster-arn', CertificateAuthorityData: 'certificateAuthority-data', - OpenIdConnectIssuerUrl: undefined, - OpenIdConnectIssuer: undefined, + ClusterSecurityGroupId: '', + EncryptionConfigKeyArn: '', + OpenIdConnectIssuerUrl: '', + OpenIdConnectIssuer: '', }, }); test.done(); @@ -272,7 +274,7 @@ export = { test.done(); }, - async '"roleArn" requires a replcement'(test: Test) { + async '"roleArn" requires a replacement'(test: Test) { const handler = new ClusterResourceHandler(mocks.client, mocks.newRequest('Update', { roleArn: 'new-arn', }, { @@ -422,8 +424,10 @@ export = { Endpoint: 'http://endpoint', Arn: 'arn:cluster-arn', CertificateAuthorityData: 'certificateAuthority-data', - OpenIdConnectIssuerUrl: undefined, - OpenIdConnectIssuer: undefined, + ClusterSecurityGroupId: '', + EncryptionConfigKeyArn: '', + OpenIdConnectIssuerUrl: '', + OpenIdConnectIssuer: '', }, }); test.done(); @@ -496,7 +500,106 @@ export = { test.done(); }, }, + + 'logging or access change': { + async 'from undefined to partial logging enabled'(test: Test) { + const handler = new ClusterResourceHandler(mocks.client, mocks.newRequest('Update', { + logging: { + clusterLogging: [ + { + types: [ 'api' ], + enabled: true, + }, + ], + }, + }, { + logging: undefined, + })); + const resp = await handler.onEvent(); + test.deepEqual(resp, { EksUpdateId: mocks.MOCK_UPDATE_STATUS_ID }); + test.deepEqual(mocks.actualRequest.updateClusterConfigRequest!, { + name: 'physical-resource-id', + logging: { + clusterLogging: [ + { + types: [ 'api' ], + enabled: true, + }, + ], + }, + }); + test.equal(mocks.actualRequest.createClusterRequest, undefined); + test.done(); + }, + + async 'from partial vpc configuration to only private access enabled'(test: Test) { + const handler = new ClusterResourceHandler(mocks.client, mocks.newRequest('Update', { + resourcesVpcConfig: { + securityGroupIds: ['sg1', 'sg2', 'sg3'], + endpointPrivateAccess: true, + }, + }, { + resourcesVpcConfig: { + securityGroupIds: ['sg1', 'sg2', 'sg3'], + }, + })); + const resp = await handler.onEvent(); + test.deepEqual(resp, { EksUpdateId: mocks.MOCK_UPDATE_STATUS_ID }); + test.deepEqual(mocks.actualRequest.updateClusterConfigRequest!, { + name: 'physical-resource-id', + logging: undefined, + resourcesVpcConfig: { + endpointPrivateAccess: true, + endpointPublicAccess: undefined, + publicAccessCidrs: undefined, + }, + }); + test.equal(mocks.actualRequest.createClusterRequest, undefined); + test.done(); + }, + + async 'from undefined to both logging and access fully enabled'(test: Test) { + const handler = new ClusterResourceHandler(mocks.client, mocks.newRequest('Update', { + logging: { + clusterLogging: [ + { + types: [ 'api', 'audit', 'authenticator', 'controllerManager', 'scheduler' ], + enabled: true, + }, + ], + }, + resourcesVpcConfig: { + endpointPrivateAccess: true, + endpointPublicAccess: true, + publicAccessCidrs: [ '0.0.0.0/0' ], + }, + }, { + logging: undefined, + resourcesVpcConfig: undefined, + })); + + const resp = await handler.onEvent(); + test.deepEqual(resp, { EksUpdateId: mocks.MOCK_UPDATE_STATUS_ID }); + test.deepEqual(mocks.actualRequest.updateClusterConfigRequest!, { + name: 'physical-resource-id', + logging: { + clusterLogging: [ + { + types: [ 'api', 'audit', 'authenticator', 'controllerManager', 'scheduler' ], + enabled: true, + }, + ], + }, + resourcesVpcConfig: { + endpointPrivateAccess: true, + endpointPublicAccess: true, + publicAccessCidrs: [ '0.0.0.0/0' ], + }, + }); + test.equal(mocks.actualRequest.createClusterRequest, undefined); + test.done(); + }, + }, }, }, - }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster.ts b/packages/@aws-cdk/aws-eks/test/test.cluster.ts index daeded7e80743..4b00ac1ec6e36 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster.ts @@ -306,6 +306,8 @@ export = { clusterName: cluster.clusterName, securityGroups: cluster.connections.securityGroups, clusterCertificateAuthorityData: cluster.clusterCertificateAuthorityData, + clusterSecurityGroupId: cluster.clusterSecurityGroupId, + clusterEncryptionConfigKeyArn: cluster.clusterEncryptionConfigKeyArn, }); // this should cause an export/import diff --git a/packages/@aws-cdk/aws-eks/test/test.fargate.ts b/packages/@aws-cdk/aws-eks/test/test.fargate.ts index 7fe71200c245a..8599090df444b 100644 --- a/packages/@aws-cdk/aws-eks/test/test.fargate.ts +++ b/packages/@aws-cdk/aws-eks/test/test.fargate.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import { Stack, Tag } from '@aws-cdk/core'; @@ -252,6 +252,49 @@ export = { test.done(); }, + 'multiple Fargate profiles added to a cluster are processed sequentially'(test: Test) { + // GIVEN + const stack = new Stack(); + const cluster = new eks.Cluster(stack, 'MyCluster'); + + // WHEN + cluster.addFargateProfile('MyProfile1', { + selectors: [ { namespace: 'namespace1' } ], + }); + cluster.addFargateProfile('MyProfile2', { + selectors: [ { namespace: 'namespace2' } ], + }); + + // THEN + expect(stack).to(haveResource('Custom::AWSCDK-EKS-FargateProfile', { + Config: { + clusterName: { Ref: 'MyCluster8AD82BF8' }, + podExecutionRoleArn: { 'Fn::GetAtt': [ 'MyClusterfargateprofileMyProfile1PodExecutionRole794E9E37', 'Arn' ] }, + selectors: [ { namespace: 'namespace1' } ], + }, + })); + expect(stack).to(haveResource('Custom::AWSCDK-EKS-FargateProfile', { + Properties: { + ServiceToken: { 'Fn::GetAtt': [ + 'awscdkawseksClusterResourceProviderNestedStackawscdkawseksClusterResourceProviderNestedStackResource9827C454', + 'Outputs.awscdkawseksClusterResourceProviderframeworkonEventEA97AA31Arn', + ]}, + AssumeRoleArn: { 'Fn::GetAtt': [ 'MyClusterCreationRoleB5FA4FF3', 'Arn' ] }, + Config: { + clusterName: { Ref: 'MyCluster8AD82BF8' }, + podExecutionRoleArn: { 'Fn::GetAtt': [ 'MyClusterfargateprofileMyProfile2PodExecutionRoleD1151CCF', 'Arn' ] }, + selectors: [ { namespace: 'namespace2' } ], + }, + }, + DependsOn: [ + 'MyClusterfargateprofileMyProfile1PodExecutionRole794E9E37', + 'MyClusterfargateprofileMyProfile1879D501A', + ], + }, ResourcePart.CompleteDefinition)); + + test.done(); + }, + 'fargate role is added to RBAC'(test: Test) { // GIVEN const stack = new Stack(); diff --git a/packages/@aws-cdk/aws-eks/test/test.helm-chart.ts b/packages/@aws-cdk/aws-eks/test/test.helm-chart.ts index ea7828d18fdee..7fed84a2b6185 100644 --- a/packages/@aws-cdk/aws-eks/test/test.helm-chart.ts +++ b/packages/@aws-cdk/aws-eks/test/test.helm-chart.ts @@ -1,4 +1,5 @@ import { expect, haveResource } from '@aws-cdk/assert'; +import { Duration } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as eks from '../lib'; import { testFixtureCluster } from './util'; @@ -70,7 +71,18 @@ export = { new eks.HelmChart(stack, 'MyWaitingChart', { cluster, chart: 'chart' }); // THEN - expect(stack).to(haveResource(eks.HelmChart.RESOURCE_TYPE, { Wait: false})); + expect(stack).to(haveResource(eks.HelmChart.RESOURCE_TYPE, { Wait: false })); + test.done(); + }, + 'should timeout only after 10 minutes'(test: Test) { + // GIVEN + const { stack, cluster } = testFixtureCluster(); + + // WHEN + new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart', timeout: Duration.minutes(10) }); + + // THEN + expect(stack).to(haveResource(eks.HelmChart.RESOURCE_TYPE, { Timeout: 600 })); test.done(); }, }, diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index 01b211d16e142..fb7283b2643d8 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -266,6 +266,9 @@ The `logRetention` property can be used to set a different expiration period. It is possible to obtain the function's log group as a `logs.ILogGroup` by calling the `logGroup` property of the `Function` construct. +By default, CDK uses the AWS SDK retry options when creating a log group. The `logRetentionRetryOptions` property +allows you to customize the maximum number of retries and base backoff duration. + *Note* that, if either `logRetention` is set or `logGroup` property is called, a [CloudFormation custom resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cfn-customresource.html) is added to the stack that pre-creates the log group as part of the stack deployment, if it already doesn't exist, and sets the diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index ea2d2bf1f18ef..d99d1b1ce8377 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -12,7 +12,7 @@ import { calculateFunctionHash, trimFromStart } from './function-hash'; import { Version, VersionOptions } from './lambda-version'; import { CfnFunction } from './lambda.generated'; import { ILayerVersion } from './layers'; -import { LogRetention } from './log-retention'; +import { LogRetention, LogRetentionRetryOptions } from './log-retention'; import { Runtime } from './runtime'; /** @@ -232,6 +232,14 @@ export interface FunctionOptions extends EventInvokeConfigOptions { */ readonly logRetentionRole?: iam.IRole; + /** + * When log retention is specified, a custom resource attempts to create the CloudWatch log group. + * These options control the retry policy when interacting with CloudWatch APIs. + * + * @default - Default AWS SDK retry options. + */ + readonly logRetentionRetryOptions?: LogRetentionRetryOptions; + /** * Options for the `lambda.Version` resource automatically created by the * `fn.currentVersion` method. @@ -544,6 +552,7 @@ export class Function extends FunctionBase { logGroupName: `/aws/lambda/${this.functionName}`, retention: props.logRetention, role: props.logRetentionRole, + logRetentionRetryOptions: props.logRetentionRetryOptions, }); this._logGroup = logs.LogGroup.fromLogGroupArn(this, 'LogGroup', logretention.logGroupArn); } diff --git a/packages/@aws-cdk/aws-lambda/lib/log-retention-provider/index.ts b/packages/@aws-cdk/aws-lambda/lib/log-retention-provider/index.ts index 38ac1fb709d8b..16d44a5fd83de 100644 --- a/packages/@aws-cdk/aws-lambda/lib/log-retention-provider/index.ts +++ b/packages/@aws-cdk/aws-lambda/lib/log-retention-provider/index.ts @@ -2,15 +2,23 @@ // eslint-disable-next-line import/no-extraneous-dependencies import * as AWS from 'aws-sdk'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { RetryDelayOptions } from 'aws-sdk/lib/config'; + +interface SdkRetryOptions { + maxRetries?: number; + retryOptions?: RetryDelayOptions; +} /** * Creates a log group and doesn't throw if it exists. * - * @param logGroupName the name of the log group to create + * @param logGroupName the name of the log group to create. + * @param options CloudWatch API SDK options. */ -async function createLogGroupSafe(logGroupName: string) { +async function createLogGroupSafe(logGroupName: string, options?: SdkRetryOptions) { try { // Try to create the log group - const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28' }); + const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28', ...options }); await cloudwatchlogs.createLogGroup({ logGroupName }).promise(); } catch (e) { if (e.code !== 'ResourceAlreadyExistsException') { @@ -23,10 +31,11 @@ async function createLogGroupSafe(logGroupName: string) { * Puts or deletes a retention policy on a log group. * * @param logGroupName the name of the log group to create + * @param options CloudWatch API SDK options. * @param retentionInDays the number of days to retain the log events in the specified log group. */ -async function setRetentionPolicy(logGroupName: string, retentionInDays?: number) { - const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28' }); +async function setRetentionPolicy(logGroupName: string, options?: SdkRetryOptions, retentionInDays?: number) { + const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28', ...options }); if (!retentionInDays) { await cloudwatchlogs.deleteRetentionPolicy({ logGroupName }).promise(); } else { @@ -41,10 +50,13 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent // The target log group const logGroupName = event.ResourceProperties.LogGroupName; + // Parse to AWS SDK retry options + const retryOptions = parseRetryOptions(event.ResourceProperties.SdkRetry); + if (event.RequestType === 'Create' || event.RequestType === 'Update') { // Act on the target log group - await createLogGroupSafe(logGroupName); - await setRetentionPolicy(logGroupName, parseInt(event.ResourceProperties.RetentionInDays, 10)); + await createLogGroupSafe(logGroupName, retryOptions); + await setRetentionPolicy(logGroupName, retryOptions, parseInt(event.ResourceProperties.RetentionInDays, 10)); if (event.RequestType === 'Create') { // Set a retention policy of 1 day on the logs of this function. The log @@ -56,8 +68,8 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent // same time. This can sometime result in an OperationAbortedException. To // avoid this and because this operation is not critical we catch all errors. try { - await createLogGroupSafe(`/aws/lambda/${context.functionName}`); - await setRetentionPolicy(`/aws/lambda/${context.functionName}`, 1); + await createLogGroupSafe(`/aws/lambda/${context.functionName}`, retryOptions); + await setRetentionPolicy(`/aws/lambda/${context.functionName}`, retryOptions, 1); } catch (e) { console.log(e); } @@ -108,4 +120,19 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent } }); } + + function parseRetryOptions(rawOptions: any): SdkRetryOptions { + const retryOptions: SdkRetryOptions = {}; + if (rawOptions) { + if (rawOptions.maxRetries) { + retryOptions.maxRetries = parseInt(rawOptions.maxRetries, 10); + } + if (rawOptions.base) { + retryOptions.retryOptions = { + base: parseInt(rawOptions.base, 10), + }; + } + } + return retryOptions; + } } diff --git a/packages/@aws-cdk/aws-lambda/lib/log-retention.ts b/packages/@aws-cdk/aws-lambda/lib/log-retention.ts index 74feeb5e62794..6c5fec2da7cd9 100644 --- a/packages/@aws-cdk/aws-lambda/lib/log-retention.ts +++ b/packages/@aws-cdk/aws-lambda/lib/log-retention.ts @@ -26,6 +26,31 @@ export interface LogRetentionProps { * @default - A new role is created */ readonly role?: iam.IRole; + + /** + * Retry options for all AWS API calls. + * + * @default - AWS SDK default retry options + */ + readonly logRetentionRetryOptions?: LogRetentionRetryOptions; +} + +/** + * Retry options for all AWS API calls. + */ +export interface LogRetentionRetryOptions { + /** + * The maximum amount of retries. + * + * @default 3 (AWS SDK default) + */ + readonly maxRetries?: number; + /** + * The base duration to use in the exponential backoff for operation retries. + * + * @default Duration.millis(100) (AWS SDK default) + */ + readonly base?: cdk.Duration; } /** @@ -64,11 +89,16 @@ export class LogRetention extends cdk.Construct { // Need to use a CfnResource here to prevent lerna dependency cycles // @aws-cdk/aws-cloudformation -> @aws-cdk/aws-lambda -> @aws-cdk/aws-cloudformation + const retryOptions = props.logRetentionRetryOptions; const resource = new cdk.CfnResource(this, 'Resource', { type: 'Custom::LogRetention', properties: { ServiceToken: provider.functionArn, LogGroupName: props.logGroupName, + SdkRetry: retryOptions ? { + maxRetries: retryOptions.maxRetries, + base: retryOptions.base?.toMilliseconds(), + } : undefined, RetentionInDays: props.retention === logs.RetentionDays.INFINITE ? undefined : props.retention, }, }); diff --git a/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json index 4ca25f4d0ba99..3e45c9ce6d65d 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json +++ b/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json @@ -133,7 +133,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3Bucket7046E6CE" + "Ref": "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3Bucket21D86049" }, "S3Key": { "Fn::Join": [ @@ -146,7 +146,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3VersionKey3194A583" + "Ref": "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3VersionKey1F67C4C1" } ] } @@ -159,7 +159,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3VersionKey3194A583" + "Ref": "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3VersionKey1F67C4C1" } ] } @@ -331,17 +331,17 @@ } }, "Parameters": { - "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3Bucket7046E6CE": { + "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3Bucket21D86049": { "Type": "String", - "Description": "S3 bucket for asset \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\"" + "Description": "S3 bucket for asset \"3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3\"" }, - "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3VersionKey3194A583": { + "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3VersionKey1F67C4C1": { "Type": "String", - "Description": "S3 key for asset version \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\"" + "Description": "S3 key for asset version \"3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3\"" }, - "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eArtifactHashB967D42A": { + "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3ArtifactHash31AA1F7C": { "Type": "String", - "Description": "Artifact hash for asset \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\"" + "Description": "Artifact hash for asset \"3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/test.log-retention-provider.ts b/packages/@aws-cdk/aws-lambda/test/test.log-retention-provider.ts index bfc37c0d1b6f2..007f2836bf44b 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.log-retention-provider.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.log-retention-provider.ts @@ -296,4 +296,41 @@ export = { test.done(); }, + + async 'custom log retention retry options'(test: Test) { + AWS.mock('CloudWatchLogs', 'createLogGroup', sinon.fake.resolves({})); + AWS.mock('CloudWatchLogs', 'putRetentionPolicy', sinon.fake.resolves({})); + AWS.mock('CloudWatchLogs', 'deleteRetentionPolicy', sinon.fake.resolves({})); + + const event = { + ...eventCommon, + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'token', + RetentionInDays: '30', + LogGroupName: 'group', + SdkRetry: { + maxRetries: '5', + base: '300', + }, + }, + }; + + const request = createRequest('SUCCESS'); + + await provider.handler(event as AWSLambda.CloudFormationCustomResourceCreateEvent, context); + + sinon.assert.calledWith(AWSSDK.CloudWatchLogs as any, { + apiVersion: '2014-03-28', + maxRetries: 5, + retryOptions: { + base: 300, + }, + }); + + test.equal(request.isDone(), true); + + test.done(); + }, + }; diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 070ac5ca1698c..5ce914f99f47a 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -47,7 +47,7 @@ your instances will be launched privately or publicly: ```ts const instance = new DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.ORACLE_SE1, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'syscdk', vpc }); @@ -62,7 +62,7 @@ Example for max storage configuration: ```ts const instance = new DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.ORACLE_SE1, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'syscdk', vpc, maxAllocatedStorage: 200 @@ -76,14 +76,13 @@ a source database respectively: new DatabaseInstanceFromSnapshot(stack, 'Instance', { snapshotIdentifier: 'my-snapshot', engine: rds.DatabaseInstanceEngine.POSTGRES, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), vpc }); new DatabaseInstanceReadReplica(stack, 'ReadReplica', { sourceDatabaseInstance: sourceInstance, - engine: rds.DatabaseInstanceEngine.POSTGRES, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), vpc }); ``` diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index 5ed0925bf5d7d..7af0ebe13a58c 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -313,7 +313,7 @@ export interface DatabaseInstanceNewProps { /** * The name of the compute and memory capacity classes. */ - readonly instanceClass: ec2.InstanceType; + readonly instanceType: ec2.InstanceType; /** * Specifies if the database instance is a multiple Availability Zone deployment. @@ -610,7 +610,7 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData availabilityZone: props.multiAz ? undefined : props.availabilityZone, backupRetentionPeriod: props.backupRetention ? props.backupRetention.toDays() : undefined, copyTagsToSnapshot: props.copyTagsToSnapshot !== undefined ? props.copyTagsToSnapshot : true, - dbInstanceClass: `db.${props.instanceClass}`, + dbInstanceClass: `db.${props.instanceType}`, dbInstanceIdentifier: props.instanceIdentifier, dbSubnetGroupName: subnetGroup.ref, deleteAutomatedBackups: props.deleteAutomatedBackups, @@ -995,7 +995,7 @@ export class DatabaseInstanceFromSnapshot extends DatabaseInstanceSource impleme /** * Construction properties for a DatabaseInstanceReadReplica. */ -export interface DatabaseInstanceReadReplicaProps extends DatabaseInstanceSourceProps { +export interface DatabaseInstanceReadReplicaProps extends DatabaseInstanceNewProps { /** * The source database instance. * diff --git a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json index d5c7708151b53..01687488c079e 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json @@ -967,7 +967,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3Bucket7046E6CE" + "Ref": "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3Bucket21D86049" }, "S3Key": { "Fn::Join": [ @@ -980,7 +980,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3VersionKey3194A583" + "Ref": "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3VersionKey1F67C4C1" } ] } @@ -993,7 +993,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3VersionKey3194A583" + "Ref": "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3VersionKey1F67C4C1" } ] } @@ -1108,17 +1108,17 @@ } }, "Parameters": { - "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3Bucket7046E6CE": { + "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3Bucket21D86049": { "Type": "String", - "Description": "S3 bucket for asset \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\"" + "Description": "S3 bucket for asset \"3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3\"" }, - "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3VersionKey3194A583": { + "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3VersionKey1F67C4C1": { "Type": "String", - "Description": "S3 key for asset version \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\"" + "Description": "S3 key for asset version \"3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3\"" }, - "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eArtifactHashB967D42A": { + "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3ArtifactHash31AA1F7C": { "Type": "String", - "Description": "Artifact hash for asset \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\"" + "Description": "Artifact hash for asset \"3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3\"" } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.ts b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.ts index f386c04b0a1d6..d37fd89f9b935 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.ts +++ b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.ts @@ -46,7 +46,7 @@ class DatabaseInstanceStack extends cdk.Stack { const instance = new rds.DatabaseInstance(this, 'Instance', { engine: rds.DatabaseInstanceEngine.ORACLE_SE1, licenseModel: rds.LicenseModel.BRING_YOUR_OWN_LICENSE, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE3, ec2.InstanceSize.MEDIUM), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE3, ec2.InstanceSize.MEDIUM), multiAz: true, storageType: rds.StorageType.IO1, masterUsername: 'syscdk', diff --git a/packages/@aws-cdk/aws-rds/test/test.instance.ts b/packages/@aws-cdk/aws-rds/test/test.instance.ts index baefed5b6b157..c22f926c95fbe 100644 --- a/packages/@aws-cdk/aws-rds/test/test.instance.ts +++ b/packages/@aws-cdk/aws-rds/test/test.instance.ts @@ -18,7 +18,7 @@ export = { new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.ORACLE_SE1, licenseModel: rds.LicenseModel.BRING_YOUR_OWN_LICENSE, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.MEDIUM), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.MEDIUM), multiAz: true, storageType: rds.StorageType.IO1, masterUsername: 'syscdk', @@ -215,7 +215,7 @@ export = { // WHEN new rds.DatabaseInstance(stack, 'Database', { engine: rds.DatabaseInstanceEngine.SQL_SERVER_EE, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'syscdk', masterUserPassword: cdk.SecretValue.plainText('tooshort'), vpc, @@ -244,7 +244,7 @@ export = { new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { snapshotIdentifier: 'my-snapshot', engine: rds.DatabaseInstanceEngine.POSTGRES, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), vpc, }); @@ -264,7 +264,7 @@ export = { test.throws(() => new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { snapshotIdentifier: 'my-snapshot', engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), vpc, generateMasterUserPassword: true, }), '`masterUsername` must be specified when `generateMasterUserPassword` is set to true.'); @@ -281,7 +281,7 @@ export = { test.throws(() => new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { snapshotIdentifier: 'my-snapshot', engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), vpc, masterUsername: 'superadmin', }), 'Cannot specify `masterUsername` when `generateMasterUserPassword` is set to false.'); @@ -298,7 +298,7 @@ export = { test.throws(() => new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { snapshotIdentifier: 'my-snapshot', engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), vpc, masterUserPassword: cdk.SecretValue.plainText('supersecret'), generateMasterUserPassword: true, @@ -313,7 +313,7 @@ export = { const vpc = new ec2.Vpc(stack, 'VPC'); const sourceInstance = new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'admin', vpc, }); @@ -321,8 +321,7 @@ export = { // WHEN new rds.DatabaseInstanceReadReplica(stack, 'ReadReplica', { sourceDatabaseInstance: sourceInstance, - engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), vpc, }); @@ -354,7 +353,7 @@ export = { const vpc = new ec2.Vpc(stack, 'VPC'); const instance = new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'admin', vpc, }); @@ -421,7 +420,7 @@ export = { const vpc = new ec2.Vpc(stack, 'VPC'); const instance = new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'admin', vpc, }); @@ -474,7 +473,7 @@ export = { // WHEN const instance = new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'admin', vpc, }); @@ -499,7 +498,7 @@ export = { // WHEN const instance = new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'admin', vpc, }); @@ -530,7 +529,7 @@ export = { // WHEN new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'admin', vpc, backupRetention: cdk.Duration.seconds(0), @@ -583,7 +582,7 @@ export = { // WHEN new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'admin', vpc, monitoringInterval: cdk.Duration.minutes(1), @@ -612,7 +611,7 @@ export = { // WHEN const instance = new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'admin', vpc, securityGroups: [securityGroup], @@ -649,7 +648,7 @@ export = { const vpc = new ec2.Vpc(stack, 'VPC'); const instance = new rds.DatabaseInstance(stack, 'Database', { engine: rds.DatabaseInstanceEngine.SQL_SERVER_EE, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'syscdk', masterUserPassword: cdk.SecretValue.plainText('tooshort'), vpc, @@ -667,7 +666,7 @@ export = { const vpc = new ec2.Vpc(stack, 'VPC'); const instance = new rds.DatabaseInstance(stack, 'Database', { engine: rds.DatabaseInstanceEngine.SQL_SERVER_EE, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'syscdk', vpc, }); @@ -694,7 +693,7 @@ export = { tzSupportedEngines.forEach((engine) => { test.ok(new rds.DatabaseInstance(stack, `${engine.name}-db`, { engine, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.SMALL), masterUsername: 'master', timezone: 'Europe/Zurich', vpc, @@ -704,7 +703,7 @@ export = { tzUnsupportedEngines.forEach((engine) => { test.throws(() => new rds.DatabaseInstance(stack, `${engine.name}-db`, { engine, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.SMALL), masterUsername: 'master', timezone: 'Europe/Zurich', vpc, @@ -723,7 +722,7 @@ export = { new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { snapshotIdentifier: 'my-snapshot', engine: rds.DatabaseInstanceEngine.POSTGRES, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), vpc, maxAllocatedStorage: 200, }); @@ -744,7 +743,7 @@ export = { // WHEN new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'admin', vpc, backupRetention: cdk.Duration.seconds(0), diff --git a/packages/@aws-cdk/aws-s3-assets/README.md b/packages/@aws-cdk/aws-s3-assets/README.md index 07d3a88bb0208..3833f750ebe02 100644 --- a/packages/@aws-cdk/aws-s3-assets/README.md +++ b/packages/@aws-cdk/aws-s3-assets/README.md @@ -50,9 +50,6 @@ The following examples grants an IAM group read permissions on an asset: [Example of granting read access to an asset](./test/integ.assets.permissions.lit.ts) -The following example uses custom asset bundling to convert a markdown file to html: -[Example of using asset bundling](./test/integ.assets.bundling.lit.ts) - ## How does it work? When an asset is defined in a construct, a construct metadata entry @@ -73,6 +70,26 @@ the asset store, it is uploaded during deployment. Now, when the toolkit deploys the stack, it will set the relevant CloudFormation Parameters to point to the actual bucket and key for each asset. +## Asset Bundling + +When defining an asset, you can use the `bundling` option to specify a command +to run inside a docker container. The command can read the contents of the asset +source from `/asset-input` and is expected to write files under `/asset-output` +(directories mapped inside the container). The files under `/asset-output` will +be zipped and uploaded to S3 as the asset. + +The following example uses custom asset bundling to convert a markdown file to html: + +[Example of using asset bundling](./test/integ.assets.bundling.lit.ts). + +The bundling docker image (`image`) can either come from a registry (`BundlingDockerImage.fromRegistry`) +or it can be built from a `Dockerfile` located inside your project (`BundlingDockerImage.fromAsset`). + +You can set the `CDK_DOCKER` environment variable in order to provide a custom +docker program to execute. This may sometime be needed when building in +environments where the standard docker cannot be executed (see +https://github.com/aws/aws-cdk/issues/8460 for details). + ## CloudFormation Resource Metadata > NOTE: This section is relevant for authors of AWS Resource Constructs. diff --git a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts index 5c3f0a514f07e..7a60aebbaf1f4 100644 --- a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts +++ b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts @@ -19,7 +19,9 @@ export interface AssetOptions extends assets.CopyOptions, cdk.AssetOptions { readonly readers?: iam.IGrantable[]; /** - * Custom source hash to use when identifying the specific version of the asset. + * Custom hash to use when identifying the specific version of the asset. For consistency, + * this custom hash will be SHA256 hashed and encoded as hex. The resulting hash will be + * the asset hash. * * NOTE: the source hash is used in order to identify a specific revision of the asset, * and used for optimizing and caching deployment activities related to this asset such as diff --git a/packages/@aws-cdk/aws-s3-deployment/lambda/build.sh b/packages/@aws-cdk/aws-s3-deployment/lambda/build.sh index ddcbf4807ab0d..cc9df52d130df 100755 --- a/packages/@aws-cdk/aws-s3-deployment/lambda/build.sh +++ b/packages/@aws-cdk/aws-s3-deployment/lambda/build.sh @@ -27,7 +27,7 @@ cd ${staging} # install python requirements # Must use --prefix to because --target cannot be used on # platforms that have a default --prefix set. -pip3 install --ignore-installed --prefix ${piptemp} -r ${staging}/requirements.txt +pip3 install --ignore-installed --prefix ${piptemp} --no-user -r ${staging}/requirements.txt mv ${piptemp}/lib/python*/*-packages/* . [ -d ${piptemp}/lib64 ] && mv ${piptemp}/lib64/python*/*-packages/* . rm -fr ./awscli/examples diff --git a/packages/@aws-cdk/aws-s3-deployment/lambda/test.sh b/packages/@aws-cdk/aws-s3-deployment/lambda/test.sh index a11706a3cee31..e87f8dfc2492b 100755 --- a/packages/@aws-cdk/aws-s3-deployment/lambda/test.sh +++ b/packages/@aws-cdk/aws-s3-deployment/lambda/test.sh @@ -4,6 +4,7 @@ # # prepares a staging directory with the requirements set -e +set -x scriptdir=$(cd $(dirname $0) && pwd) # prepare staging directory @@ -16,7 +17,7 @@ cp -f ${scriptdir}/src/* $PWD cp -f ${scriptdir}/test/* $PWD # install deps -pip3 install -r requirements.txt -t . +pip3 install --no-user -r requirements.txt -t . # run our tests exec python3 test.py $@ diff --git a/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts b/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts index e8f4fda42651b..f989429b7552d 100644 --- a/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts +++ b/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts @@ -161,11 +161,11 @@ export class BucketDeployment extends cdk.Construct { throw new Error('Distribution must be specified if distribution paths are specified'); } - const sourceHash = calcSourceHash(handlerSourceDirectory); + const assetHash = calcSourceHash(handlerSourceDirectory); const handler = new lambda.SingletonFunction(this, 'CustomResourceHandler', { uuid: this.renderSingletonUuid(props.memoryLimit), - code: lambda.Code.fromAsset(handlerCodeBundle, { sourceHash }), + code: lambda.Code.fromAsset(handlerCodeBundle, { assetHash }), runtime: lambda.Runtime.PYTHON_3_6, handler: 'index.handler', lambdaPurpose: 'Custom::CDKBucketDeployment', diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-cloudfront.expected.json b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-cloudfront.expected.json index cd76f93ae2e36..d0e61e14a23ae 100644 --- a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-cloudfront.expected.json +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-cloudfront.expected.json @@ -248,7 +248,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersa9125fa9a40550c71cde90bd478cc23091e868067a12380c1df0827d013ad2ffS3Bucket848A1F31" + "Ref": "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0S3Bucket88A20322" }, "S3Key": { "Fn::Join": [ @@ -261,7 +261,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersa9125fa9a40550c71cde90bd478cc23091e868067a12380c1df0827d013ad2ffS3VersionKey983DBE96" + "Ref": "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0S3VersionKey5726B1E8" } ] } @@ -274,7 +274,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersa9125fa9a40550c71cde90bd478cc23091e868067a12380c1df0827d013ad2ffS3VersionKey983DBE96" + "Ref": "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0S3VersionKey5726B1E8" } ] } @@ -301,17 +301,17 @@ } }, "Parameters": { - "AssetParametersa9125fa9a40550c71cde90bd478cc23091e868067a12380c1df0827d013ad2ffS3Bucket848A1F31": { + "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0S3Bucket88A20322": { "Type": "String", - "Description": "S3 bucket for asset \"a9125fa9a40550c71cde90bd478cc23091e868067a12380c1df0827d013ad2ff\"" + "Description": "S3 bucket for asset \"85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0\"" }, - "AssetParametersa9125fa9a40550c71cde90bd478cc23091e868067a12380c1df0827d013ad2ffS3VersionKey983DBE96": { + "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0S3VersionKey5726B1E8": { "Type": "String", - "Description": "S3 key for asset version \"a9125fa9a40550c71cde90bd478cc23091e868067a12380c1df0827d013ad2ff\"" + "Description": "S3 key for asset version \"85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0\"" }, - "AssetParametersa9125fa9a40550c71cde90bd478cc23091e868067a12380c1df0827d013ad2ffArtifactHash08605F5E": { + "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0ArtifactHash877EFA91": { "Type": "String", - "Description": "Artifact hash for asset \"a9125fa9a40550c71cde90bd478cc23091e868067a12380c1df0827d013ad2ff\"" + "Description": "Artifact hash for asset \"85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0\"" }, "AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3Bucket9CD8B20A": { "Type": "String", diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json index 628b948fbd440..e3308f63a431d 100644 --- a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json @@ -291,7 +291,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersa9125fa9a40550c71cde90bd478cc23091e868067a12380c1df0827d013ad2ffS3Bucket848A1F31" + "Ref": "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0S3Bucket88A20322" }, "S3Key": { "Fn::Join": [ @@ -304,7 +304,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersa9125fa9a40550c71cde90bd478cc23091e868067a12380c1df0827d013ad2ffS3VersionKey983DBE96" + "Ref": "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0S3VersionKey5726B1E8" } ] } @@ -317,7 +317,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersa9125fa9a40550c71cde90bd478cc23091e868067a12380c1df0827d013ad2ffS3VersionKey983DBE96" + "Ref": "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0S3VersionKey5726B1E8" } ] } @@ -478,17 +478,17 @@ } }, "Parameters": { - "AssetParametersa9125fa9a40550c71cde90bd478cc23091e868067a12380c1df0827d013ad2ffS3Bucket848A1F31": { + "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0S3Bucket88A20322": { "Type": "String", - "Description": "S3 bucket for asset \"a9125fa9a40550c71cde90bd478cc23091e868067a12380c1df0827d013ad2ff\"" + "Description": "S3 bucket for asset \"85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0\"" }, - "AssetParametersa9125fa9a40550c71cde90bd478cc23091e868067a12380c1df0827d013ad2ffS3VersionKey983DBE96": { + "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0S3VersionKey5726B1E8": { "Type": "String", - "Description": "S3 key for asset version \"a9125fa9a40550c71cde90bd478cc23091e868067a12380c1df0827d013ad2ff\"" + "Description": "S3 key for asset version \"85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0\"" }, - "AssetParametersa9125fa9a40550c71cde90bd478cc23091e868067a12380c1df0827d013ad2ffArtifactHash08605F5E": { + "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0ArtifactHash877EFA91": { "Type": "String", - "Description": "Artifact hash for asset \"a9125fa9a40550c71cde90bd478cc23091e868067a12380c1df0827d013ad2ff\"" + "Description": "Artifact hash for asset \"85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0\"" }, "AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3Bucket9CD8B20A": { "Type": "String", diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index e0e89b4ecd924..801a4b322bd03 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -85,8 +85,8 @@ The following example provides the field named `input` as the input to the `Task state that runs a Lambda function. ```ts -const submitJob = new sfn.Task(stack, 'Invoke Handler', { - task: new tasks.RunLambdaTask(submitJobLambda), +const submitJob = new tasks.LambdaInvoke(stack, 'Invoke Handler', { + lambdaFunction: submitJobLambda, inputPath: '$.input' }); ``` @@ -105,8 +105,8 @@ as well as other metadata. The following example assigns the output from the Task to a field named `result` ```ts -const submitJob = new sfn.Task(stack, 'Invoke Handler', { - task: new tasks.RunLambdaTask(submitJobLambda), +const submitJob = new tasks.LambdaInvoke(stack, 'Invoke Handler', { + lambdaFunction: submitJobLambda, outputPath: '$.Payload.result' }); ``` @@ -150,11 +150,10 @@ The following example provides the field named `input` as the input to the Lambd and invokes it asynchronously. ```ts -const submitJob = new sfn.Task(stack, 'Invoke Handler', { - task: new tasks.RunLambdaTask(submitJobLambda, { - payload: sfn.Data.StringAt('$.input'), - invocationType: tasks.InvocationType.EVENT, - }), +const submitJob = new tasks.LambdaInvoke(stack, 'Invoke Handler', { + lambdaFunction: submitJobLambda, + payload: sfn.Data.StringAt('$.input'), + invocationType: tasks.InvocationType.EVENT, }); ``` diff --git a/packages/@aws-cdk/cloudformation-diff/package.json b/packages/@aws-cdk/cloudformation-diff/package.json index 88342e0ba3835..f7c1e6015cfdc 100644 --- a/packages/@aws-cdk/cloudformation-diff/package.json +++ b/packages/@aws-cdk/cloudformation-diff/package.json @@ -33,7 +33,7 @@ "@types/string-width": "^4.0.1", "@types/table": "^4.0.7", "cdk-build-tools": "0.0.0", - "fast-check": "^1.24.2", + "fast-check": "^1.25.0", "jest": "^25.5.4", "pkglint": "0.0.0", "ts-jest": "^26.1.0" diff --git a/packages/@aws-cdk/core/lib/asset-staging.ts b/packages/@aws-cdk/core/lib/asset-staging.ts index c37d8d441d7c0..460d89d70133b 100644 --- a/packages/@aws-cdk/core/lib/asset-staging.ts +++ b/packages/@aws-cdk/core/lib/asset-staging.ts @@ -1,9 +1,10 @@ import * as cxapi from '@aws-cdk/cx-api'; +import * as crypto from 'crypto'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { AssetHashType, AssetOptions } from './assets'; -import { BUNDLING_INPUT_DIR, BUNDLING_OUTPUT_DIR, BundlingOptions } from './bundling'; +import { BundlingOptions } from './bundling'; import { Construct, ISynthesisSession } from './construct-compat'; import { FileSystem, FingerprintOptions } from './fs'; @@ -36,6 +37,18 @@ export interface AssetStagingProps extends FingerprintOptions, AssetOptions { * means that only if content was changed, copy will happen. */ export class AssetStaging extends Construct { + /** + * The directory inside the bundling container into which the asset sources will be mounted. + * @experimental + */ + public static readonly BUNDLING_INPUT_DIR = '/asset-input'; + + /** + * The directory inside the bundling container into which the bundled output should be written. + * @experimental + */ + public static readonly BUNDLING_OUTPUT_DIR = '/asset-output'; + /** * The path to the asset (stringinfied token). * @@ -137,17 +150,27 @@ export class AssetStaging extends Construct { private bundle(options: BundlingOptions): string { // Create temporary directory for bundling - const bundleDir = fs.mkdtempSync(path.resolve(path.join(os.tmpdir(), 'cdk-asset-bundle-'))); + const bundleDir = FileSystem.mkdtemp('cdk-asset-bundle-'); + + let user: string; + if (options.user) { + user = options.user; + } else { // Default to current user + const userInfo = os.userInfo(); + user = userInfo.uid !== -1 // uid is -1 on Windows + ? `${userInfo.uid}:${userInfo.gid}` + : '1000:1000'; + } // Always mount input and output dir const volumes = [ { hostPath: this.sourcePath, - containerPath: BUNDLING_INPUT_DIR, + containerPath: AssetStaging.BUNDLING_INPUT_DIR, }, { hostPath: bundleDir, - containerPath: BUNDLING_OUTPUT_DIR, + containerPath: AssetStaging.BUNDLING_OUTPUT_DIR, }, ...options.volumes ?? [], ]; @@ -155,16 +178,17 @@ export class AssetStaging extends Construct { try { options.image._run({ command: options.command, + user, volumes, environment: options.environment, - workingDirectory: options.workingDirectory ?? BUNDLING_INPUT_DIR, + workingDirectory: options.workingDirectory ?? AssetStaging.BUNDLING_INPUT_DIR, }); } catch (err) { throw new Error(`Failed to run bundling Docker image for asset ${this.node.path}: ${err}`); } if (FileSystem.isEmpty(bundleDir)) { - throw new Error(`Bundling did not produce any output. Check that your container writes content to ${BUNDLING_OUTPUT_DIR}.`); + throw new Error(`Bundling did not produce any output. Check that your container writes content to ${AssetStaging.BUNDLING_OUTPUT_DIR}.`); } return bundleDir; @@ -196,7 +220,9 @@ export class AssetStaging extends Construct { if (!props.assetHash) { throw new Error('`assetHash` must be specified when `assetHashType` is set to `AssetHashType.CUSTOM`.'); } - return props.assetHash; + // Hash the hash to make sure we can use it in a file/directory name. + // The resulting hash will also have the same length as for the other hash types. + return crypto.createHash('sha256').update(props.assetHash).digest('hex'); default: throw new Error('Unknown asset hash type.'); } diff --git a/packages/@aws-cdk/core/lib/assets.ts b/packages/@aws-cdk/core/lib/assets.ts index bad303dbd8c31..19dfc0c89e89b 100644 --- a/packages/@aws-cdk/core/lib/assets.ts +++ b/packages/@aws-cdk/core/lib/assets.ts @@ -18,7 +18,9 @@ export interface IAsset { export interface AssetOptions { /** * Specify a custom hash for this asset. If `assetHashType` is set it must - * be set to `AssetHashType.CUSTOM`. + * be set to `AssetHashType.CUSTOM`. For consistency, this custom hash will + * be SHA256 hashed and encoded as hex. The resulting hash will be the asset + * hash. * * NOTE: the hash is used in order to identify a specific revision of the asset, and * used for optimizing and caching deployment activities related to this asset such as diff --git a/packages/@aws-cdk/core/lib/bundling.ts b/packages/@aws-cdk/core/lib/bundling.ts index bfff68b40f5cd..1034517534f10 100644 --- a/packages/@aws-cdk/core/lib/bundling.ts +++ b/packages/@aws-cdk/core/lib/bundling.ts @@ -1,8 +1,5 @@ import { spawnSync } from 'child_process'; -export const BUNDLING_INPUT_DIR = '/asset-input'; -export const BUNDLING_OUTPUT_DIR = '/asset-output'; - /** * Bundling options * @@ -45,6 +42,17 @@ export interface BundlingOptions { * @default /asset-input */ readonly workingDirectory?: string; + + /** + * The user to use when running the container. + * + * user | user:group | uid | uid:gid | user:gid | uid:group + * + * @see https://docs.docker.com/engine/reference/run/#user + * + * @default - uid:gid of the current user or 1000:1000 on Windows + */ + readonly user?: string; } /** @@ -75,7 +83,7 @@ export class BundlingDockerImage { path, ]; - const docker = exec('docker', dockerArgs); + const docker = dockerExec(dockerArgs); const match = docker.stdout.toString().match(/Successfully built ([a-z0-9]+)/); @@ -101,6 +109,9 @@ export class BundlingDockerImage { const dockerArgs: string[] = [ 'run', '--rm', + ...options.user + ? ['-u', options.user] + : [], ...flatten(volumes.map(v => ['-v', `${v.hostPath}:${v.containerPath}`])), ...flatten(Object.entries(environment).map(([k, v]) => ['--env', `${k}=${v}`])), ...options.workingDirectory @@ -110,7 +121,7 @@ export class BundlingDockerImage { ...command, ]; - exec('docker', dockerArgs); + dockerExec(dockerArgs); } } @@ -160,6 +171,13 @@ interface DockerRunOptions { * @default - image default */ readonly workingDirectory?: string; + + /** + * The user to use when running the container. + * + * @default - root or image default + */ + readonly user?: string; } /** @@ -178,8 +196,9 @@ function flatten(x: string[][]) { return Array.prototype.concat([], ...x); } -function exec(cmd: string, args: string[]) { - const proc = spawnSync(cmd, args); +function dockerExec(args: string[]) { + const prog = process.env.CDK_DOCKER ?? 'docker'; + const proc = spawnSync(prog, args); if (proc.error) { throw proc.error; diff --git a/packages/@aws-cdk/core/lib/fs/index.ts b/packages/@aws-cdk/core/lib/fs/index.ts index 01c6d132956e2..4ecfea7c2471c 100644 --- a/packages/@aws-cdk/core/lib/fs/index.ts +++ b/packages/@aws-cdk/core/lib/fs/index.ts @@ -1,4 +1,6 @@ import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; import { copyDirectory } from './copy'; import { fingerprint } from './fingerprint'; import { CopyOptions, FingerprintOptions } from './options'; @@ -43,4 +45,27 @@ export class FileSystem { public static isEmpty(dir: string): boolean { return fs.readdirSync(dir).length === 0; } + + /** + * The real path of the system temp directory + */ + public static get tmpdir(): string { + if (FileSystem._tmpdir) { + return FileSystem._tmpdir; + } + FileSystem._tmpdir = fs.realpathSync(os.tmpdir()); + return FileSystem._tmpdir; + } + + /** + * Creates a unique temporary directory in the **system temp directory**. + * + * @param prefix A prefix for the directory name. Six random characters + * will be generated and appended behind this prefix. + */ + public static mkdtemp(prefix: string): string { + return fs.mkdtempSync(path.join(FileSystem.tmpdir, prefix)); + } + + private static _tmpdir?: string; } diff --git a/packages/@aws-cdk/core/lib/private/runtime-info.ts b/packages/@aws-cdk/core/lib/private/runtime-info.ts index e18fabc5ecaa1..dce0ac508d71f 100644 --- a/packages/@aws-cdk/core/lib/private/runtime-info.ts +++ b/packages/@aws-cdk/core/lib/private/runtime-info.ts @@ -3,7 +3,7 @@ import { basename, dirname } from 'path'; import { major as nodeMajorVersion } from './node-version'; // list of NPM scopes included in version reporting e.g. @aws-cdk and @aws-solutions-konstruk -const WHITELIST_SCOPES = ['@aws-cdk', '@aws-solutions-konstruk']; +const WHITELIST_SCOPES = ['@aws-cdk', '@aws-solutions-konstruk', '@aws-solutions-constructs']; /** * Returns a list of loaded modules and their versions. diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index c1066fd4799a6..6d9d38199e6a1 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -158,7 +158,7 @@ "@types/sinon": "^9.0.4", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", - "fast-check": "^1.24.2", + "fast-check": "^1.25.0", "lodash": "^4.17.15", "nodeunit": "^0.11.3", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/core/test/docker-stub.sh b/packages/@aws-cdk/core/test/docker-stub.sh new file mode 100755 index 0000000000000..45a78ef881ebd --- /dev/null +++ b/packages/@aws-cdk/core/test/docker-stub.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -euo pipefail + +# stub for the `docker` executable. it is used as CDK_DOCKER when executing unit +# tests in `test.staging.ts` It outputs the command line to +# `/tmp/docker-stub.input` and accepts one of 3 commands that impact it's +# behavior. + +echo "$@" > /tmp/docker-stub.input + +if echo "$@" | grep "DOCKER_STUB_SUCCESS_NO_OUTPUT"; then + exit 0 +fi + +if echo "$@" | grep "DOCKER_STUB_FAIL"; then + echo "A HUGE FAILING DOCKER STUFF" + exit 1 +fi + +if echo "$@" | grep "DOCKER_STUB_SUCCESS"; then + outdir=$(echo "$@" | xargs -n1 | grep "/asset-output" | head -n1 | cut -d":" -f1) + touch ${outdir}/test.txt + exit 0 +fi + +echo "Docker mock only supports one of the following commands: DOCKER_STUB_SUCCESS_NO_OUTPUT,DOCKER_STUB_FAIL,DOCKER_STUB_SUCCESS" +exit 1 diff --git a/packages/@aws-cdk/core/test/fs/test.fs.ts b/packages/@aws-cdk/core/test/fs/test.fs.ts new file mode 100644 index 0000000000000..cc6d4898c922e --- /dev/null +++ b/packages/@aws-cdk/core/test/fs/test.fs.ts @@ -0,0 +1,48 @@ +import * as fs from 'fs'; +import { Test } from 'nodeunit'; +import * as os from 'os'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { FileSystem } from '../../lib/fs'; + +export = { + 'tearDown'(callback: any) { + sinon.restore(); + callback(); + }, + + 'tmpdir returns a real path and is cached'(test: Test) { + // Create symlink that points to /tmp + const symlinkTmp = path.join(__dirname, 'tmp-link'); + fs.symlinkSync(os.tmpdir(), symlinkTmp); + + // Now stub os.tmpdir() to return this link instead of /tmp + const tmpdirStub = sinon.stub(os, 'tmpdir').returns(symlinkTmp); + + test.ok(path.isAbsolute(FileSystem.tmpdir)); + + const p = path.join(FileSystem.tmpdir, 'tmpdir-test.txt'); + fs.writeFileSync(p, 'tmpdir-test'); + + test.equal(p, fs.realpathSync(p)); + test.equal(fs.readFileSync(p, 'utf8'), 'tmpdir-test'); + + test.ok(tmpdirStub.calledOnce); // cached result + + fs.unlinkSync(p); + fs.unlinkSync(symlinkTmp); + + test.done(); + }, + + 'mkdtemp creates a temporary directory in the system temp'(test: Test) { + const tmpdir = FileSystem.mkdtemp('cdk-mkdtemp-'); + + test.equal(path.dirname(tmpdir), FileSystem.tmpdir); + test.ok(fs.existsSync(tmpdir)); + + fs.rmdirSync(tmpdir); + + test.done(); + }, +}; diff --git a/packages/@aws-cdk/core/test/test.bundling.ts b/packages/@aws-cdk/core/test/test.bundling.ts index 658aa99901bb6..2ba23a83ffce9 100644 --- a/packages/@aws-cdk/core/test/test.bundling.ts +++ b/packages/@aws-cdk/core/test/test.bundling.ts @@ -28,10 +28,12 @@ export = { }, volumes: [{ hostPath: '/host-path', containerPath: '/container-path' }], workingDirectory: '/working-directory', + user: 'user:group', }); test.ok(spawnSyncStub.calledWith('docker', [ 'run', '--rm', + '-u', 'user:group', '-v', '/host-path:/container-path', '--env', 'VAR1=value1', '--env', 'VAR2=value2', diff --git a/packages/@aws-cdk/core/test/test.staging.ts b/packages/@aws-cdk/core/test/test.staging.ts index 5d5ab521eba59..08e20fa0fbad0 100644 --- a/packages/@aws-cdk/core/test/test.staging.ts +++ b/packages/@aws-cdk/core/test/test.staging.ts @@ -1,10 +1,33 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs'; import { Test } from 'nodeunit'; +import * as os from 'os'; import * as path from 'path'; import { App, AssetHashType, AssetStaging, BundlingDockerImage, Stack } from '../lib'; +const STUB_INPUT_FILE = '/tmp/docker-stub.input'; + +enum DockerStubCommand { + SUCCESS = 'DOCKER_STUB_SUCCESS', + FAIL = 'DOCKER_STUB_FAIL', + SUCCESS_NO_OUTPUT = 'DOCKER_STUB_SUCCESS_NO_OUTPUT' +} + +const userInfo = os.userInfo(); +const USER_ARG = `-u ${userInfo.uid}:${userInfo.gid}`; + +// this is a way to provide a custom "docker" command for staging. +process.env.CDK_DOCKER = `${__dirname}/docker-stub.sh`; + export = { + + 'tearDown'(cb: any) { + if (fs.existsSync(STUB_INPUT_FILE)) { + fs.unlinkSync(STUB_INPUT_FILE); + } + cb(); + }, + 'base case'(test: Test) { // GIVEN const stack = new Stack(); @@ -86,12 +109,16 @@ export = { sourcePath: directory, bundling: { image: BundlingDockerImage.fromRegistry('alpine'), - command: ['touch', '/asset-output/test.txt'], + command: [ DockerStubCommand.SUCCESS ], }, }); // THEN const assembly = app.synth(); + test.deepEqual( + readDockerStubInput(), + `run --rm ${USER_ARG} -v /input:/asset-input -v /output:/asset-output -w /asset-input alpine DOCKER_STUB_SUCCESS`, + ); test.deepEqual(fs.readdirSync(assembly.directory), [ 'asset.2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00', 'cdk.out', @@ -114,9 +141,14 @@ export = { sourcePath: directory, bundling: { image: BundlingDockerImage.fromRegistry('alpine'), + command: [ DockerStubCommand.SUCCESS_NO_OUTPUT ], }, }), /Bundling did not produce any output/); + test.equal( + readDockerStubInput(), + `run --rm ${USER_ARG} -v /input:/asset-input -v /output:/asset-output -w /asset-input alpine DOCKER_STUB_SUCCESS_NO_OUTPUT`, + ); test.done(); }, @@ -131,11 +163,16 @@ export = { sourcePath: directory, bundling: { image: BundlingDockerImage.fromRegistry('alpine'), - command: ['touch', '/asset-output/test.txt'], + command: [ DockerStubCommand.SUCCESS ], }, assetHashType: AssetHashType.BUNDLE, }); + // THEN + test.equal( + readDockerStubInput(), + `run --rm ${USER_ARG} -v /input:/asset-input -v /output:/asset-output -w /asset-input alpine DOCKER_STUB_SUCCESS`, + ); test.equal(asset.assetHash, '33cbf2cae5432438e0f046bc45ba8c3cef7b6afcf47b59d1c183775c1918fb1f'); test.done(); @@ -153,7 +190,9 @@ export = { assetHash: 'my-custom-hash', }); - test.equal(asset.assetHash, 'my-custom-hash'); + // THEN + test.equal(fs.existsSync(STUB_INPUT_FILE), false); + test.equal(asset.assetHash, 'b9c77053f5b83bbe5ba343bc18e92db939a49017010813225fea91fa892c4823'); // hash of 'my-custom-hash' test.done(); }, @@ -169,11 +208,15 @@ export = { sourcePath: directory, bundling: { image: BundlingDockerImage.fromRegistry('alpine'), - command: ['touch', '/asset-output/test.txt'], + command: [ DockerStubCommand.SUCCESS ], }, assetHash: 'my-custom-hash', assetHashType: AssetHashType.BUNDLE, }), /Cannot specify `bundle` for `assetHashType`/); + test.equal( + readDockerStubInput(), + `run --rm ${USER_ARG} -v /input:/asset-input -v /output:/asset-output -w /asset-input alpine DOCKER_STUB_SUCCESS`, + ); test.done(); }, @@ -189,6 +232,7 @@ export = { sourcePath: directory, assetHashType: AssetHashType.BUNDLE, }), /Cannot use `AssetHashType.BUNDLE` when `bundling` is not specified/); + test.equal(fs.existsSync(STUB_INPUT_FILE), false); test.done(); }, @@ -204,6 +248,7 @@ export = { sourcePath: directory, assetHashType: AssetHashType.CUSTOM, }), /`assetHash` must be specified when `assetHashType` is set to `AssetHashType.CUSTOM`/); + test.equal(fs.existsSync(STUB_INPUT_FILE), false); // "docker" not executed test.done(); }, @@ -219,9 +264,21 @@ export = { sourcePath: directory, bundling: { image: BundlingDockerImage.fromRegistry('this-is-an-invalid-docker-image'), + command: [ DockerStubCommand.FAIL ], }, }), /Failed to run bundling Docker image for asset stack\/Asset/); + test.equal( + readDockerStubInput(), + `run --rm ${USER_ARG} -v /input:/asset-input -v /output:/asset-output -w /asset-input this-is-an-invalid-docker-image DOCKER_STUB_FAIL`, + ); test.done(); }, }; + +function readDockerStubInput() { + const out = fs.readFileSync(STUB_INPUT_FILE, 'utf-8').trim(); + return out + .replace(/-v ([^:]+):\/asset-input/, '-v /input:/asset-input') + .replace(/-v ([^:]+):\/asset-output/, '-v /output:/asset-output'); +} diff --git a/packages/@aws-cdk/custom-resources/lib/provider-framework/runtime/outbound.ts b/packages/@aws-cdk/custom-resources/lib/provider-framework/runtime/outbound.ts index 9b15ec01864f7..682632fd1a40a 100644 --- a/packages/@aws-cdk/custom-resources/lib/provider-framework/runtime/outbound.ts +++ b/packages/@aws-cdk/custom-resources/lib/provider-framework/runtime/outbound.ts @@ -1,8 +1,19 @@ /* istanbul ignore file */ // eslint-disable-next-line import/no-extraneous-dependencies import * as AWS from 'aws-sdk'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { ConfigurationOptions } from 'aws-sdk/lib/config'; import * as https from 'https'; +const FRAMEWORK_HANDLER_TIMEOUT = 900000; // 15 minutes + +// In order to honor the overall maximum timeout set for the target process, +// the default 2 minutes from AWS SDK has to be overriden: +// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#httpOptions-property +const awsSdkConfig: ConfigurationOptions = { + httpOptions: { timeout: FRAMEWORK_HANDLER_TIMEOUT }, +}; + async function defaultHttpRequest(options: https.RequestOptions, responseBody: string) { return new Promise((resolve, reject) => { try { @@ -21,7 +32,7 @@ let lambda: AWS.Lambda; async function defaultStartExecution(req: AWS.StepFunctions.StartExecutionInput): Promise { if (!sfn) { - sfn = new AWS.StepFunctions(); + sfn = new AWS.StepFunctions(awsSdkConfig); } return await sfn.startExecution(req).promise(); @@ -29,7 +40,7 @@ async function defaultStartExecution(req: AWS.StepFunctions.StartExecutionInput) async function defaultInvokeFunction(req: AWS.Lambda.InvocationRequest): Promise { if (!lambda) { - lambda = new AWS.Lambda(); + lambda = new AWS.Lambda(awsSdkConfig); } return await lambda.invoke(req).promise(); diff --git a/packages/@aws-cdk/custom-resources/test/provider-framework/integ.provider.expected.json b/packages/@aws-cdk/custom-resources/test/provider-framework/integ.provider.expected.json index 9907ab690dd70..eac6081caf809 100644 --- a/packages/@aws-cdk/custom-resources/test/provider-framework/integ.provider.expected.json +++ b/packages/@aws-cdk/custom-resources/test/provider-framework/integ.provider.expected.json @@ -200,7 +200,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket222C40AB" }, "S3Key": { "Fn::Join": [ @@ -213,7 +213,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } ] } @@ -226,7 +226,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } ] } @@ -579,7 +579,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket222C40AB" }, "S3Key": { "Fn::Join": [ @@ -592,7 +592,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } ] } @@ -605,7 +605,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } ] } @@ -721,7 +721,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket222C40AB" }, "S3Key": { "Fn::Join": [ @@ -734,7 +734,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } ] } @@ -747,7 +747,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } ] } @@ -860,7 +860,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket222C40AB" }, "S3Key": { "Fn::Join": [ @@ -873,7 +873,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } ] } @@ -886,7 +886,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } ] } @@ -1042,17 +1042,17 @@ "Type": "String", "Description": "Artifact hash for asset \"f465f835a93a93413d7d25f5572670bbb6379304f4cdbad718d4f6a5562d1368\"" }, - "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C": { + "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket222C40AB": { "Type": "String", - "Description": "S3 bucket for asset \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" + "Description": "S3 bucket for asset \"164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441\"" }, - "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB": { + "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802": { "Type": "String", - "Description": "S3 key for asset version \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" + "Description": "S3 key for asset version \"164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441\"" }, - "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1ArtifactHash251241BC": { + "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441ArtifactHash88D60D5A": { "Type": "String", - "Description": "Artifact hash for asset \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" + "Description": "Artifact hash for asset \"164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441\"" }, "AssetParameters4bafad8d010ba693e235b77d2c6decfc2ac79a8208d4477cbb36d31caf7189e8S3Bucket0DB889DF": { "Type": "String", diff --git a/packages/@monocdk-experiment/rewrite-imports/package.json b/packages/@monocdk-experiment/rewrite-imports/package.json index d9a1f74eb18e8..8e7df3d741e57 100644 --- a/packages/@monocdk-experiment/rewrite-imports/package.json +++ b/packages/@monocdk-experiment/rewrite-imports/package.json @@ -35,7 +35,7 @@ "typescript": "~3.8.3" }, "devDependencies": { - "@types/glob": "^7.1.1", + "@types/glob": "^7.1.2", "@types/jest": "^25.2.3", "@types/node": "^10.17.25", "cdk-build-tools": "0.0.0", diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 7a056f1b1ba02..f679127cf4612 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -43,7 +43,7 @@ "@aws-cdk/core": "0.0.0", "@types/archiver": "^3.1.0", "@types/fs-extra": "^8.1.0", - "@types/glob": "^7.1.1", + "@types/glob": "^7.1.2", "@types/jest": "^25.2.3", "@types/minimatch": "^3.0.3", "@types/mockery": "^1.4.29", diff --git a/packages/cdk-assets/package.json b/packages/cdk-assets/package.json index 91c272bb380c8..b2fbabf9b77ab 100644 --- a/packages/cdk-assets/package.json +++ b/packages/cdk-assets/package.json @@ -31,7 +31,7 @@ "license": "Apache-2.0", "devDependencies": { "@types/archiver": "^3.1.0", - "@types/glob": "^7.1.1", + "@types/glob": "^7.1.2", "@types/jest": "^25.2.3", "@types/mock-fs": "^4.10.0", "@types/node": "^10.17.25", diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index b8ff407bc883f..71096964a4b7a 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -39,7 +39,7 @@ "pkglint": "0.0.0" }, "dependencies": { - "@typescript-eslint/eslint-plugin": "^3.1.0", + "@typescript-eslint/eslint-plugin": "^3.2.0", "@typescript-eslint/parser": "^2.19.2", "awslint": "0.0.0", "colors": "^1.4.0", @@ -52,7 +52,7 @@ "jsii": "^1.6.0", "jsii-pacmak": "^1.6.0", "nodeunit": "^0.11.3", - "nyc": "^15.0.1", + "nyc": "^15.1.0", "ts-jest": "^26.1.0", "tslint": "^5.20.1", "typescript": "~3.8.3", diff --git a/yarn.lock b/yarn.lock index 3434b584d847c..af3aac24745c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1042,10 +1042,10 @@ inquirer "^6.2.0" npmlog "^4.1.2" -"@lerna/publish@3.22.0": - version "3.22.0" - resolved "https://registry.yarnpkg.com/@lerna/publish/-/publish-3.22.0.tgz#7a3fb61026d3b7425f3b9a1849421f67d795c55d" - integrity sha512-8LBeTLBN8NIrCrLGykRu+PKrfrCC16sGCVY0/bzq9TDioR7g6+cY0ZAw653Qt/0Kr7rg3J7XxVNdzj3fvevlwA== +"@lerna/publish@3.22.1": + version "3.22.1" + resolved "https://registry.yarnpkg.com/@lerna/publish/-/publish-3.22.1.tgz#b4f7ce3fba1e9afb28be4a1f3d88222269ba9519" + integrity sha512-PG9CM9HUYDreb1FbJwFg90TCBQooGjj+n/pb3gw/eH5mEDq0p8wKdLFe0qkiqUkm/Ub5C8DbVFertIo0Vd0zcw== dependencies: "@evocateur/libnpmaccess" "^3.1.2" "@evocateur/npm-registry-fetch" "^4.0.0" @@ -1068,7 +1068,7 @@ "@lerna/run-lifecycle" "3.16.2" "@lerna/run-topologically" "3.18.5" "@lerna/validation-error" "3.13.0" - "@lerna/version" "3.22.0" + "@lerna/version" "3.22.1" figgy-pudding "^3.5.1" fs-extra "^8.1.0" npm-package-arg "^6.1.0" @@ -1181,10 +1181,10 @@ dependencies: npmlog "^4.1.2" -"@lerna/version@3.22.0": - version "3.22.0" - resolved "https://registry.yarnpkg.com/@lerna/version/-/version-3.22.0.tgz#67e1340c1904e9b339becd66429f32dd8ad65a55" - integrity sha512-6uhL6RL7/FeW6u1INEgyKjd5dwO8+IsbLfkfC682QuoVLS7VG6OOB+JmTpCvnuyYWI6fqGh1bRk9ww8kPsj+EA== +"@lerna/version@3.22.1": + version "3.22.1" + resolved "https://registry.yarnpkg.com/@lerna/version/-/version-3.22.1.tgz#9805a9247a47ee62d6b81bd9fa5fb728b24b59e2" + integrity sha512-PSGt/K1hVqreAFoi3zjD0VEDupQ2WZVlVIwesrE5GbrL2BjXowjCsTDPqblahDUPy0hp6h7E2kG855yLTp62+g== dependencies: "@lerna/check-working-tree" "3.16.5" "@lerna/child-process" "3.16.5" @@ -1427,11 +1427,6 @@ resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== -"@types/events@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" - integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== - "@types/fs-extra@^8.1.0": version "8.1.0" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.0.tgz#1114834b53c3914806cd03b3304b37b3bd221a4d" @@ -1446,12 +1441,11 @@ dependencies: "@types/node" "*" -"@types/glob@*", "@types/glob@^7.1.1": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" - integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== +"@types/glob@*", "@types/glob@^7.1.1", "@types/glob@^7.1.2": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.2.tgz#06ca26521353a545d94a0adc74f38a59d232c987" + integrity sha512-VgNIkxK+j7Nz5P7jvUZlRvhuPSmsEfS03b0alKcq5V/STUKAa3Plemsn5mrQUO7am6OErJ4rhGEGJbACclrtRA== dependencies: - "@types/events" "*" "@types/minimatch" "*" "@types/node" "*" @@ -1648,12 +1642,12 @@ resolved "https://registry.yarnpkg.com/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.3.tgz#38fb31d82ed07dea87df6bd565721d11979fd761" integrity sha512-mhdQq10tYpiNncMkg1vovCud5jQm+rWeRVz6fxjCJlY6uhDlAn9GnMSmBa2DQwqPf/jS5YR0K/xChDEh1jdOQg== -"@typescript-eslint/eslint-plugin@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.1.0.tgz#4ac00ecca3bbea740c577f1843bc54fa69c3def2" - integrity sha512-D52KwdgkjYc+fmTZKW7CZpH5ZBJREJKZXRrveMiRCmlzZ+Rw9wRVJ1JAmHQ9b/+Ehy1ZeaylofDB9wwXUt83wg== +"@typescript-eslint/eslint-plugin@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.2.0.tgz#7fb997f391af32ae6ca1dbe56bcefe4dd30bda14" + integrity sha512-t9RTk/GyYilIXt6BmZurhBzuMT9kLKw3fQoJtK9ayv0tXTlznXEAnx07sCLXdkN3/tZDep1s1CEV95CWuARYWA== dependencies: - "@typescript-eslint/experimental-utils" "3.1.0" + "@typescript-eslint/experimental-utils" "3.2.0" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" semver "^7.3.2" @@ -1669,13 +1663,13 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/experimental-utils@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.1.0.tgz#2d5dba7c2ac2a3da3bfa3f461ff64de38587a872" - integrity sha512-Zf8JVC2K1svqPIk1CB/ehCiWPaERJBBokbMfNTNRczCbQSlQXaXtO/7OfYz9wZaecNvdSvVADt6/XQuIxhC79w== +"@typescript-eslint/experimental-utils@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.2.0.tgz#4dab8fc9f44f059ec073470a81bb4d7d7d51e6c5" + integrity sha512-UbJBsk+xO9dIFKtj16+m42EvUvsjZbbgQ2O5xSTSfVT1Z3yGkL90DVu0Hd3029FZ5/uBgl+F3Vo8FAcEcqc6aQ== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "3.1.0" + "@typescript-eslint/typescript-estree" "3.2.0" eslint-scope "^5.0.0" eslint-utils "^2.0.0" @@ -1702,10 +1696,10 @@ semver "^6.3.0" tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.1.0.tgz#eaff52d31e615e05b894f8b9d2c3d8af152a5dd2" - integrity sha512-+4nfYauqeQvK55PgFrmBWFVYb6IskLyOosYEmhH3mSVhfBp9AIJnjExdgDmKWoOBHRcPM8Ihfm2BFpZf0euUZQ== +"@typescript-eslint/typescript-estree@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.2.0.tgz#c735f1ca6b4d3cd671f30de8c9bde30843e7ead8" + integrity sha512-uh+Y2QO7dxNrdLw7mVnjUqkwO/InxEqwN0wF+Za6eo3coxls9aH9kQ/5rSvW2GcNanebRTmsT5w1/92lAOb1bA== dependencies: debug "^4.1.1" eslint-visitor-keys "^1.1.0" @@ -4123,13 +4117,13 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -fast-check@^1.24.2: - version "1.24.2" - resolved "https://registry.yarnpkg.com/fast-check/-/fast-check-1.24.2.tgz#1f5e4a3c20530c3a85a861e60f680c32229d4fcb" - integrity sha512-ZL48cyZZLJnVsUj127Zi1mfFLM98yzw0LlSSH8CMeVmpL5RCfSRcZSZZ0kJWrRK4eOgNFnXXKNDbzuRb3Vsdhg== +fast-check@^1.25.0: + version "1.25.0" + resolved "https://registry.yarnpkg.com/fast-check/-/fast-check-1.25.0.tgz#ae22f452e2d53577c32c7903d5b4e1eb3aacf7d5" + integrity sha512-HhiFqNBfty56YNuPLBlEKBmyCmUtAAV8d9qr/n3DJT1mi8dM34uK542tnO5HAgrhVjICLQuH3jrD6kULw+uCJQ== dependencies: pure-rand "^2.0.0" - tslib "^1.10.0" + tslib "^2.0.0" fast-deep-equal@^2.0.1: version "2.0.1" @@ -4502,6 +4496,11 @@ get-caller-file@^2.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + get-pkg-repo@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz#c73b489c06d80cc5536c2c853f9e05232056972d" @@ -5916,9 +5915,9 @@ js-tokens@^4.0.0: integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^3.13.1, js-yaml@^3.2.7: - version "3.13.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" - integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -6195,10 +6194,10 @@ lcov-parse@^1.0.0: resolved "https://registry.yarnpkg.com/lcov-parse/-/lcov-parse-1.0.0.tgz#eb0d46b54111ebc561acb4c408ef9363bdc8f7e0" integrity sha1-6w1GtUER68VhrLTECO+TY73I9+A= -lerna@^3.22.0: - version "3.22.0" - resolved "https://registry.yarnpkg.com/lerna/-/lerna-3.22.0.tgz#da14d08f183ffe6eec566a4ef3f0e11afa621183" - integrity sha512-xWlHdAStcqK/IjKvjsSMHPZjPkBV1lS60PmsIeObU8rLljTepc4Sg/hncw4HWfQxPIewHAUTqhrxPIsqf9L2Eg== +lerna@^3.22.1: + version "3.22.1" + resolved "https://registry.yarnpkg.com/lerna/-/lerna-3.22.1.tgz#82027ac3da9c627fd8bf02ccfeff806a98e65b62" + integrity sha512-vk1lfVRFm+UuEFA7wkLKeSF7Iz13W+N/vFd48aW2yuS7Kv0RbNm2/qcDPV863056LMfkRlsEe+QYOw3palj5Lg== dependencies: "@lerna/add" "3.21.0" "@lerna/bootstrap" "3.21.0" @@ -6213,9 +6212,9 @@ lerna@^3.22.0: "@lerna/init" "3.21.0" "@lerna/link" "3.21.0" "@lerna/list" "3.21.0" - "@lerna/publish" "3.22.0" + "@lerna/publish" "3.22.1" "@lerna/run" "3.21.0" - "@lerna/version" "3.22.0" + "@lerna/version" "3.22.1" import-local "^2.0.0" npmlog "^4.1.2" @@ -7148,10 +7147,10 @@ nyc@^14.0.0: yargs "^13.2.2" yargs-parser "^13.0.0" -nyc@^15.0.1: - version "15.0.1" - resolved "https://registry.yarnpkg.com/nyc/-/nyc-15.0.1.tgz#bd4d5c2b17f2ec04370365a5ca1fc0ed26f9f93d" - integrity sha512-n0MBXYBYRqa67IVt62qW1r/d9UH/Qtr7SF1w/nQLJ9KxvWF6b2xCHImRAixHN9tnMMYHC2P14uo6KddNGwMgGg== +nyc@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/nyc/-/nyc-15.1.0.tgz#1335dae12ddc87b6e249d5a1994ca4bdaea75f02" + integrity sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A== dependencies: "@istanbuljs/load-nyc-config" "^1.0.0" "@istanbuljs/schema" "^0.1.2" @@ -7161,6 +7160,7 @@ nyc@^15.0.1: find-cache-dir "^3.2.0" find-up "^4.1.0" foreground-child "^2.0.0" + get-package-type "^0.1.0" glob "^7.1.6" istanbul-lib-coverage "^3.0.0" istanbul-lib-hook "^3.0.0" @@ -9474,11 +9474,16 @@ tsconfig-paths@^3.9.0: minimist "^1.2.0" strip-bom "^3.0.0" -tslib@^1.10.0, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: +tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: version "1.11.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== +tslib@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.0.tgz#18d13fc2dce04051e20f074cc8387fd8089ce4f3" + integrity sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g== + tslint@^5.20.1: version "5.20.1" resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.20.1.tgz#e401e8aeda0152bc44dd07e614034f3f80c67b7d"