diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 620b728b91435..4789a8cc62410 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -11,7 +11,6 @@ - Amazon API Gateway is a fully managed service that makes it easy for developers to publish, maintain, monitor, and secure APIs at any scale. Create an API to access data, business logic, or functionality from your back-end services, such @@ -79,7 +78,7 @@ The following code defines a REST API that routes all requests to the specified AWS Lambda function: ```ts -const backend = new lambda.Function(...); +declare const backend: lambda.Function; new apigateway.LambdaRestApi(this, 'myapi', { handler: backend, }); @@ -89,7 +88,7 @@ You can also supply `proxy: false`, in which case you will have to explicitly define the API model: ```ts -const backend = new lambda.Function(...); +declare const backend: lambda.Function; const api = new apigateway.LambdaRestApi(this, 'myapi', { handler: backend, proxy: false @@ -134,7 +133,9 @@ The following example shows how to integrate the `GET /book/{book_id}` method to an AWS Lambda function: ```ts -const getBookHandler = new lambda.Function(...); +declare const getBookHandler: lambda.Function; +declare const book: apigateway.Resource; + const getBookIntegration = new apigateway.LambdaIntegration(getBookHandler); book.addMethod('GET', getBookIntegration); ``` @@ -142,6 +143,9 @@ book.addMethod('GET', getBookIntegration); Integration options can be optionally be specified: ```ts +declare const getBookHandler: lambda.Function; +declare const getBookIntegration: apigateway.LambdaIntegration; + const getBookIntegration = new apigateway.LambdaIntegration(getBookHandler, { contentHandling: apigateway.ContentHandling.CONVERT_TO_TEXT, // convert to base64 credentialsPassthrough: true, // use caller identity to invoke the function @@ -151,6 +155,9 @@ const getBookIntegration = new apigateway.LambdaIntegration(getBookHandler, { Method options can optionally be specified when adding methods: ```ts +declare const book: apigateway.Resource; +declare const getBookIntegration: apigateway.LambdaIntegration; + book.addMethod('GET', getBookIntegration, { authorizationType: apigateway.AuthorizationType.IAM, apiKeyRequired: true @@ -162,9 +169,9 @@ It is possible to also integrate with AWS services in a different region. The fo ```ts const getMessageIntegration = new apigateway.AwsIntegration({ - service: 'sqs', - path: 'queueName', - region: 'eu-west-1' + service: 'sqs', + path: 'queueName', + region: 'eu-west-1' }); ``` @@ -172,11 +179,13 @@ const getMessageIntegration = new apigateway.AwsIntegration({ A usage plan specifies who can access one or more deployed API stages and methods, and the rate at which they can be accessed. The plan uses API keys to identify API clients and meters access to the associated API stages for each key. -Usage plans also allow configuring throttling limits and quota limits that are enforced on individual client API keys. +Usage plans also allow configuring throttling limits and quota limits that are enforced on individual client API keys. The following example shows how to create and asscociate a usage plan and an API key: ```ts +declare const integration: apigateway.LambdaIntegration; + const api = new apigateway.RestApi(this, 'hello-api'); const v1 = api.root.addResource('v1'); @@ -198,6 +207,10 @@ plan.addApiKey(key); To associate a plan to a given RestAPI stage: ```ts +declare const plan: apigateway.UsagePlan; +declare const api: apigateway.RestApi; +declare const echoMethod: apigateway.Method; + plan.addApiStage({ stage: api.deploymentStage, throttle: [ @@ -215,13 +228,14 @@ plan.addApiStage({ Existing usage plans can be imported into a CDK app using its id. ```ts -const importedUsagePlan = UsagePlan.fromUsagePlanId(stack, 'imported-usage-plan', ''); +const importedUsagePlan = apigateway.UsagePlan.fromUsagePlanId(this, 'imported-usage-plan', ''); ``` The name and value of the API Key can be specified at creation; if not provided, a name and value will be automatically generated by API Gateway. ```ts +declare const api: apigateway.RestApi; const key = api.addApiKey('ApiKey', { apiKeyName: 'myApiKey1', value: 'MyApiKeyThatIsAtLeast20Characters', @@ -231,14 +245,16 @@ const key = api.addApiKey('ApiKey', { Existing API keys can also be imported into a CDK app using its id. ```ts -const importedKey = ApiKey.fromApiKeyId(this, 'imported-key', ''); +const importedKey = apigateway.ApiKey.fromApiKeyId(this, 'imported-key', ''); ``` The "grant" methods can be used to give prepackaged sets of permissions to other resources. The following code provides read permission to an API key. ```ts -importedKey.grantRead(lambda); +declare const importedKey: apigateway.ApiKey; +declare const lambdaFn: lambda.Function; +importedKey.grantRead(lambdaFn); ``` ### ⚠️ Multiple API Keys @@ -254,6 +270,9 @@ being deleted remain unchanged. Make note of the logical ids of these API keys before removing any, and set it as part of the `addApiKey()` method: ```ts +declare const usageplan: apigateway.UsagePlan; +declare const apiKey: apigateway.ApiKey; + usageplan.addApiKey(apiKey, { overrideLogicalId: '...', }); @@ -271,6 +290,8 @@ The API key created has the specified rate limits, such as quota and throttles, The following example shows how to use a rate limited api key : ```ts +declare const api: apigateway.RestApi; + const key = new apigateway.RateLimitedApiKey(this, 'rate-limited-api-key', { customerId: 'hello-customer', resources: [api], @@ -300,7 +321,9 @@ const resource = api.root.addResource('v1'); You can define more parameters on the integration to tune the behavior of API Gateway ```ts -const integration = new LambdaIntegration(hello, { +declare const hello: lambda.Function; + +const integration = new apigateway.LambdaIntegration(hello, { proxy: false, requestParameters: { // You can define mapping parameters from your method to your integration @@ -317,7 +340,7 @@ const integration = new LambdaIntegration(hello, { 'application/json': JSON.stringify({ action: 'sayHello', pollId: "$util.escapeJavaScript($input.params('who'))" }) }, // This parameter defines the behavior of the engine is no suitable response template is found - passthroughBehavior: PassthroughBehavior.NEVER, + passthroughBehavior: apigateway.PassthroughBehavior.NEVER, integrationResponses: [ { // Successful response from the Lambda function, no filter defined @@ -360,17 +383,19 @@ const integration = new LambdaIntegration(hello, { You can define models for your responses (and requests) ```ts +declare const api: apigateway.RestApi; + // We define the JSON Schema for the transformed valid response const responseModel = api.addModel('ResponseModel', { contentType: 'application/json', modelName: 'ResponseModel', schema: { - schema: JsonSchemaVersion.DRAFT4, + schema: apigateway.JsonSchemaVersion.DRAFT4, title: 'pollResponse', - type: JsonSchemaType.OBJECT, + type: apigateway.JsonSchemaType.OBJECT, properties: { - state: { type: JsonSchemaType.STRING }, - greeting: { type: JsonSchemaType.STRING } + state: { type: apigateway.JsonSchemaType.STRING }, + greeting: { type: apigateway.JsonSchemaType.STRING } } } }); @@ -380,12 +405,12 @@ const errorResponseModel = api.addModel('ErrorResponseModel', { contentType: 'application/json', modelName: 'ErrorResponseModel', schema: { - schema: JsonSchemaVersion.DRAFT4, + schema: apigateway.JsonSchemaVersion.DRAFT4, title: 'errorResponse', - type: JsonSchemaType.OBJECT, + type: apigateway.JsonSchemaType.OBJECT, properties: { - state: { type: JsonSchemaType.STRING }, - message: { type: JsonSchemaType.STRING } + state: { type: apigateway.JsonSchemaType.STRING }, + message: { type: apigateway.JsonSchemaType.STRING } } } }); @@ -395,6 +420,11 @@ const errorResponseModel = api.addModel('ErrorResponseModel', { And reference all on your method definition. ```ts +declare const integration: apigateway.LambdaIntegration; +declare const resource: apigateway.Resource; +declare const responseModel: apigateway.Model; +declare const errorResponseModel: apigateway.Model; + resource.addMethod('GET', integration, { // We can mark the parameters as required requestParameters: { @@ -405,7 +435,7 @@ resource.addMethod('GET', integration, { requestValidatorName: 'test-validator', validateRequestBody: true, validateRequestParameters: false - } + }, methodResponses: [ { // Successful response from the integration @@ -455,12 +485,12 @@ integration. This means that all API methods that do not explicitly define an integration will be routed to this AWS Lambda function. ```ts -const booksBackend = new apigateway.LambdaIntegration(...); +declare const booksBackend: apigateway.LambdaIntegration; const api = new apigateway.RestApi(this, 'books', { defaultIntegration: booksBackend }); -const books = new api.root.addResource('books'); +const books = api.root.addResource('books'); books.addMethod('GET'); // integrated with `booksBackend` books.addMethod('POST'); // integrated with `booksBackend` @@ -477,8 +507,10 @@ Read more about authorization scopes Authorization scopes for a Method can be configured using the `authorizationScopes` property as shown below - ```ts +declare const books: apigateway.Resource; + books.addMethod('GET', new apigateway.HttpIntegration('http://amazon.com'), { - authorizationType: AuthorizationType.COGNITO, + authorizationType: apigateway.AuthorizationType.COGNITO, authorizationScopes: ['Scope1','Scope2'] }); ``` @@ -489,8 +521,10 @@ The `addProxy` method can be used to install a greedy `{proxy+}` resource on a path. By default, this also installs an `"ANY"` method: ```ts +declare const resource: apigateway.Resource; +declare const handler: lambda.Function; const proxy = resource.addProxy({ - defaultIntegration: new LambdaIntegration(handler), + defaultIntegration: new apigateway.LambdaIntegration(handler), // "false" will require explicitly adding methods on the `proxy` resource anyMethod: true // "true" is the default @@ -507,6 +541,9 @@ that can be used for controlling access to your REST APIs. The following CDK code provides 'execute-api' permission to an IAM user, via IAM policies, for the 'GET' method on the `books` resource: ```ts +declare const books: apigateway.Resource; +declare const iamUser: iam.User; + const getBooks = books.addMethod('GET', new apigateway.HttpIntegration('http://amazon.com'), { authorizationType: apigateway.AuthorizationType.IAM }); @@ -541,10 +578,8 @@ inputs and outputs of the Lambda handler. The following code attaches a token-based Lambda authorizer to the 'GET' Method of the Book resource: ```ts -const authFn = new lambda.Function(this, 'booksAuthorizerLambda', { - // ... - // ... -}); +declare const authFn: lambda.Function; +declare const books: apigateway.Resource; const auth = new apigateway.TokenAuthorizer(this, 'booksAuthorizer', { handler: authFn @@ -583,14 +618,12 @@ inputs and outputs of the Lambda handler. The following code attaches a request-based Lambda authorizer to the 'GET' Method of the Book resource: ```ts -const authFn = new lambda.Function(this, 'booksAuthorizerLambda', { - // ... - // ... -}); +declare const authFn: lambda.Function; +declare const books: apigateway.Resource; const auth = new apigateway.RequestAuthorizer(this, 'booksAuthorizer', { handler: authFn, - identitySources: [IdentitySource.header('Authorization')] + identitySources: [apigateway.IdentitySource.header('Authorization')] }); books.addMethod('GET', new apigateway.HttpIntegration('http://amazon.com'), { @@ -616,12 +649,13 @@ API Gateway also allows [Amazon Cognito user pools as authorizer](https://docs.a The following snippet configures a Cognito user pool as an authorizer: ```ts -const userPool = new cognito.UserPool(stack, 'UserPool'); +const userPool = new cognito.UserPool(this, 'UserPool'); const auth = new apigateway.CognitoUserPoolsAuthorizer(this, 'booksAuthorizer', { cognitoUserPools: [userPool] }); +declare const books: apigateway.Resource; books.addMethod('GET', new apigateway.HttpIntegration('http://amazon.com'), { authorizer: auth, authorizationType: apigateway.AuthorizationType.COGNITO, @@ -633,11 +667,13 @@ books.addMethod('GET', new apigateway.HttpIntegration('http://amazon.com'), { Mutual TLS can be configured to limit access to your API based by using client certificates instead of (or as an extension of) using authorization headers. ```ts -new apigw.DomainName(this, 'domain-name', { +declare const acm: any; + +new apigateway.DomainName(this, 'domain-name', { domainName: 'example.com', certificate: acm.Certificate.fromCertificateArn(this, 'cert', 'arn:aws:acm:us-east-1:1111111:certificate/11-3336f1-44483d-adc7-9cd375c5169d'), mtls: { - bucket: new Bucket(this, 'bucket'), + bucket: new s3.Bucket(this, 'bucket'), key: 'truststore.pem', version: 'version', }, @@ -712,7 +748,9 @@ To associate an API with a custom domain, use the `domainName` configuration whe you define your API: ```ts -const api = new apigw.RestApi(this, 'MyDomain', { +declare const acmCertificateForExampleCom: any; + +const api = new apigateway.RestApi(this, 'MyDomain', { domainName: { domainName: 'example.com', certificate: acmCertificateForExampleCom, @@ -734,6 +772,9 @@ CNAME records only for subdomains.) import * as route53 from '@aws-cdk/aws-route53'; import * as targets from '@aws-cdk/aws-route53-targets'; +declare const api: apigateway.RestApi; +declare const hostedZoneForExampleCom: any; + new route53.ARecord(this, 'CustomDomainAliasRecord', { zone: hostedZoneForExampleCom, target: route53.RecordTarget.fromAlias(new targets.ApiGateway(api)) @@ -743,11 +784,13 @@ new route53.ARecord(this, 'CustomDomainAliasRecord', { You can also define a `DomainName` resource directly in order to customize the default behavior: ```ts -new apigw.DomainName(this, 'custom-domain', { +declare const acmCertificateForExampleCom: any; + +new apigateway.DomainName(this, 'custom-domain', { domainName: 'example.com', certificate: acmCertificateForExampleCom, - endpointType: apigw.EndpointType.EDGE, // default is REGIONAL - securityPolicy: apigw.SecurityPolicy.TLS_1_2 + endpointType: apigateway.EndpointType.EDGE, // default is REGIONAL + securityPolicy: apigateway.SecurityPolicy.TLS_1_2 }); ``` @@ -756,6 +799,10 @@ The following example will map the URL to the `api1` API and to the `api2` API. ```ts +declare const domain: apigateway.DomainName; +declare const api1: apigateway.RestApi; +declare const api2: apigateway.RestApi; + domain.addBasePathMapping(api1, { basePath: 'go-to-api1' }); domain.addBasePathMapping(api2, { basePath: 'boom' }); ``` @@ -764,10 +811,13 @@ You can specify the API `Stage` to which this base path URL will map to. By defa `deploymentStage` of the `RestApi`. ```ts -const betaDeploy = new Deployment(this, 'beta-deployment', { +declare const domain: apigateway.DomainName; +declare const restapi: apigateway.RestApi; + +const betaDeploy = new apigateway.Deployment(this, 'beta-deployment', { api: restapi, }); -const betaStage = new Stage(this, 'beta-stage', { +const betaStage = new apigateway.Stage(this, 'beta-stage', { deployment: betaDeploy, }); domain.addBasePathMapping(restapi, { basePath: 'api/beta', stage: betaStage }); @@ -777,6 +827,8 @@ If you don't specify `basePath`, all URLs under this domain will be mapped to the API, and you won't be able to map another API to the same domain: ```ts +declare const domain: apigateway.DomainName; +declare const api: apigateway.RestApi; domain.addBasePathMapping(api); ``` @@ -786,6 +838,9 @@ domain as demonstrated above. If you wish to setup this domain with an Amazon Route53 alias, use the `targets.ApiGatewayDomain`: ```ts +declare const hostedZoneForExampleCom: any; +declare const domainName: apigateway.DomainName; + import * as route53 from '@aws-cdk/aws-route53'; import * as targets from '@aws-cdk/aws-route53-targets'; @@ -809,17 +864,17 @@ Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-log ```ts // production stage -const prdLogGroup = new cwlogs.LogGroup(this, "PrdLogs"); +const prdLogGroup = new logs.LogGroup(this, "PrdLogs"); const api = new apigateway.RestApi(this, 'books', { deployOptions: { accessLogDestination: new apigateway.LogGroupLogDestination(prdLogGroup), accessLogFormat: apigateway.AccessLogFormat.jsonWithStandardFields() } }) -const deployment = new apigateway.Deployment(stack, 'Deployment', {api}); +const deployment = new apigateway.Deployment(this, 'Deployment', {api}); // development stage -const devLogGroup = new cwlogs.LogGroup(this, "DevLogs"); +const devLogGroup = new logs.LogGroup(this, "DevLogs"); new apigateway.Stage(this, 'dev', { deployment, accessLogDestination: new apigateway.LogGroupLogDestination(devLogGroup), @@ -840,7 +895,7 @@ new apigateway.Stage(this, 'dev', { The following code will generate the access log in the [CLF format](https://en.wikipedia.org/wiki/Common_Log_Format). ```ts -const logGroup = new cwlogs.LogGroup(this, "ApiGatewayAccessLogs"); +const logGroup = new logs.LogGroup(this, "ApiGatewayAccessLogs"); const api = new apigateway.RestApi(this, 'books', { deployOptions: { accessLogDestination: new apigateway.LogGroupLogDestination(logGroup), @@ -852,12 +907,12 @@ You can also configure your own access log format by using the `AccessLogFormat. `AccessLogField` provides commonly used fields. The following code configures access log to contain. ```ts -const logGroup = new cwlogs.LogGroup(this, "ApiGatewayAccessLogs"); +const logGroup = new logs.LogGroup(this, "ApiGatewayAccessLogs"); new apigateway.RestApi(this, 'books', { deployOptions: { accessLogDestination: new apigateway.LogGroupLogDestination(logGroup), accessLogFormat: apigateway.AccessLogFormat.custom( - `${AccessLogField.contextRequestId()} ${AccessLogField.contextErrorMessage()} ${AccessLogField.contextErrorMessageString()}` + `${apigateway.AccessLogField.contextRequestId()} ${apigateway.AccessLogField.contextErrorMessage()} ${apigateway.AccessLogField.contextErrorMessageString()}` ) } }); @@ -924,6 +979,8 @@ The following example will add an OPTIONS method to the `myResource` API resourc only allows GET and PUT HTTP requests from the origin ```ts +declare const myResource: apigateway.Resource; + myResource.addCorsPreflight({ allowOrigins: [ 'https://amazon.com' ], allowMethods: [ 'GET', 'PUT' ] @@ -937,6 +994,8 @@ API reference for a detailed list of supported configuration options. You can specify defaults this at the resource level, in which case they will be applied to the entire resource sub-tree: ```ts +declare const resource: apigateway.Resource; + const subtree = resource.addResource('subtree', { defaultCorsPreflightOptions: { allowOrigins: [ 'https://amazon.com' ] @@ -957,9 +1016,9 @@ API gateway allows you to specify an To define an endpoint type for the API gateway, use `endpointConfiguration` property: ```ts -const api = new apigw.RestApi(stack, 'api', { +const api = new apigateway.RestApi(this, 'api', { endpointConfiguration: { - types: [ apigw.EndpointType.EDGE ] + types: [ apigateway.EndpointType.EDGE ] } }); ``` @@ -972,10 +1031,11 @@ Route53 Alias DNS record which you can use to invoke your private APIs. More inf Here is an example: ```ts -const someEndpoint: IVpcEndpoint = /* Get or Create endpoint here */ -const api = new apigw.RestApi(stack, 'api', { +declare const someEndpoint: ec2.IVpcEndpoint; + +const api = new apigateway.RestApi(this, 'api', { endpointConfiguration: { - types: [ apigw.EndpointType.PRIVATE ], + types: [ apigateway.EndpointType.PRIVATE ], vpcEndpoints: [ someEndpoint ] } }); @@ -998,18 +1058,20 @@ Method. The following code sets up a private integration with a network load balancer - ```ts -const vpc = new ec2.Vpc(stack, 'VPC'); -const nlb = new elbv2.NetworkLoadBalancer(stack, 'NLB', { +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; + +const vpc = new ec2.Vpc(this, 'VPC'); +const nlb = new elbv2.NetworkLoadBalancer(this, 'NLB', { vpc, }); -const link = new apigw.VpcLink(stack, 'link', { +const link = new apigateway.VpcLink(this, 'link', { targets: [nlb], }); -const integration = new apigw.Integration({ - type: apigw.IntegrationType.HTTP_PROXY, +const integration = new apigateway.Integration({ + type: apigateway.IntegrationType.HTTP_PROXY, options: { - connectionType: apigw.ConnectionType.VPC_LINK, + connectionType: apigateway.ConnectionType.VPC_LINK, vpcLink: link, }, }); @@ -1023,9 +1085,7 @@ property. Any existing `VpcLink` resource can be imported into the CDK app via the `VpcLink.fromVpcLinkId()`. ```ts -const stack = new Stack(app, 'my-stack'); - -const awesomeLink = VpcLink.fromVpcLinkId(stack, 'awesome-vpc-link', 'us-east-1_oiuR12Abd'); +const awesomeLink = apigateway.VpcLink.fromVpcLinkId(this, 'awesome-vpc-link', 'us-east-1_oiuR12Abd'); ``` ## Gateway response @@ -1042,7 +1102,7 @@ The following code configures a Gateway Response when the response is 'access de ```ts const api = new apigateway.RestApi(this, 'books-api'); api.addGatewayResponse('test-response', { - type: ResponseType.ACCESS_DENIED, + type: apigateway.ResponseType.ACCESS_DENIED, statusCode: '500', responseHeaders: { 'Access-Control-Allow-Origin': "test.com", @@ -1063,12 +1123,14 @@ OpenAPI](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gatewa The following code creates a REST API using an external OpenAPI definition JSON file - ```ts +declare const integration: apigateway.Integration; + 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', ...); +booksResource.addMethod('GET', integration); ``` It is possible to use the `addResource()` API to define additional API Gateway Resources. @@ -1095,8 +1157,10 @@ By default, `SpecRestApi` will create an edge optimized endpoint. This can be modified as shown below: ```ts +declare const apiDefinition: apigateway.ApiDefinition; + const api = new apigateway.SpecRestApi(this, 'ExampleRestApi', { - // ... + apiDefinition, endpointTypes: [apigateway.EndpointType.PRIVATE] }); ``` @@ -1114,7 +1178,7 @@ The APIs with the `metric` prefix can be used to get reference to specific metri the method below refers to the client side errors metric for this API. ```ts -const api = new apigw.RestApi(stack, 'my-api'); +const api = new apigateway.RestApi(this, 'my-api'); const clientErrorMetric = api.metricClientError(); ``` diff --git a/packages/@aws-cdk/aws-apigateway/lib/access-log.ts b/packages/@aws-cdk/aws-apigateway/lib/access-log.ts index 726a6928315ee..bc73b13d78604 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/access-log.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/access-log.ts @@ -486,15 +486,17 @@ export class AccessLogFormat { * Custom log format. * You can create any log format string. You can easily get the $ context variable by using the methods of AccessLogField. * @param format - * @example custom(JSON.stringify({ - * requestId: AccessLogField.contextRequestId(), - * sourceIp: AccessLogField.contextIdentitySourceIp(), - * method: AccessLogFiled.contextHttpMethod(), - * userContext: { - * sub: AccessLogField.contextAuthorizerClaims('sub'), - * email: AccessLogField.contextAuthorizerClaims('email') - * } - * })) + * @example + * + * apigateway.AccessLogFormat.custom(JSON.stringify({ + * requestId: apigateway.AccessLogField.contextRequestId(), + * sourceIp: apigateway.AccessLogField.contextIdentitySourceIp(), + * method: apigateway.AccessLogField.contextHttpMethod(), + * userContext: { + * sub: apigateway.AccessLogField.contextAuthorizerClaims('sub'), + * email: apigateway.AccessLogField.contextAuthorizerClaims('email') + * } + * })) */ public static custom(format: string): AccessLogFormat { return new AccessLogFormat(format); diff --git a/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts b/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts index 7b138cfcf23d1..850087920f152 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts @@ -21,7 +21,8 @@ export abstract class ApiDefinition { * schema of OpenAPI 2.0 or OpenAPI 3.0 * * @example - * ApiDefinition.fromInline({ + * + * apigateway.ApiDefinition.fromInline({ * openapi: '3.0.2', * paths: { * '/pets': { diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts index 97772fe7eddb5..fb0c9a1e84934 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts @@ -32,8 +32,9 @@ export interface LambdaIntegrationOptions extends IntegrationOptions { * * @example * - * const handler = new lambda.Function(this, 'MyFunction', ...); - * api.addMethod('GET', new LambdaIntegration(handler)); + * declare const resource: apigateway.Resource; + * declare const handler: lambda.Function; + * resource.addMethod('GET', new apigateway.LambdaIntegration(handler)); * */ export class LambdaIntegration extends AwsIntegration { diff --git a/packages/@aws-cdk/aws-apigateway/lib/method.ts b/packages/@aws-cdk/aws-apigateway/lib/method.ts index ff9c4aaef47dd..919d27e6573be 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/method.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -74,15 +74,18 @@ export interface MethodOptions { * * @example * + * declare const api: apigateway.RestApi; + * declare const userLambda: lambda.Function; + * * const userModel: apigateway.Model = api.addModel('UserModel', { * schema: { - * type: apigateway.JsonSchemaType.OBJECT + * type: apigateway.JsonSchemaType.OBJECT, * properties: { * userId: { - * type: apigateway.JsonSchema.STRING + * type: apigateway.JsonSchemaType.STRING * }, * name: { - * type: apigateway.JsonSchema.STRING + * type: apigateway.JsonSchemaType.STRING * } * }, * required: ['userId'] diff --git a/packages/@aws-cdk/aws-apigateway/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-apigateway/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..54f81e82e4460 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/rosetta/default.ts-fixture @@ -0,0 +1,17 @@ +// Fixture with packages imported, but nothing else +import { Construct, Stack } from '@aws-cdk/core'; +import apigateway = require('@aws-cdk/aws-apigateway'); +import cognito = require('@aws-cdk/aws-cognito'); +import lambda = require('@aws-cdk/aws-lambda'); +import iam = require('@aws-cdk/aws-iam'); +import s3 = require('@aws-cdk/aws-s3'); +import ec2 = require('@aws-cdk/aws-ec2'); +import logs = require('@aws-cdk/aws-logs'); + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} 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 index 2607281fbc923..77094eb60b600 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi-import.lit.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi-import.lit.ts @@ -31,7 +31,7 @@ class RootStack extends Stack { }); new DeployStack(this, { restApiId: restApi.restApiId, - methods: [...petsStack.methods, ...booksStack.methods], + methods: petsStack.methods.concat(booksStack.methods), }); new CfnOutput(this, 'PetsURL', { @@ -117,7 +117,11 @@ class DeployStack extends NestedStack { const deployment = new Deployment(this, 'Deployment', { api: RestApi.fromRestApiId(this, 'RestApi', props.restApiId), }); - (props.methods ?? []).forEach((method) => deployment.node.addDependency(method)); + if (props.methods) { + for (const method of props.methods) { + deployment.node.addDependency(method); + } + } new Stage(this, 'Stage', { deployment }); } } diff --git a/packages/@aws-cdk/aws-iam/lib/policy-statement.ts b/packages/@aws-cdk/aws-iam/lib/policy-statement.ts index 14ca172de5506..ecedbe7c2ded5 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy-statement.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy-statement.ts @@ -1,10 +1,11 @@ import * as cdk from '@aws-cdk/core'; +import { AnyPrincipal } from '.'; import { Group } from './group'; import { - AccountPrincipal, AccountRootPrincipal, Anyone, ArnPrincipal, CanonicalUserPrincipal, + AccountPrincipal, AccountRootPrincipal, ArnPrincipal, CanonicalUserPrincipal, FederatedPrincipal, IPrincipal, PrincipalBase, PrincipalPolicyFragment, ServicePrincipal, ServicePrincipalOpts, } from './principals'; -import { mergePrincipal } from './util'; +import { LITERAL_STRING_KEY, mergePrincipal } from './util'; const ensureArrayOrUndefined = (field: any) => { if (field === undefined) { @@ -239,7 +240,7 @@ export class PolicyStatement { * Adds all identities in all accounts ("*") to this policy statement */ public addAnyPrincipal() { - this.addPrincipals(new Anyone()); + this.addPrincipals(new AnyPrincipal()); } // @@ -370,6 +371,11 @@ export class PolicyStatement { function _normPrincipal(principal: { [key: string]: any[] }) { const keys = Object.keys(principal); if (keys.length === 0) { return undefined; } + + if (LITERAL_STRING_KEY in principal) { + return principal[LITERAL_STRING_KEY][0]; + } + const result: any = {}; for (const key of keys) { const normVal = _norm(principal[key]); @@ -600,9 +606,13 @@ class JsonPrincipal extends PrincipalBase { constructor(json: any = { }) { super(); - // special case: if principal is a string, turn it into an "AWS" principal + // special case: if principal is a string, turn it into a "LiteralString" principal, + // so we render the exact same string back out. if (typeof(json) === 'string') { - json = { AWS: json }; + json = { [LITERAL_STRING_KEY]: [json] }; + } + if (typeof(json) !== 'object') { + throw new Error(`JSON IAM principal should be an object, got ${JSON.stringify(json)}`); } this.policyFragment = { diff --git a/packages/@aws-cdk/aws-iam/lib/principals.ts b/packages/@aws-cdk/aws-iam/lib/principals.ts index 2c89f96749324..001792cbcc475 100644 --- a/packages/@aws-cdk/aws-iam/lib/principals.ts +++ b/packages/@aws-cdk/aws-iam/lib/principals.ts @@ -3,7 +3,7 @@ import { Default, RegionInfo } from '@aws-cdk/region-info'; import { IOpenIdConnectProvider } from './oidc-provider'; import { Condition, Conditions, PolicyStatement } from './policy-statement'; import { ISamlProvider } from './saml-provider'; -import { mergePrincipal } from './util'; +import { LITERAL_STRING_KEY, mergePrincipal } from './util'; /** * Any object that has an associated principal that a permission can be granted to @@ -252,6 +252,15 @@ export class PrincipalWithConditions implements IPrincipal { * * This consists of the JSON used in the "Principal" field, and optionally a * set of "Condition"s that need to be applied to the policy. + * + * Generally, a principal looks like: + * + * { '': ['ID', 'ID', ...] } + * + * And this is also the type of the field `principalJson`. However, there is a + * special type of principal that is just the string '*', which is treated + * differently by some services. To represent that principal, `principalJson` + * should contain `{ 'LiteralString': ['*'] }`. */ export class PrincipalPolicyFragment { /** @@ -545,7 +554,14 @@ export class AccountRootPrincipal extends AccountPrincipal { } /** - * A principal representing all identities in all accounts + * A principal representing all AWS identities in all accounts + * + * Some services behave differently when you specify `Principal: '*'` + * or `Principal: { AWS: "*" }` in their resource policy. + * + * `AnyPrincipal` renders to `Principal: { AWS: "*" }`. This is correct + * most of the time, but in cases where you need the other principal, + * use `StarPrincipal` instead. */ export class AnyPrincipal extends ArnPrincipal { constructor() { @@ -563,6 +579,26 @@ export class AnyPrincipal extends ArnPrincipal { */ export class Anyone extends AnyPrincipal { } +/** + * A principal that uses a literal '*' in the IAM JSON language + * + * Some services behave differently when you specify `Principal: "*"` + * or `Principal: { AWS: "*" }` in their resource policy. + * + * `StarPrincipal` renders to `Principal: *`. Most of the time, you + * should use `AnyPrincipal` instead. + */ +export class StarPrincipal extends PrincipalBase { + public readonly policyFragment: PrincipalPolicyFragment = { + principalJson: { [LITERAL_STRING_KEY]: ['*'] }, + conditions: {}, + }; + + public toString() { + return 'StarPrincipal()'; + } +} + /** * Represents a principal that has multiple types of principals. A composite principal cannot * have conditions. i.e. multiple ServicePrincipals that form a composite principal diff --git a/packages/@aws-cdk/aws-iam/lib/util.ts b/packages/@aws-cdk/aws-iam/lib/util.ts index 19fcbffe09639..11ef02a44ff5c 100644 --- a/packages/@aws-cdk/aws-iam/lib/util.ts +++ b/packages/@aws-cdk/aws-iam/lib/util.ts @@ -4,6 +4,8 @@ import { IPolicy } from './policy'; const MAX_POLICY_NAME_LEN = 128; +export const LITERAL_STRING_KEY = 'LiteralString'; + export function undefinedIfEmpty(f: () => string[]): string[] { return Lazy.list({ produce: () => { @@ -67,10 +69,18 @@ export class AttachedPolicies { /** * Merge two dictionaries that represent IAM principals + * + * Does an in-place merge. */ export function mergePrincipal(target: { [key: string]: string[] }, source: { [key: string]: string[] }) { + // If one represents a literal string, the other one must be empty + if ((LITERAL_STRING_KEY in source && !isEmptyObject(target)) || + (LITERAL_STRING_KEY in target && !isEmptyObject(source))) { + throw new Error(`Cannot merge principals ${JSON.stringify(target)} and ${JSON.stringify(source)}; if one uses a literal principal string the other one must be empty`); + } + for (const key of Object.keys(source)) { - target[key] = target[key] || []; + target[key] = target[key] ?? []; let value = source[key]; if (!Array.isArray(value)) { @@ -123,4 +133,8 @@ export class UniqueStringSet implements IResolvable, IPostProcessor { public toString(): string { return Token.asString(this); } +} + +function isEmptyObject(x: { [key: string]: any }): boolean { + return Object.keys(x).length === 0; } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/policy-document.test.ts b/packages/@aws-cdk/aws-iam/test/policy-document.test.ts index c548b1aeb63fd..bf93e31901c6c 100644 --- a/packages/@aws-cdk/aws-iam/test/policy-document.test.ts +++ b/packages/@aws-cdk/aws-iam/test/policy-document.test.ts @@ -764,6 +764,7 @@ describe('IAM policy document', () => { }); }).toThrow(/Statement must be an array/); }); + }); test('adding another condition with the same operator does not delete the original', () => { diff --git a/packages/@aws-cdk/aws-iam/test/policy-statement.test.ts b/packages/@aws-cdk/aws-iam/test/policy-statement.test.ts index 929343ac240c7..09b6e30b1e4c0 100644 --- a/packages/@aws-cdk/aws-iam/test/policy-statement.test.ts +++ b/packages/@aws-cdk/aws-iam/test/policy-statement.test.ts @@ -39,7 +39,32 @@ describe('IAM policy statement', () => { const doc2 = PolicyDocument.fromJson(doc1.toJSON()); expect(stack.resolve(doc2)).toEqual(stack.resolve(doc1)); + }); + + test('should not convert `Principal: *` to `Principal: { AWS: * }`', () => { + const stack = new Stack(); + const s = PolicyStatement.fromJson({ + Action: ['service:action1'], + Principal: '*', + Resource: '*', + }); + + const doc1 = new PolicyDocument(); + doc1.addStatements(s); + + const rendered = stack.resolve(doc1); + expect(rendered).toEqual({ + Statement: [ + { + Action: 'service:action1', + Effect: 'Allow', + Principal: '*', + Resource: '*', + }, + ], + Version: '2012-10-17', + }); }); test('parses a given notPrincipal', () => { diff --git a/packages/@aws-cdk/aws-iam/test/principals.test.ts b/packages/@aws-cdk/aws-iam/test/principals.test.ts index 1914f174adfd4..1bf7d47950875 100644 --- a/packages/@aws-cdk/aws-iam/test/principals.test.ts +++ b/packages/@aws-cdk/aws-iam/test/principals.test.ts @@ -167,6 +167,35 @@ test('SAML principal', () => { }); }); +test('StarPrincipal', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const pol = new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + actions: ['service:action'], + resources: ['*'], + principals: [new iam.StarPrincipal()], + }), + ], + }); + + // THEN + expect(stack.resolve(pol)).toEqual({ + Statement: [ + { + Action: 'service:action', + Effect: 'Allow', + Principal: '*', + Resource: '*', + }, + ], + Version: '2012-10-17', + }); +}); + test('PrincipalWithConditions.addCondition should work', () => { // GIVEN const stack = new Stack(); diff --git a/packages/@aws-cdk/aws-msk/lib/cluster.ts b/packages/@aws-cdk/aws-msk/lib/cluster.ts index be29d3a7a749d..8dfdae4561004 100644 --- a/packages/@aws-cdk/aws-msk/lib/cluster.ts +++ b/packages/@aws-cdk/aws-msk/lib/cluster.ts @@ -540,7 +540,7 @@ export class Cluster extends ClusterBase { new iam.PolicyStatement({ sid: 'Allow access through AWS Secrets Manager for all principals in the account that are authorized to use AWS Secrets Manager', - principals: [new iam.Anyone()], + principals: [new iam.AnyPrincipal()], actions: [ 'kms:Encrypt', 'kms:Decrypt', diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index 8ee6bf55e0aa1..19c814c818182 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -2,6 +2,50 @@ ## New Resource Types + +## Attribute Changes + + +## Property Changes + +* AWS::EKS::Cluster Logging (__deleted__) +* AWS::EKS::Cluster Tags (__deleted__) +* AWS::EKS::Cluster ResourcesVpcConfig.UpdateType (__changed__) + * Old: Mutable + * New: Immutable + +## Property Type Changes + +* AWS::EKS::Cluster.ClusterLogging (__removed__) +* AWS::EKS::Cluster.Logging (__removed__) +* AWS::EKS::Cluster.LoggingTypeConfig (__removed__) +* AWS::EKS::Cluster.Provider (__added__) +* AWS::EKS::Cluster.EncryptionConfig Provider.PrimitiveType (__deleted__) +* AWS::EKS::Cluster.EncryptionConfig Provider.Type (__added__) +* AWS::EKS::Cluster.EncryptionConfig Provider.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::EKS::Cluster.EncryptionConfig Resources.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::EKS::Cluster.KubernetesNetworkConfig ServiceIpv4Cidr.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::EKS::Cluster.ResourcesVpcConfig EndpointPrivateAccess (__deleted__) +* AWS::EKS::Cluster.ResourcesVpcConfig EndpointPublicAccess (__deleted__) +* AWS::EKS::Cluster.ResourcesVpcConfig PublicAccessCidrs (__deleted__) +* AWS::EKS::Cluster.ResourcesVpcConfig SecurityGroupIds.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::EKS::Cluster.ResourcesVpcConfig SubnetIds.UpdateType (__changed__) + * Old: Immutable + * New: Mutable + + +# CloudFormation Resource Specification v43.0.0 + +## New Resource Types + * AWS::Backup::Framework * AWS::Backup::ReportPlan * AWS::Lightsail::Disk diff --git a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json index 3da6206bf3b2b..b5b29349fe4e4 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json +++ b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json @@ -23800,33 +23800,21 @@ } } }, - "AWS::EKS::Cluster.ClusterLogging": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-clusterlogging.html", - "Properties": { - "EnabledTypes": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-clusterlogging.html#cfn-eks-cluster-clusterlogging-enabledtypes", - "ItemType": "LoggingTypeConfig", - "Required": false, - "Type": "List", - "UpdateType": "Mutable" - } - } - }, "AWS::EKS::Cluster.EncryptionConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-encryptionconfig.html", "Properties": { "Provider": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-encryptionconfig.html#cfn-eks-cluster-encryptionconfig-provider", - "PrimitiveType": "Json", "Required": false, - "UpdateType": "Immutable" + "Type": "Provider", + "UpdateType": "Mutable" }, "Resources": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-encryptionconfig.html#cfn-eks-cluster-encryptionconfig-resources", "PrimitiveItemType": "String", "Required": false, "Type": "List", - "UpdateType": "Immutable" + "UpdateType": "Mutable" } } }, @@ -23837,26 +23825,15 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-kubernetesnetworkconfig.html#cfn-eks-cluster-kubernetesnetworkconfig-serviceipv4cidr", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" - } - } - }, - "AWS::EKS::Cluster.Logging": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-logging.html", - "Properties": { - "ClusterLogging": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-logging.html#cfn-eks-cluster-logging-clusterlogging", - "Required": false, - "Type": "ClusterLogging", "UpdateType": "Mutable" } } }, - "AWS::EKS::Cluster.LoggingTypeConfig": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-loggingtypeconfig.html", + "AWS::EKS::Cluster.Provider": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-provider.html", "Properties": { - "Type": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-loggingtypeconfig.html#cfn-eks-cluster-loggingtypeconfig-type", + "KeyArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-provider.html#cfn-eks-cluster-provider-keyarn", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" @@ -23866,38 +23843,19 @@ "AWS::EKS::Cluster.ResourcesVpcConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-resourcesvpcconfig.html", "Properties": { - "EndpointPrivateAccess": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-resourcesvpcconfig.html#cfn-eks-cluster-resourcesvpcconfig-endpointprivateaccess", - "PrimitiveType": "Boolean", - "Required": false, - "UpdateType": "Mutable" - }, - "EndpointPublicAccess": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-resourcesvpcconfig.html#cfn-eks-cluster-resourcesvpcconfig-endpointpublicaccess", - "PrimitiveType": "Boolean", - "Required": false, - "UpdateType": "Mutable" - }, - "PublicAccessCidrs": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-resourcesvpcconfig.html#cfn-eks-cluster-resourcesvpcconfig-publicaccesscidrs", - "PrimitiveItemType": "String", - "Required": false, - "Type": "List", - "UpdateType": "Mutable" - }, "SecurityGroupIds": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-resourcesvpcconfig.html#cfn-eks-cluster-resourcesvpcconfig-securitygroupids", "PrimitiveItemType": "String", "Required": false, "Type": "List", - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "SubnetIds": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-resourcesvpcconfig.html#cfn-eks-cluster-resourcesvpcconfig-subnetids", "PrimitiveItemType": "String", "Required": true, "Type": "List", - "UpdateType": "Immutable" + "UpdateType": "Mutable" } } }, @@ -79081,12 +79039,6 @@ "Type": "KubernetesNetworkConfig", "UpdateType": "Immutable" }, - "Logging": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-cluster.html#cfn-eks-cluster-logging", - "Required": false, - "Type": "Logging", - "UpdateType": "Mutable" - }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-cluster.html#cfn-eks-cluster-name", "PrimitiveType": "String", @@ -79097,7 +79049,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-cluster.html#cfn-eks-cluster-resourcesvpcconfig", "Required": true, "Type": "ResourcesVpcConfig", - "UpdateType": "Mutable" + "UpdateType": "Immutable" }, "RoleArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-cluster.html#cfn-eks-cluster-rolearn", @@ -79105,14 +79057,6 @@ "Required": true, "UpdateType": "Immutable" }, - "Tags": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-cluster.html#cfn-eks-cluster-tags", - "DuplicatesAllowed": false, - "ItemType": "Tag", - "Required": false, - "Type": "List", - "UpdateType": "Mutable" - }, "Version": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-cluster.html#cfn-eks-cluster-version", "PrimitiveType": "String",