From e6c4ca5e71934e890eabe41190e9c2d0bd42aefb Mon Sep 17 00:00:00 2001 From: Ben Chaimberg Date: Thu, 16 Sep 2021 18:31:26 -0400 Subject: [PATCH 1/5] feat(opensearch): rebrand Elasticsearch as OpenSearch Amazon Elasticsearch Service is being rebranded to Amazon OpenSearch Service. All customer-facing references to Elasticsearch within AWS CDK must be replaced with their OpenSearch equivalent, including documentation and API. To avoid breaking existing customers, all API changes will be implemented as a deprecation of the old API and introduction of a new, rebranded API. Concretely, this change consists of the following: - add a disclaimer at the top of the @aws-cdk/aws-elasticsearch module README about the rebranding - add migration instructions to the @aws-cdk/aws-elasticsearch module README - introduce the @aws-cdk/aws-opensearch module - copy all files from the Elasticsearch module - uses new AWS::OpenSearch::Domain resource. CFN guarantees a no-replacement update when a resource moves from AWS::Elasticsearch::Domain to AWS::OpenSearch::Domain, meaning customers should be able to move from the old CDK module to the new one with no downtime. - rename DomainProps.cognitoKibanaAuth -> DomainProps.cognitoDashboardsAuth - replaces ElasticsearchVersion with OpenSearchVersion - ElasticsearchVersion.V{major}_{minor} -> OpenSearchVersion.ELASTICSEARCH_{major}_{minor} - ElasticsearchVersion.of(version: string) -> OpenSearchVersion.elasticsearch(version: string) - introduces OpenSearchVersion.OPENSEARCH_1_0 and OpenSearchVersion.openSearch(version: string) - replace .elasticsearch instance suffix with .search - rename logical ID and type of a custom resource (Custom::ElasticsearchAccessPolicy -> Custom::OpenSearchAccessPolicy) that we generate to update the domain's access policy. This will cause a resource replacement, which will avoid a CloudFormation error (Update of resource type is not permitted) for stacks that switch modules Closes aws/aws-cdk#16467 --- packages/@aws-cdk/aws-elasticsearch/README.md | 102 + .../rosetta/migrate-opensearch.ts-fixture | 16 + ...elasticsearch.custom-kms-key.expected.json | 38 +- .../integ.elasticsearch.custom-kms-key.ts | 2 +- .../test/integ.elasticsearch.expected.json | 18 +- .../aws-opensearchservice/.eslintrc.js | 3 + .../@aws-cdk/aws-opensearchservice/.gitignore | 19 + .../@aws-cdk/aws-opensearchservice/.npmignore | 29 + .../@aws-cdk/aws-opensearchservice/LICENSE | 201 + .../@aws-cdk/aws-opensearchservice/NOTICE | 2 + .../@aws-cdk/aws-opensearchservice/README.md | 311 ++ .../aws-opensearchservice/jest.config.js | 2 + .../aws-opensearchservice/lib/domain.ts | 1749 +++++++ .../aws-opensearchservice/lib/index.ts | 5 + .../lib/log-group-resource-policy.ts | 53 + .../lib/opensearch-access-policy.ts | 53 + .../aws-opensearchservice/lib/perms.ts | 16 + .../aws-opensearchservice/lib/version.ts | 81 + .../aws-opensearchservice/package.json | 131 + .../aws-opensearchservice/test/domain.test.ts | 1742 +++++++ ....opensearch.advancedsecurity.expected.json | 52 + .../test/integ.opensearch.advancedsecurity.ts | 29 + ...eg.opensearch.custom-kms-key.expected.json | 301 ++ .../test/integ.opensearch.custom-kms-key.ts | 37 + .../test/integ.opensearch.expected.json | 624 +++ .../test/integ.opensearch.ts | 51 + .../integ.opensearch.ultrawarm.expected.json | 42 + .../test/integ.opensearch.ultrawarm.ts | 22 + ...opensearch.unsignedbasicauth.expected.json | 259 ++ .../integ.opensearch.unsignedbasicauth.ts | 20 + .../test/integ.opensearch.vpc.expected.json | 591 +++ .../test/integ.opensearch.vpc.ts | 28 + .../test/log-group-resource-policy.test.ts | 65 + .../test/opensearch-access-policy.test.ts | 68 + .../902_OpenSearch_Domain_patch.json | 500 ++ .../cloudformation-include/package.json | 2 + .../opensearch-2021-01-01.paginators.json | 64 + .../opensearch-2021-01-01.service.json | 4080 +++++++++++++++++ .../lib/aws-custom-resource/runtime/index.ts | 39 + packages/aws-cdk-lib/package.json | 1 + packages/decdk/package.json | 1 + packages/monocdk/package.json | 1 + 42 files changed, 11429 insertions(+), 21 deletions(-) create mode 100644 packages/@aws-cdk/aws-elasticsearch/rosetta/migrate-opensearch.ts-fixture create mode 100644 packages/@aws-cdk/aws-opensearchservice/.eslintrc.js create mode 100644 packages/@aws-cdk/aws-opensearchservice/.gitignore create mode 100644 packages/@aws-cdk/aws-opensearchservice/.npmignore create mode 100644 packages/@aws-cdk/aws-opensearchservice/LICENSE create mode 100644 packages/@aws-cdk/aws-opensearchservice/NOTICE create mode 100644 packages/@aws-cdk/aws-opensearchservice/README.md create mode 100644 packages/@aws-cdk/aws-opensearchservice/jest.config.js create mode 100644 packages/@aws-cdk/aws-opensearchservice/lib/domain.ts create mode 100644 packages/@aws-cdk/aws-opensearchservice/lib/index.ts create mode 100644 packages/@aws-cdk/aws-opensearchservice/lib/log-group-resource-policy.ts create mode 100644 packages/@aws-cdk/aws-opensearchservice/lib/opensearch-access-policy.ts create mode 100644 packages/@aws-cdk/aws-opensearchservice/lib/perms.ts create mode 100644 packages/@aws-cdk/aws-opensearchservice/lib/version.ts create mode 100644 packages/@aws-cdk/aws-opensearchservice/package.json create mode 100644 packages/@aws-cdk/aws-opensearchservice/test/domain.test.ts create mode 100644 packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.advancedsecurity.expected.json create mode 100644 packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.advancedsecurity.ts create mode 100644 packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.custom-kms-key.expected.json create mode 100644 packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.custom-kms-key.ts create mode 100644 packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.expected.json create mode 100644 packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.ts create mode 100644 packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.ultrawarm.expected.json create mode 100644 packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.ultrawarm.ts create mode 100644 packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.unsignedbasicauth.expected.json create mode 100644 packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.unsignedbasicauth.ts create mode 100644 packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.vpc.expected.json create mode 100644 packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.vpc.ts create mode 100644 packages/@aws-cdk/aws-opensearchservice/test/log-group-resource-policy.test.ts create mode 100644 packages/@aws-cdk/aws-opensearchservice/test/opensearch-access-policy.test.ts create mode 100644 packages/@aws-cdk/cfnspec/spec-source/902_OpenSearch_Domain_patch.json create mode 100644 packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/aws-sdk-patch/opensearch-2021-01-01.paginators.json create mode 100644 packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/aws-sdk-patch/opensearch-2021-01-01.service.json diff --git a/packages/@aws-cdk/aws-elasticsearch/README.md b/packages/@aws-cdk/aws-elasticsearch/README.md index 7a002f3d2cfde..3de87e175693c 100644 --- a/packages/@aws-cdk/aws-elasticsearch/README.md +++ b/packages/@aws-cdk/aws-elasticsearch/README.md @@ -22,6 +22,8 @@ Higher level constructs for Domain | ![Stable](https://img.shields.io/badge/stab +> Amazon Elasticsearch Service has been renamed to Amazon OpenSearch Service; consequently, the [@aws-cdk/aws-opensearchservice](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-opensearchservice-readme.html) module should be used instead. See [Amazon OpenSearch Service FAQs](https://aws.amazon.com/opensearch-service/faqs/#Name_change) for details. See [Migrating to OpenSearch](#migrating-to-opensearch) for migration instructions. + ## Quick start Create a development cluster by simply specifying the version: @@ -305,3 +307,103 @@ new Domain(stack, 'Domain', { }, }); ``` + +## Migrating to OpenSearch + +To migrate from this module (`@aws-cdk/aws-elasticsearch`) to the new `@aws-cdk/aws-opensearchservice` module, you must modify your CDK application to refer to the new module (including some associated changes) and then perform a CloudFormation resource deletion/import. + +### Necessary CDK Modifications + +Make the following modifications to your CDK application to migrate to the `@aws-cdk/aws-opensearchservice` module. + +- Rewrite module imports to use `'@aws-cdk/aws-opensearchservice` to `'@aws-cdk/aws-elasticsearch`. + For example: + + ```ts nofixture + import * as es from '@aws-cdk/aws-elasticsearch'; + import { Domain } from '@aws-cdk/aws-elasticsearch'; + ``` + + ...becomes... + + ```ts nofixture + import * as opensearch from '@aws-cdk/aws-opensearchservice'; + import { Domain } from '@aws-cdk/aws-opensearchservice'; + ``` + +- Replace instances of `es.ElasticsearchVersion` with `opensearch.EngineVersion`. + For example: + + ```ts fixture=migrate-opensearch + const version = es.ElasticsearchVersion.V7_1; + ``` + + ...becomes... + + ```ts fixture=migrate-opensearch + const version = opensearch.EngineVersion.ELASTICSEARCH_7_1; + ``` + +- Replace the `cognitoKibanaAuth` property of `DomainProps` with `cognitoDashboardsAuth`. + For example: + + ```ts fixture=migrate-opensearch + new es.Domain(this, 'Domain', { + cognitoKibanaAuth: { + identityPoolId: 'test-identity-pool-id', + userPoolId: 'test-user-pool-id', + role: role, + }, + version: elasticsearchVersion, + }); + ``` + + ...becomes... + + ```ts fixture=migrate-opensearch + new opensearch.Domain(this, 'Domain', { + cognitoDashboardsAuth: { + identityPoolId: 'test-identity-pool-id', + userPoolId: 'test-user-pool-id', + role: role, + }, + version: openSearchVersion, + }); + ``` + +- Rewrite instance type suffixes from `.elasticsearch` to `.search`. + For example: + + ```ts fixture=migrate-opensearch + new es.Domain(this, 'Domain', { + capacity: { + masterNodeInstanceType: 'r5.large.elasticsearch', + }, + version: elasticsearchVersion, + }); + ``` + + ...becomes... + + ```ts fixture=migrate-opensearch + new opensearch.Domain(this, 'Domain', { + capacity: { + masterNodeInstanceType: 'r5.large.search', + }, + version: openSearchVersion, + }); + ``` + +- Any `CfnInclude`'d domains will need to be re-written in their original template in + order to be successfully included as a `opensearch.CfnDomain` + +### CloudFormation Migration + +Follow these steps to migrate your application without data loss: + +- Ensure that the [removal policy](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.RemovalPolicy.html) on your domains are set to `RemovalPolicy.RETAIN`. This is the default for the domain construct, so nothing is required unless you have specifically set the removal policy to some other value. +- Remove the domain resource from your CloudFormation stacks by manually modifying the synthesized templates used to create the CloudFormation stacks. This may also involve modifying or deleting dependent resources, such as the custom resources that CDK creates to manage the domain's access policy or any other resource you have connected to the domain. You will need to search for references to each domain's logical ID to determine which other resources refer to it and replace or delete those references. Do not remove resources that are dependencies of the domain or you will have to recreate or import them before importing the domain. After modification, deploy the stacks through the AWS Management Console or using the AWS CLI. +- Migrate your CDK application to use the new `@aws-cdk/aws-opensearchservice` module by applying the necessary modifications listed above. Synthesize your application and obtain the resulting stack templates. +- Copy just the definition of the domain from the "migrated" templates to the corresponding "stripped" templates that you deployed above. [Import](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resource-import-existing-stack.html) the orphaned domains into your CloudFormation stacks using these templates. +- Synthesize and deploy your CDK application to reconfigure/recreate the modified dependent resources. The CloudFormation stacks should now contain the same resources as existed prior to migration. +- Proceed with development as normal! diff --git a/packages/@aws-cdk/aws-elasticsearch/rosetta/migrate-opensearch.ts-fixture b/packages/@aws-cdk/aws-elasticsearch/rosetta/migrate-opensearch.ts-fixture new file mode 100644 index 0000000000000..bb93c1d40f369 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticsearch/rosetta/migrate-opensearch.ts-fixture @@ -0,0 +1,16 @@ +import * as cdk from '@aws-cdk/core'; +import * as es from '@aws-cdk/aws-elasticsearch'; +import * as iam from '@aws-cdk/aws-iam'; +import * as opensearch from '@aws-cdk/aws-opensearchservice'; + +declare const role: iam.IRole; +declare const elasticsearchVersion: es.ElasticsearchVersion; +declare const openSearchVersion: opensearch.EngineVersion; + +class Fixture extends cdk.Construct { + constructor(scope: cdk.Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.expected.json index 181a2ca93d6fe..33066ad9091b6 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.expected.json @@ -244,7 +244,15 @@ { "Ref": "Domain66AC69E0" }, - "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"*\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", { "Ref": "Domain66AC69E0" }, @@ -260,7 +268,15 @@ { "Ref": "Domain66AC69E0" }, - "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"*\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", { "Ref": "Domain66AC69E0" }, @@ -343,7 +359,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters4600faecd25ab407ff0a9d16f935c93062aaea5d415e97046bb8befe6c8ec02cS3BucketD609D0D9" + "Ref": "AssetParametersf4b39c228007db80daf4497318957f3b455415dce70fdbb7aeb6151a0b6da924S3BucketA716C641" }, "S3Key": { "Fn::Join": [ @@ -356,7 +372,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters4600faecd25ab407ff0a9d16f935c93062aaea5d415e97046bb8befe6c8ec02cS3VersionKey77CF589B" + "Ref": "AssetParametersf4b39c228007db80daf4497318957f3b455415dce70fdbb7aeb6151a0b6da924S3VersionKey2B40A946" } ] } @@ -369,7 +385,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters4600faecd25ab407ff0a9d16f935c93062aaea5d415e97046bb8befe6c8ec02cS3VersionKey77CF589B" + "Ref": "AssetParametersf4b39c228007db80daf4497318957f3b455415dce70fdbb7aeb6151a0b6da924S3VersionKey2B40A946" } ] } @@ -396,17 +412,17 @@ } }, "Parameters": { - "AssetParameters4600faecd25ab407ff0a9d16f935c93062aaea5d415e97046bb8befe6c8ec02cS3BucketD609D0D9": { + "AssetParametersf4b39c228007db80daf4497318957f3b455415dce70fdbb7aeb6151a0b6da924S3BucketA716C641": { "Type": "String", - "Description": "S3 bucket for asset \"4600faecd25ab407ff0a9d16f935c93062aaea5d415e97046bb8befe6c8ec02c\"" + "Description": "S3 bucket for asset \"f4b39c228007db80daf4497318957f3b455415dce70fdbb7aeb6151a0b6da924\"" }, - "AssetParameters4600faecd25ab407ff0a9d16f935c93062aaea5d415e97046bb8befe6c8ec02cS3VersionKey77CF589B": { + "AssetParametersf4b39c228007db80daf4497318957f3b455415dce70fdbb7aeb6151a0b6da924S3VersionKey2B40A946": { "Type": "String", - "Description": "S3 key for asset version \"4600faecd25ab407ff0a9d16f935c93062aaea5d415e97046bb8befe6c8ec02c\"" + "Description": "S3 key for asset version \"f4b39c228007db80daf4497318957f3b455415dce70fdbb7aeb6151a0b6da924\"" }, - "AssetParameters4600faecd25ab407ff0a9d16f935c93062aaea5d415e97046bb8befe6c8ec02cArtifactHash86CFA15D": { + "AssetParametersf4b39c228007db80daf4497318957f3b455415dce70fdbb7aeb6151a0b6da924ArtifactHash2B031F57": { "Type": "String", - "Description": "Artifact hash for asset \"4600faecd25ab407ff0a9d16f935c93062aaea5d415e97046bb8befe6c8ec02c\"" + "Description": "Artifact hash for asset \"f4b39c228007db80daf4497318957f3b455415dce70fdbb7aeb6151a0b6da924\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.ts b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.ts index b18ff225d3cd9..73475672f1716 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.ts @@ -33,7 +33,7 @@ class TestStack extends Stack { new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['es:ESHttp*'], - principals: [new iam.AnyPrincipal()], + principals: [new iam.AccountRootPrincipal()], resources: ['*'], }), ], diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json index 4299402202092..e976e19e502a2 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json @@ -296,7 +296,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3BucketB17E5ABD" + "Ref": "AssetParametersf2b7671fc0b80f63e3c94441727cbff799bb0f4991339d990d7b4c419e1ab3ddS3Bucket292EB571" }, "S3Key": { "Fn::Join": [ @@ -309,7 +309,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3VersionKey77778F6A" + "Ref": "AssetParametersf2b7671fc0b80f63e3c94441727cbff799bb0f4991339d990d7b4c419e1ab3ddS3VersionKeyCE9A5F79" } ] } @@ -322,7 +322,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3VersionKey77778F6A" + "Ref": "AssetParametersf2b7671fc0b80f63e3c94441727cbff799bb0f4991339d990d7b4c419e1ab3ddS3VersionKeyCE9A5F79" } ] } @@ -608,17 +608,17 @@ } }, "Parameters": { - "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3BucketB17E5ABD": { + "AssetParametersf2b7671fc0b80f63e3c94441727cbff799bb0f4991339d990d7b4c419e1ab3ddS3Bucket292EB571": { "Type": "String", - "Description": "S3 bucket for asset \"5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4\"" + "Description": "S3 bucket for asset \"f2b7671fc0b80f63e3c94441727cbff799bb0f4991339d990d7b4c419e1ab3dd\"" }, - "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3VersionKey77778F6A": { + "AssetParametersf2b7671fc0b80f63e3c94441727cbff799bb0f4991339d990d7b4c419e1ab3ddS3VersionKeyCE9A5F79": { "Type": "String", - "Description": "S3 key for asset version \"5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4\"" + "Description": "S3 key for asset version \"f2b7671fc0b80f63e3c94441727cbff799bb0f4991339d990d7b4c419e1ab3dd\"" }, - "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4ArtifactHash580E429C": { + "AssetParametersf2b7671fc0b80f63e3c94441727cbff799bb0f4991339d990d7b4c419e1ab3ddArtifactHash86854188": { "Type": "String", - "Description": "Artifact hash for asset \"5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4\"" + "Description": "Artifact hash for asset \"f2b7671fc0b80f63e3c94441727cbff799bb0f4991339d990d7b4c419e1ab3dd\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-opensearchservice/.eslintrc.js b/packages/@aws-cdk/aws-opensearchservice/.eslintrc.js new file mode 100644 index 0000000000000..61dd8dd001f63 --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-opensearchservice/.gitignore b/packages/@aws-cdk/aws-opensearchservice/.gitignore new file mode 100644 index 0000000000000..62ebc95d75ce6 --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk +nyc.config.js +!.eslintrc.js +!jest.config.js +junit.xml diff --git a/packages/@aws-cdk/aws-opensearchservice/.npmignore b/packages/@aws-cdk/aws-opensearchservice/.npmignore new file mode 100644 index 0000000000000..f931fede67c44 --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/.npmignore @@ -0,0 +1,29 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json + +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml +test/ +!*.lit.ts diff --git a/packages/@aws-cdk/aws-opensearchservice/LICENSE b/packages/@aws-cdk/aws-opensearchservice/LICENSE new file mode 100644 index 0000000000000..28e4bdcec77ec --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-opensearchservice/NOTICE b/packages/@aws-cdk/aws-opensearchservice/NOTICE new file mode 100644 index 0000000000000..5fc3826926b5b --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-opensearchservice/README.md b/packages/@aws-cdk/aws-opensearchservice/README.md new file mode 100644 index 0000000000000..dddddf5b24f09 --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/README.md @@ -0,0 +1,311 @@ +# Amazon OpenSearch Service Construct Library + + +--- + +Features | Stability +-----------------------------------|---------------------------------------------------------------- +CFN Resources | ![Stable](https://img.shields.io/badge/stable-success.svg?style=for-the-badge) +Higher level constructs for Domain | ![Stable](https://img.shields.io/badge/stable-success.svg?style=for-the-badge) + +> **CFN Resources:** All classes with the `Cfn` prefix in this module ([CFN Resources]) are always +> stable and safe to use. +> +> [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib + + + +> **Stable:** Higher level constructs in this module that are marked stable will not undergo any +> breaking changes. They will strictly follow the [Semantic Versioning](https://semver.org/) model. + +--- + + + +Amazon OpenSearch Service is the successor to Amazon Elasticsearch Service. + +See [Migrating to OpenSearch](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-elasticsearch-readme.html#migrating-to-opensearch) for migration instructions from `@aws-cdk/aws-elasticsearch` to this module, `@aws-cdk/aws-opensearch`. + +## Quick start + +Create a development cluster by simply specifying the version: + +```ts +import * as opensearch from '@aws-cdk/aws-opensearch'; + +const devDomain = new opensearch.Domain(this, 'Domain', { + version: opensearch.EngineVersion.OPENSEARCH_1_0, +}); +``` + +To perform version upgrades without replacing the entire domain, specify the `enableVersionUpgrade` property. + +```ts +import * as opensearch from '@aws-cdk/aws-opensearch'; + +const devDomain = new opensearch.Domain(this, 'Domain', { + version: opensearch.EngineVersion.OPENSEARCH_1_0, + enableVersionUpgrade: true // defaults to false +}); +``` + +Create a production grade cluster by also specifying things like capacity and az distribution + +```ts +const prodDomain = new opensearch.Domain(this, 'Domain', { + version: opensearch.EngineVersion.OPENSEARCH_1_0, + capacity: { + masterNodes: 5, + dataNodes: 20 + }, + ebs: { + volumeSize: 20 + }, + zoneAwareness: { + availabilityZoneCount: 3 + }, + logging: { + slowSearchLogEnabled: true, + appLogEnabled: true, + slowIndexLogEnabled: true, + }, +}); +``` + +This creates an Amazon OpenSearch Service cluster and automatically sets up log groups for +logging the domain logs and slow search logs. + +## A note about SLR + +Some cluster configurations (e.g VPC access) require the existence of the [`AWSServiceRoleForAmazonElasticsearchService`](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/slr.html) Service-Linked Role. + +When performing such operations via the AWS Console, this SLR is created automatically when needed. However, this is not the behavior when using CloudFormation. If an SLR is needed, but doesn't exist, you will encounter a failure message simlar to: + +```console +Before you can proceed, you must enable a service-linked role to give Amazon OpenSearch Service... +``` + +To resolve this, you need to [create](https://docs.aws.amazon.com/IAM/latest/UserGuide/using-service-linked-roles.html#create-service-linked-role) the SLR. We recommend using the AWS CLI: + +```console +aws iam create-service-linked-role --aws-service-name es.amazonaws.com +``` + +You can also create it using the CDK, **but note that only the first application deploying this will succeed**: + +```ts +const slr = new iam.CfnServiceLinkedRole(this, 'Service Linked Role', { + awsServiceName: 'es.amazonaws.com' +}); +``` + +## Importing existing domains + +To import an existing domain into your CDK application, use the `Domain.fromDomainEndpoint` factory method. +This method accepts a domain endpoint of an already existing domain: + +```ts +const domainEndpoint = 'https://my-domain-jcjotrt6f7otem4sqcwbch3c4u.us-east-1.es.amazonaws.com'; +const domain = Domain.fromDomainEndpoint(this, 'ImportedDomain', domainEndpoint); +``` + +## Permissions + +### IAM + +Helper methods also exist for managing access to the domain. + +```ts +const lambda = new lambda.Function(this, 'Lambda', { /* ... */ }); + +// Grant write access to the app-search index +domain.grantIndexWrite('app-search', lambda); + +// Grant read access to the 'app-search/_search' path +domain.grantPathRead('app-search/_search', lambda); +``` + +## Encryption + +The domain can also be created with encryption enabled: + +```ts +const domain = new opensearch.Domain(this, 'Domain', { + version: opensearch.EngineVersion.OPENSEARCH_1_0, + ebs: { + volumeSize: 100, + volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, + }, + nodeToNodeEncryption: true, + encryptionAtRest: { + enabled: true, + }, +}); +``` + +This sets up the domain with node to node encryption and encryption at +rest. You can also choose to supply your own KMS key to use for encryption at +rest. + +## VPC Support + +Domains can be placed inside a VPC, providing a secure communication between Amazon OpenSearch Service and other services within the VPC without the need for an internet gateway, NAT device, or VPN connection. + +> Visit [VPC Support for Amazon OpenSearch Service Domains](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/vpc.html) for more details. + +```ts +const vpc = new ec2.Vpc(this, 'Vpc'); +const domainProps: opensearch.DomainProps = { + version: opensearch.EngineVersion.OPENSEARCH_1_0, + removalPolicy: RemovalPolicy.DESTROY, + vpc, + // must be enabled since our VPC contains multiple private subnets. + zoneAwareness: { + enabled: true, + }, + capacity: { + // must be an even number since the default az count is 2. + dataNodes: 2, + }, +}; +new opensearch.Domain(this, 'Domain', domainProps); +``` + +In addition, you can use the `vpcSubnets` property to control which specific subnets will be used, and the `securityGroups` property to control +which security groups will be attached to the domain. By default, CDK will select all *private* subnets in the VPC, and create one dedicated security group. + +## Metrics + +Helper methods exist to access common domain metrics for example: + +```ts +const freeStorageSpace = domain.metricFreeStorageSpace(); +const masterSysMemoryUtilization = domain.metric('MasterSysMemoryUtilization'); +``` + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +## Fine grained access control + +The domain can also be created with a master user configured. The password can +be supplied or dynamically created if not supplied. + +```ts +const domain = new opensearch.Domain(this, 'Domain', { + version: opensearch.EngineVersion.OPENSEARCH_1_0, + enforceHttps: true, + nodeToNodeEncryption: true, + encryptionAtRest: { + enabled: true, + }, + fineGrainedAccessControl: { + masterUserName: 'master-user', + }, +}); + +const masterUserPassword = domain.masterUserPassword; +``` + +## Using unsigned basic auth + +For convenience, the domain can be configured to allow unsigned HTTP requests +that use basic auth. Unless the domain is configured to be part of a VPC this +means anyone can access the domain using the configured master username and +password. + +To enable unsigned basic auth access the domain is configured with an access +policy that allows anyonmous requests, HTTPS required, node to node encryption, +encryption at rest and fine grained access control. + +If the above settings are not set they will be configured as part of enabling +unsigned basic auth. If they are set with conflicting values, an error will be +thrown. + +If no master user is configured a default master user is created with the +username `admin`. + +If no password is configured a default master user password is created and +stored in the AWS Secrets Manager as secret. The secret has the prefix +`MasterUser`. + +```ts +const domain = new opensearch.Domain(this, 'Domain', { + version: opensearch.EngineVersion.OPENSEARCH_1_0, + useUnsignedBasicAuth: true, +}); + +const masterUserPassword = domain.masterUserPassword; +``` + + + +## Audit logs + +Audit logs can be enabled for a domain, but only when fine grained access control is enabled. + +```ts +const domain = new opensearch.Domain(this, 'Domain', { + version: opensearch.EngineVersion.OPENSEARCH_1_0, + enforceHttps: true, + nodeToNodeEncryption: true, + encryptionAtRest: { + enabled: true, + }, + fineGrainedAccessControl: { + masterUserName: 'master-user', + }, + logging: { + auditLogEnabled: true, + slowSearchLogEnabled: true, + appLogEnabled: true, + slowIndexLogEnabled: true, + }, +}); +``` + +## UltraWarm + +UltraWarm nodes can be enabled to provide a cost-effective way to store large amounts of read-only data. + +```ts +const domain = new opensearch.Domain(this, 'Domain', { + version: opensearch.EngineVersion.OPENSEARCH_1_0, + capacity: { + masterNodes: 2, + warmNodes: 2, + warmInstanceType: 'ultrawarm1.medium.search', + }, +}); +``` + +## Custom endpoint + +Custom endpoints can be configured to reach the domain under a custom domain name. + +```ts +new Domain(stack, 'Domain', { + version: EngineVersion.OPENSEARCH_1_0, + customEndpoint: { + domainName: 'search.example.com', + }, +}); +``` + +It is also possible to specify a custom certificate instead of the auto-generated one. + +Additionally, an automatic CNAME-Record is created if a hosted zone is provided for the custom endpoint + +## Advanced options + +[Advanced options](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/createupdatedomains.html#createdomain-configure-advanced-options) can used to configure additional options. + +```ts +new Domain(stack, 'Domain', { + version: EngineVersion.OPENSEARCH_1_0, + advancedOptions: { + 'rest.action.multi.allow_explicit_index': 'false', + 'indices.fielddata.cache.size': '25', + 'indices.query.bool.max_clause_count': '2048', + }, +}); +``` diff --git a/packages/@aws-cdk/aws-opensearchservice/jest.config.js b/packages/@aws-cdk/aws-opensearchservice/jest.config.js new file mode 100644 index 0000000000000..54e28beb9798b --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-opensearchservice/lib/domain.ts b/packages/@aws-cdk/aws-opensearchservice/lib/domain.ts new file mode 100644 index 0000000000000..1279d9525f831 --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/lib/domain.ts @@ -0,0 +1,1749 @@ +import { URL } from 'url'; + +import * as acm from '@aws-cdk/aws-certificatemanager'; +import { Metric, MetricOptions, Statistic } from '@aws-cdk/aws-cloudwatch'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; +import * as logs from '@aws-cdk/aws-logs'; +import * as route53 from '@aws-cdk/aws-route53'; +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +import { LogGroupResourcePolicy } from './log-group-resource-policy'; +import { OpenSearchAccessPolicy } from './opensearch-access-policy'; +import { CfnDomain } from './opensearchservice.generated'; +import * as perms from './perms'; +import { EngineVersion } from './version'; + +/** + * Configures the capacity of the cluster such as the instance type and the + * number of instances. + */ +export interface CapacityConfig { + /** + * The number of instances to use for the master node. + * + * @default - no dedicated master nodes + */ + readonly masterNodes?: number; + + /** + * The hardware configuration of the computer that hosts the dedicated master + * node, such as `m3.medium.search`. For valid values, see [Supported + * Instance Types] + * (https://docs.aws.amazon.com/opensearch-service/latest/developerguide/supported-instance-types.html) + * in the Amazon OpenSearch Service Developer Guide. + * + * @default - r5.large.search + */ + readonly masterNodeInstanceType?: string; + + /** + * The number of data nodes (instances) to use in the Amazon OpenSearch Service domain. + * + * @default - 1 + */ + readonly dataNodes?: number; + + /** + * The instance type for your data nodes, such as + * `m3.medium.search`. For valid values, see [Supported Instance + * Types](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/supported-instance-types.html) + * in the Amazon OpenSearch Service Developer Guide. + * + * @default - r5.large.search + */ + readonly dataNodeInstanceType?: string; + + /** + * The number of UltraWarm nodes (instances) to use in the Amazon OpenSearch Service domain. + * + * @default - no UltraWarm nodes + */ + readonly warmNodes?: number; + + /** + * The instance type for your UltraWarm node, such as `ultrawarm1.medium.search`. + * For valid values, see [UltraWarm Storage Limits] + * (https://docs.aws.amazon.com/opensearch-service/latest/developerguide/limits.html#limits-ultrawarm) + * in the Amazon OpenSearch Service Developer Guide. + * + * @default - ultrawarm1.medium.search + */ + readonly warmInstanceType?: string; + +} + +/** + * Specifies zone awareness configuration options. + */ +export interface ZoneAwarenessConfig { + /** + * Indicates whether to enable zone awareness for the Amazon OpenSearch Service domain. + * When you enable zone awareness, Amazon OpenSearch Service allocates the nodes and replica + * index shards that belong to a cluster across two Availability Zones (AZs) + * in the same region to prevent data loss and minimize downtime in the event + * of node or data center failure. Don't enable zone awareness if your cluster + * has no replica index shards or is a single-node cluster. For more information, + * see [Configuring a Multi-AZ Domain] + * (https://docs.aws.amazon.com/opensearch-service/latest/developerguide/managedomains-multiaz.html) + * in the Amazon OpenSearch Service Developer Guide. + * + * @default - false + */ + readonly enabled?: boolean; + + /** + * If you enabled multiple Availability Zones (AZs), the number of AZs that you + * want the domain to use. Valid values are 2 and 3. + * + * @default - 2 if zone awareness is enabled. + */ + readonly availabilityZoneCount?: number; +} + +/** + * The configurations of Amazon Elastic Block Store (Amazon EBS) volumes that + * are attached to data nodes in the Amazon OpenSearch Service domain. For more information, see + * [Amazon EBS] + * (https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AmazonEBS.html) + * in the Amazon Elastic Compute Cloud Developer Guide. + */ +export interface EbsOptions { + /** + * Specifies whether Amazon EBS volumes are attached to data nodes in the + * Amazon OpenSearch Service domain. + * + * @default - true + */ + readonly enabled?: boolean; + + /** + * The number of I/O operations per second (IOPS) that the volume + * supports. This property applies only to the Provisioned IOPS (SSD) EBS + * volume type. + * + * @default - iops are not set. + */ + readonly iops?: number; + + /** + * The size (in GiB) of the EBS volume for each data node. The minimum and + * maximum size of an EBS volume depends on the EBS volume type and the + * instance type to which it is attached. For valid values, see + * [EBS volume size limits] + * (https://docs.aws.amazon.com/opensearch-service/latest/developerguide/limits.html#ebsresource) + * in the Amazon OpenSearch Service Developer Guide. + * + * @default 10 + */ + readonly volumeSize?: number; + + /** + * The EBS volume type to use with the Amazon OpenSearch Service domain, such as standard, gp2, io1. + * + * @default gp2 + */ + readonly volumeType?: ec2.EbsDeviceVolumeType; +} + +/** + * Configures log settings for the domain. + */ +export interface LoggingOptions { + /** + * Specify if slow search logging should be set up. + * Requires Elasticsearch version 5.1 or later or OpenSearch version 1.0 or later. + * + * @default - false + */ + readonly slowSearchLogEnabled?: boolean; + + /** + * Log slow searches to this log group. + * + * @default - a new log group is created if slow search logging is enabled + */ + readonly slowSearchLogGroup?: logs.ILogGroup; + + /** + * Specify if slow index logging should be set up. + * Requires Elasticsearch version 5.1 or later or OpenSearch version 1.0 or later. + * + * @default - false + */ + readonly slowIndexLogEnabled?: boolean; + + /** + * Log slow indices to this log group. + * + * @default - a new log group is created if slow index logging is enabled + */ + readonly slowIndexLogGroup?: logs.ILogGroup; + + /** + * Specify if Amazon OpenSearch Service application logging should be set up. + * Requires Elasticsearch version 5.1 or later or OpenSearch version 1.0 or later. + * + * @default - false + */ + readonly appLogEnabled?: boolean; + + /** + * Log Amazon OpenSearch Service application logs to this log group. + * + * @default - a new log group is created if app logging is enabled + */ + readonly appLogGroup?: logs.ILogGroup; + + /** + * Specify if Amazon OpenSearch Service audit logging should be set up. + * Requires Elasticsearch version 6.7 or later or OpenSearch version 1.0 or later and fine grained access control to be enabled. + * + * @default - false + */ + readonly auditLogEnabled?: boolean; + + /** + * Log Amazon OpenSearch Service audit logs to this log group. + * + * @default - a new log group is created if audit logging is enabled + */ + readonly auditLogGroup?: logs.ILogGroup; +} + +/** + * Whether the domain should encrypt data at rest, and if so, the AWS Key + * Management Service (KMS) key to use. Can only be used to create a new domain, + * not update an existing one. Requires Elasticsearch version 5.1 or later or OpenSearch version 1.0 or later. + */ +export interface EncryptionAtRestOptions { + /** + * Specify true to enable encryption at rest. + * + * @default - encryption at rest is disabled. + */ + readonly enabled?: boolean; + + /** + * Supply if using KMS key for encryption at rest. + * + * @default - uses default aws/es KMS key. + */ + readonly kmsKey?: kms.IKey; +} + +/** + * Configures Amazon OpenSearch Service to use Amazon Cognito authentication for OpenSearch Dashboards. + * @see https://docs.aws.amazon.com/opensearch-service/latest/developerguide/cognito-auth.html + */ +export interface CognitoOptions { + /** + * The Amazon Cognito identity pool ID that you want Amazon OpenSearch Service to use for OpenSearch Dashboards authentication. + */ + readonly identityPoolId: string; + + /** + * A role that allows Amazon OpenSearch Service to configure your user pool and identity pool. It must have the `AmazonESCognitoAccess` policy attached to it. + * + * @see https://docs.aws.amazon.com/opensearch-service/latest/developerguide/cognito-auth.html#cognito-auth-prereq + */ + readonly role: iam.IRole; + + /** + * The Amazon Cognito user pool ID that you want Amazon OpenSearch Service to use for OpenSearch Dashboards authentication. + */ + readonly userPoolId: string; +} + +/** + * The minimum TLS version required for traffic to the domain. + */ +export enum TLSSecurityPolicy { + /** Cipher suite TLS 1.0 */ + TLS_1_0 = 'Policy-Min-TLS-1-0-2019-07', + /** Cipher suite TLS 1.2 */ + TLS_1_2 = 'Policy-Min-TLS-1-2-2019-07' +} + +/** + * Specifies options for fine-grained access control. + */ +export interface AdvancedSecurityOptions { + /** + * ARN for the master user. Only specify this or masterUserName, but not both. + * + * @default - fine-grained access control is disabled + */ + readonly masterUserArn?: string; + + /** + * Username for the master user. Only specify this or masterUserArn, but not both. + * + * @default - fine-grained access control is disabled + */ + readonly masterUserName?: string; + + /** + * Password for the master user. + * + * You can use `SecretValue.plainText` to specify a password in plain text or + * use `secretsmanager.Secret.fromSecretAttributes` to reference a secret in + * Secrets Manager. + * + * @default - A Secrets Manager generated password + */ + readonly masterUserPassword?: cdk.SecretValue; +} + +/** + * Configures a custom domain endpoint for the Amazon OpenSearch Service domain + */ +export interface CustomEndpointOptions { + /** + * The custom domain name to assign + */ + readonly domainName: string; + + /** + * The certificate to use + * @default - create a new one + */ + readonly certificate?: acm.ICertificate; + + /** + * The hosted zone in Route53 to create the CNAME record in + * @default - do not create a CNAME + */ + readonly hostedZone?: route53.IHostedZone; +} + +/** + * Properties for an Amazon OpenSearch Service domain. + */ +export interface DomainProps { + /** + * Domain access policies. + * + * @default - No access policies. + */ + readonly accessPolicies?: iam.PolicyStatement[]; + + /** + * Additional options to specify for the Amazon OpenSearch Service domain. + * + * @see https://docs.aws.amazon.com/opensearch-service/latest/developerguide/createupdatedomains.html#createdomain-configure-advanced-options + * @default - no advanced options are specified + */ + readonly advancedOptions?: { [key: string]: (string) }; + + /** + * Configures Amazon OpenSearch Service to use Amazon Cognito authentication for OpenSearch Dashboards. + * + * @default - Cognito not used for authentication to OpenSearch Dashboards. + */ + readonly cognitoDashboardsAuth?: CognitoOptions; + + /** + * Enforces a particular physical domain name. + * + * @default - A name will be auto-generated. + */ + readonly domainName?: string; + + /** + * The configurations of Amazon Elastic Block Store (Amazon EBS) volumes that + * are attached to data nodes in the Amazon OpenSearch Service domain. + * + * @default - 10 GiB General Purpose (SSD) volumes per node. + */ + readonly ebs?: EbsOptions; + + /** + * The cluster capacity configuration for the Amazon OpenSearch Service domain. + * + * @default - 1 r5.large.search data node; no dedicated master nodes. + */ + readonly capacity?: CapacityConfig; + + /** + * The cluster zone awareness configuration for the Amazon OpenSearch Service domain. + * + * @default - no zone awareness (1 AZ) + */ + readonly zoneAwareness?: ZoneAwarenessConfig; + + /** + * The Elasticsearch/OpenSearch version that your domain will leverage. + */ + readonly version: EngineVersion; + + /** + * Encryption at rest options for the cluster. + * + * @default - No encryption at rest + */ + readonly encryptionAtRest?: EncryptionAtRestOptions; + + /** + * Configuration log publishing configuration options. + * + * @default - No logs are published + */ + readonly logging?: LoggingOptions; + + /** + * Specify true to enable node to node encryption. + * Requires Elasticsearch version 6.0 or later or OpenSearch version 1.0 or later. + * + * @default - Node to node encryption is not enabled. + */ + readonly nodeToNodeEncryption?: boolean; + + /** + * The hour in UTC during which the service takes an automated daily snapshot + * of the indices in the Amazon OpenSearch Service domain. Only applies for Elasticsearch versions + * below 5.3. + * + * @default - Hourly automated snapshots not used + */ + readonly automatedSnapshotStartHour?: number; + + /** + * Place the domain inside this VPC. + * + * @see https://docs.aws.amazon.com/opensearch-service/latest/developerguide/vpc.html + * @default - Domain is not placed in a VPC. + */ + readonly vpc?: ec2.IVpc; + + /** + * The list of security groups that are associated with the VPC endpoints + * for the domain. + * + * Only used if `vpc` is specified. + * + * @see https://docs.aws.amazon.com/vpc/latest/userguide/VPC_SecurityGroups.html + * @default - One new security group is created. + */ + readonly securityGroups?: ec2.ISecurityGroup[]; + + /** + * The specific vpc subnets the domain will be placed in. You must provide one subnet for each Availability Zone + * that your domain uses. For example, you must specify three subnet IDs for a three Availability Zone + * domain. + * + * Only used if `vpc` is specified. + * + * @see https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Subnets.html + * @default - All private subnets. + */ + readonly vpcSubnets?: ec2.SubnetSelection[]; + + /** + * True to require that all traffic to the domain arrive over HTTPS. + * + * @default - false + */ + readonly enforceHttps?: boolean; + + /** + * The minimum TLS version required for traffic to the domain. + * + * @default - TLSSecurityPolicy.TLS_1_0 + */ + readonly tlsSecurityPolicy?: TLSSecurityPolicy; + + /** + * Specifies options for fine-grained access control. + * Requires Elasticsearch version 6.7 or later or OpenSearch version 1.0 or later. Enabling fine-grained access control + * also requires encryption of data at rest and node-to-node encryption, along with + * enforced HTTPS. + * + * @default - fine-grained access control is disabled + */ + readonly fineGrainedAccessControl?: AdvancedSecurityOptions; + + /** + * Configures the domain so that unsigned basic auth is enabled. If no master user is provided a default master user + * with username `admin` and a dynamically generated password stored in KMS is created. The password can be retrieved + * by getting `masterUserPassword` from the domain instance. + * + * Setting this to true will also add an access policy that allows unsigned + * access, enable node to node encryption, encryption at rest. If conflicting + * settings are encountered (like disabling encryption at rest) enabling this + * setting will cause a failure. + * + * @default - false + */ + readonly useUnsignedBasicAuth?: boolean; + + /** + * To upgrade an Amazon OpenSearch Service domain to a new version, rather than replacing the entire + * domain resource, use the EnableVersionUpgrade update policy. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-updatepolicy.html#cfn-attributes-updatepolicy-upgradeopensearchdomain + * @default - false + */ + readonly enableVersionUpgrade?: boolean; + + /** + * Policy to apply when the domain is removed from the stack + * + * @default RemovalPolicy.RETAIN + */ + readonly removalPolicy?: cdk.RemovalPolicy; + + /** + * To configure a custom domain configure these options + * + * If you specify a Route53 hosted zone it will create a CNAME record and use DNS validation for the certificate + * @default - no custom domain endpoint will be configured + */ + readonly customEndpoint?: CustomEndpointOptions; +} + +/** + * An interface that represents an Amazon OpenSearch Service domain - either created with the CDK, or an existing one. + */ +export interface IDomain extends cdk.IResource { + /** + * Arn of the Amazon OpenSearch Service domain. + * + * @attribute + */ + readonly domainArn: string; + + /** + * Domain name of the Amazon OpenSearch Service domain. + * + * @attribute + */ + readonly domainName: string; + + /** + * Identifier of the Amazon OpenSearch Service domain. + * + * @attribute + */ + readonly domainId: string; + + /** + * Endpoint of the Amazon OpenSearch Service domain. + * + * @attribute + */ + readonly domainEndpoint: string; + + /** + * Grant read permissions for this domain and its contents to an IAM + * principal (Role/Group/User). + * + * @param identity The principal + */ + grantRead(identity: iam.IGrantable): iam.Grant; + + /** + * Grant write permissions for this domain and its contents to an IAM + * principal (Role/Group/User). + * + * @param identity The principal + */ + grantWrite(identity: iam.IGrantable): iam.Grant; + + /** + * Grant read/write permissions for this domain and its contents to an IAM + * principal (Role/Group/User). + * + * @param identity The principal + */ + grantReadWrite(identity: iam.IGrantable): iam.Grant; + + /** + * Grant read permissions for an index in this domain to an IAM + * principal (Role/Group/User). + * + * @param index The index to grant permissions for + * @param identity The principal + */ + grantIndexRead(index: string, identity: iam.IGrantable): iam.Grant; + + /** + * Grant write permissions for an index in this domain to an IAM + * principal (Role/Group/User). + * + * @param index The index to grant permissions for + * @param identity The principal + */ + grantIndexWrite(index: string, identity: iam.IGrantable): iam.Grant; + + /** + * Grant read/write permissions for an index in this domain to an IAM + * principal (Role/Group/User). + * + * @param index The index to grant permissions for + * @param identity The principal + */ + grantIndexReadWrite(index: string, identity: iam.IGrantable): iam.Grant; + + /** + * Grant read permissions for a specific path in this domain to an IAM + * principal (Role/Group/User). + * + * @param path The path to grant permissions for + * @param identity The principal + */ + grantPathRead(path: string, identity: iam.IGrantable): iam.Grant; + + /** + * Grant write permissions for a specific path in this domain to an IAM + * principal (Role/Group/User). + * + * @param path The path to grant permissions for + * @param identity The principal + */ + grantPathWrite(path: string, identity: iam.IGrantable): iam.Grant; + + /** + * Grant read/write permissions for a specific path in this domain to an IAM + * principal (Role/Group/User). + * + * @param path The path to grant permissions for + * @param identity The principal + */ + grantPathReadWrite(path: string, identity: iam.IGrantable): iam.Grant; + + /** + * Return the given named metric for this domain. + */ + metric(metricName: string, props?: MetricOptions): Metric; + + /** + * Metric for the time the cluster status is red. + * + * @default maximum over 5 minutes + */ + metricClusterStatusRed(props?: MetricOptions): Metric; + + /** + * Metric for the time the cluster status is yellow. + * + * @default maximum over 5 minutes + */ + metricClusterStatusYellow(props?: MetricOptions): Metric; + + /** + * Metric for the storage space of nodes in the cluster. + * + * @default minimum over 5 minutes + */ + metricFreeStorageSpace(props?: MetricOptions): Metric; + + /** + * Metric for the cluster blocking index writes. + * + * @default maximum over 1 minute + */ + metricClusterIndexWritesBlocked(props?: MetricOptions): Metric; + + /** + * Metric for the number of nodes. + * + * @default minimum over 1 hour + */ + metricNodes(props?: MetricOptions): Metric; + + /** + * Metric for automated snapshot failures. + * + * @default maximum over 5 minutes + */ + metricAutomatedSnapshotFailure(props?: MetricOptions): Metric; + + /** + * Metric for CPU utilization. + * + * @default maximum over 5 minutes + */ + metricCPUUtilization(props?: MetricOptions): Metric; + + /** + * Metric for JVM memory pressure. + * + * @default maximum over 5 minutes + */ + metricJVMMemoryPressure(props?: MetricOptions): Metric; + + /** + * Metric for master CPU utilization. + * + * @default maximum over 5 minutes + */ + metricMasterCPUUtilization(props?: MetricOptions): Metric; + + /** + * Metric for master JVM memory pressure. + * + * @default maximum over 5 minutes + */ + metricMasterJVMMemoryPressure(props?: MetricOptions): Metric; + + /** + * Metric for KMS key errors. + * + * @default maximum over 5 minutes + */ + metricKMSKeyError(props?: MetricOptions): Metric; + + /** + * Metric for KMS key being inaccessible. + * + * @default maximum over 5 minutes + */ + metricKMSKeyInaccessible(props?: MetricOptions): Metric; + + /** + * Metric for number of searchable documents. + * + * @default maximum over 5 minutes + */ + metricSearchableDocuments(props?: MetricOptions): Metric; + + /** + * Metric for search latency. + * + * @default p99 over 5 minutes + */ + metricSearchLatency(props?: MetricOptions): Metric; + + /** + * Metric for indexing latency. + * + * @default p99 over 5 minutes + */ + metricIndexingLatency(props?: MetricOptions): Metric; +} + + +/** + * A new or imported domain. + */ +abstract class DomainBase extends cdk.Resource implements IDomain { + public abstract readonly domainArn: string; + public abstract readonly domainName: string; + public abstract readonly domainId: string; + public abstract readonly domainEndpoint: string; + + /** + * Grant read permissions for this domain and its contents to an IAM + * principal (Role/Group/User). + * + * @param identity The principal + */ + grantRead(identity: iam.IGrantable): iam.Grant { + return this.grant( + identity, + perms.ES_READ_ACTIONS, + this.domainArn, + `${this.domainArn}/*`, + ); + } + + /** + * Grant write permissions for this domain and its contents to an IAM + * principal (Role/Group/User). + * + * @param identity The principal + */ + grantWrite(identity: iam.IGrantable): iam.Grant { + return this.grant( + identity, + perms.ES_WRITE_ACTIONS, + this.domainArn, + `${this.domainArn}/*`, + ); + } + + /** + * Grant read/write permissions for this domain and its contents to an IAM + * principal (Role/Group/User). + * + * @param identity The principal + */ + grantReadWrite(identity: iam.IGrantable): iam.Grant { + return this.grant( + identity, + perms.ES_READ_WRITE_ACTIONS, + this.domainArn, + `${this.domainArn}/*`, + ); + } + + /** + * Grant read permissions for an index in this domain to an IAM + * principal (Role/Group/User). + * + * @param index The index to grant permissions for + * @param identity The principal + */ + grantIndexRead(index: string, identity: iam.IGrantable): iam.Grant { + return this.grant( + identity, + perms.ES_READ_ACTIONS, + `${this.domainArn}/${index}`, + `${this.domainArn}/${index}/*`, + ); + } + + /** + * Grant write permissions for an index in this domain to an IAM + * principal (Role/Group/User). + * + * @param index The index to grant permissions for + * @param identity The principal + */ + grantIndexWrite(index: string, identity: iam.IGrantable): iam.Grant { + return this.grant( + identity, + perms.ES_WRITE_ACTIONS, + `${this.domainArn}/${index}`, + `${this.domainArn}/${index}/*`, + ); + } + + /** + * Grant read/write permissions for an index in this domain to an IAM + * principal (Role/Group/User). + * + * @param index The index to grant permissions for + * @param identity The principal + */ + grantIndexReadWrite(index: string, identity: iam.IGrantable): iam.Grant { + return this.grant( + identity, + perms.ES_READ_WRITE_ACTIONS, + `${this.domainArn}/${index}`, + `${this.domainArn}/${index}/*`, + ); + } + + /** + * Grant read permissions for a specific path in this domain to an IAM + * principal (Role/Group/User). + * + * @param path The path to grant permissions for + * @param identity The principal + */ + grantPathRead(path: string, identity: iam.IGrantable): iam.Grant { + return this.grant( + identity, + perms.ES_READ_ACTIONS, + `${this.domainArn}/${path}`, + ); + } + + /** + * Grant write permissions for a specific path in this domain to an IAM + * principal (Role/Group/User). + * + * @param path The path to grant permissions for + * @param identity The principal + */ + grantPathWrite(path: string, identity: iam.IGrantable): iam.Grant { + return this.grant( + identity, + perms.ES_WRITE_ACTIONS, + `${this.domainArn}/${path}`, + ); + } + + /** + * Grant read/write permissions for a specific path in this domain to an IAM + * principal (Role/Group/User). + * + * @param path The path to grant permissions for + * @param identity The principal + */ + grantPathReadWrite(path: string, identity: iam.IGrantable): iam.Grant { + return this.grant( + identity, + perms.ES_READ_WRITE_ACTIONS, + `${this.domainArn}/${path}`, + ); + } + + /** + * Return the given named metric for this domain. + */ + public metric(metricName: string, props?: MetricOptions): Metric { + return new Metric({ + namespace: 'AWS/ES', + metricName, + dimensions: { + DomainName: this.domainName, + ClientId: this.stack.account, + }, + ...props, + }).attachTo(this); + } + + /** + * Metric for the time the cluster status is red. + * + * @default maximum over 5 minutes + */ + public metricClusterStatusRed(props?: MetricOptions): Metric { + return this.metric('ClusterStatus.red', { + statistic: Statistic.MAXIMUM, + ...props, + }); + } + + /** + * Metric for the time the cluster status is yellow. + * + * @default maximum over 5 minutes + */ + public metricClusterStatusYellow(props?: MetricOptions): Metric { + return this.metric('ClusterStatus.yellow', { + statistic: Statistic.MAXIMUM, + ...props, + }); + } + + /** + * Metric for the storage space of nodes in the cluster. + * + * @default minimum over 5 minutes + */ + public metricFreeStorageSpace(props?: MetricOptions): Metric { + return this.metric('FreeStorageSpace', { + statistic: Statistic.MINIMUM, + ...props, + }); + } + + /** + * Metric for the cluster blocking index writes. + * + * @default maximum over 1 minute + */ + public metricClusterIndexWritesBlocked(props?: MetricOptions): Metric { + return this.metric('ClusterIndexWritesBlocked', { + statistic: Statistic.MAXIMUM, + period: cdk.Duration.minutes(1), + ...props, + }); + } + + /** + * Metric for the number of nodes. + * + * @default minimum over 1 hour + */ + public metricNodes(props?: MetricOptions): Metric { + return this.metric('Nodes', { + statistic: Statistic.MINIMUM, + period: cdk.Duration.hours(1), + ...props, + }); + } + + /** + * Metric for automated snapshot failures. + * + * @default maximum over 5 minutes + */ + public metricAutomatedSnapshotFailure(props?: MetricOptions): Metric { + return this.metric('AutomatedSnapshotFailure', { + statistic: Statistic.MAXIMUM, + ...props, + }); + } + + /** + * Metric for CPU utilization. + * + * @default maximum over 5 minutes + */ + public metricCPUUtilization(props?: MetricOptions): Metric { + return this.metric('CPUUtilization', { + statistic: Statistic.MAXIMUM, + ...props, + }); + } + + /** + * Metric for JVM memory pressure. + * + * @default maximum over 5 minutes + */ + public metricJVMMemoryPressure(props?: MetricOptions): Metric { + return this.metric('JVMMemoryPressure', { + statistic: Statistic.MAXIMUM, + ...props, + }); + } + + /** + * Metric for master CPU utilization. + * + * @default maximum over 5 minutes + */ + public metricMasterCPUUtilization(props?: MetricOptions): Metric { + return this.metric('MasterCPUUtilization', { + statistic: Statistic.MAXIMUM, + ...props, + }); + } + + /** + * Metric for master JVM memory pressure. + * + * @default maximum over 5 minutes + */ + public metricMasterJVMMemoryPressure(props?: MetricOptions): Metric { + return this.metric('MasterJVMMemoryPressure', { + statistic: Statistic.MAXIMUM, + ...props, + }); + } + + /** + * Metric for KMS key errors. + * + * @default maximum over 5 minutes + */ + public metricKMSKeyError(props?: MetricOptions): Metric { + return this.metric('KMSKeyError', { + statistic: Statistic.MAXIMUM, + ...props, + }); + } + + /** + * Metric for KMS key being inaccessible. + * + * @default maximum over 5 minutes + */ + public metricKMSKeyInaccessible(props?: MetricOptions): Metric { + return this.metric('KMSKeyInaccessible', { + statistic: Statistic.MAXIMUM, + ...props, + }); + } + + /** + * Metric for number of searchable documents. + * + * @default maximum over 5 minutes + */ + public metricSearchableDocuments(props?: MetricOptions): Metric { + return this.metric('SearchableDocuments', { + statistic: Statistic.MAXIMUM, + ...props, + }); + } + + /** + * Metric for search latency. + * + * @default p99 over 5 minutes + */ + public metricSearchLatency(props?: MetricOptions): Metric { + return this.metric('SearchLatency', { statistic: 'p99', ...props }); + } + + /** + * Metric for indexing latency. + * + * @default p99 over 5 minutes + */ + public metricIndexingLatency(props?: MetricOptions): Metric { + return this.metric('IndexingLatency', { statistic: 'p99', ...props }); + } + + private grant( + grantee: iam.IGrantable, + domainActions: string[], + resourceArn: string, + ...otherResourceArns: string[] + ): iam.Grant { + const resourceArns = [resourceArn, ...otherResourceArns]; + + const grant = iam.Grant.addToPrincipal({ + grantee, + actions: domainActions, + resourceArns, + scope: this, + }); + + return grant; + } + +} + + +/** + * Reference to an Amazon OpenSearch Service domain. + */ +export interface DomainAttributes { + /** + * The ARN of the Amazon OpenSearch Service domain. + */ + readonly domainArn: string; + + /** + * The domain endpoint of the Amazon OpenSearch Service domain. + */ + readonly domainEndpoint: string; +} + + +/** + * Provides an Amazon OpenSearch Service domain. + */ +export class Domain extends DomainBase implements IDomain, ec2.IConnectable { + /** + * Creates a domain construct that represents an external domain via domain endpoint. + * + * @param scope The parent creating construct (usually `this`). + * @param id The construct's name. + * @param domainEndpoint The domain's endpoint. + */ + public static fromDomainEndpoint( + scope: Construct, + id: string, + domainEndpoint: string, + ): IDomain { + const stack = cdk.Stack.of(scope); + const domainName = extractNameFromEndpoint(domainEndpoint); + const domainArn = stack.formatArn({ + service: 'es', + resource: 'domain', + resourceName: domainName, + }); + + return Domain.fromDomainAttributes(scope, id, { + domainArn, + domainEndpoint, + }); + } + + /** + * Creates a domain construct that represents an external domain. + * + * @param scope The parent creating construct (usually `this`). + * @param id The construct's name. + * @param attrs A `DomainAttributes` object. + */ + public static fromDomainAttributes(scope: Construct, id: string, attrs: DomainAttributes): IDomain { + const { domainArn, domainEndpoint } = attrs; + const domainName = cdk.Stack.of(scope).parseArn(domainArn).resourceName ?? extractNameFromEndpoint(domainEndpoint); + + return new class extends DomainBase { + public readonly domainArn = domainArn; + public readonly domainName = domainName; + public readonly domainId = domainName; + public readonly domainEndpoint = domainEndpoint; + + constructor() { super(scope, id); } + }; + } + + public readonly domainArn: string; + public readonly domainName: string; + public readonly domainId: string; + public readonly domainEndpoint: string; + + /** + * Log group that slow searches are logged to. + * + * @attribute + */ + public readonly slowSearchLogGroup?: logs.ILogGroup; + + /** + * Log group that slow indices are logged to. + * + * @attribute + */ + public readonly slowIndexLogGroup?: logs.ILogGroup; + + /** + * Log group that application logs are logged to. + * + * @attribute + */ + public readonly appLogGroup?: logs.ILogGroup; + + /** + * Log group that audit logs are logged to. + * + * @attribute + */ + public readonly auditLogGroup?: logs.ILogGroup; + + /** + * Master user password if fine grained access control is configured. + */ + public readonly masterUserPassword?: cdk.SecretValue; + + + private readonly domain: CfnDomain; + + private readonly _connections: ec2.Connections | undefined; + + constructor(scope: Construct, id: string, props: DomainProps) { + super(scope, id, { + physicalName: props.domainName, + }); + + const defaultInstanceType = 'r5.large.search'; + const warmDefaultInstanceType = 'ultrawarm1.medium.search'; + + const dedicatedMasterType = + props.capacity?.masterNodeInstanceType?.toLowerCase() ?? + defaultInstanceType; + const dedicatedMasterCount = props.capacity?.masterNodes ?? 0; + const dedicatedMasterEnabled = dedicatedMasterCount > 0; + + const instanceType = + props.capacity?.dataNodeInstanceType?.toLowerCase() ?? + defaultInstanceType; + const instanceCount = props.capacity?.dataNodes ?? 1; + + const warmType = + props.capacity?.warmInstanceType?.toLowerCase() ?? + warmDefaultInstanceType; + const warmCount = props.capacity?.warmNodes ?? 0; + const warmEnabled = warmCount > 0; + + const availabilityZoneCount = + props.zoneAwareness?.availabilityZoneCount ?? 2; + + if (![2, 3].includes(availabilityZoneCount)) { + throw new Error('Invalid zone awareness configuration; availabilityZoneCount must be 2 or 3'); + } + + const zoneAwarenessEnabled = + props.zoneAwareness?.enabled ?? + props.zoneAwareness?.availabilityZoneCount != null; + + + let securityGroups: ec2.ISecurityGroup[] | undefined; + let subnets: ec2.ISubnet[] | undefined; + + if (props.vpc) { + subnets = selectSubnets(props.vpc, props.vpcSubnets ?? [{ subnetType: ec2.SubnetType.PRIVATE }]); + securityGroups = props.securityGroups ?? [new ec2.SecurityGroup(this, 'SecurityGroup', { + vpc: props.vpc, + description: `Security group for domain ${this.node.id}`, + })]; + this._connections = new ec2.Connections({ securityGroups }); + } + + // If VPC options are supplied ensure that the number of subnets matches the number AZ + if (subnets && zoneAwarenessEnabled && new Set(subnets.map((subnet) => subnet.availabilityZone)).size < availabilityZoneCount) { + throw new Error('When providing vpc options you need to provide a subnet for each AZ you are using'); + } + + if ([dedicatedMasterType, instanceType, warmType].some(t => !t.endsWith('.search'))) { + throw new Error('Master, data and UltraWarm node instance types must end with ".search".'); + } + + if (!warmType.startsWith('ultrawarm')) { + throw new Error('UltraWarm node instance type must start with "ultrawarm".'); + } + + const unsignedBasicAuthEnabled = props.useUnsignedBasicAuth ?? false; + + if (unsignedBasicAuthEnabled) { + if (props.enforceHttps == false) { + throw new Error('You cannot disable HTTPS and use unsigned basic auth'); + } + if (props.nodeToNodeEncryption == false) { + throw new Error('You cannot disable node to node encryption and use unsigned basic auth'); + } + if (props.encryptionAtRest?.enabled == false) { + throw new Error('You cannot disable encryption at rest and use unsigned basic auth'); + } + } + + const unsignedAccessPolicy = new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['es:ESHttp*'], + principals: [new iam.AnyPrincipal()], + resources: [cdk.Lazy.string({ produce: () => `${this.domainArn}/*` })], + }); + + const masterUserArn = props.fineGrainedAccessControl?.masterUserArn; + const masterUserNameProps = props.fineGrainedAccessControl?.masterUserName; + // If basic auth is enabled set the user name to admin if no other user info is supplied. + const masterUserName = unsignedBasicAuthEnabled + ? (masterUserArn == null ? (masterUserNameProps ?? 'admin') : undefined) + : masterUserNameProps; + + if (masterUserArn != null && masterUserName != null) { + throw new Error('Invalid fine grained access control settings. Only provide one of master user ARN or master user name. Not both.'); + } + + const advancedSecurityEnabled = (masterUserArn ?? masterUserName) != null; + const internalUserDatabaseEnabled = masterUserName != null; + const masterUserPasswordProp = props.fineGrainedAccessControl?.masterUserPassword; + const createMasterUserPassword = (): cdk.SecretValue => { + return new secretsmanager.Secret(this, 'MasterUser', { + generateSecretString: { + secretStringTemplate: JSON.stringify({ + username: masterUserName, + }), + generateStringKey: 'password', + excludeCharacters: "{}'\\*[]()`", + }, + }) + .secretValueFromJson('password'); + }; + this.masterUserPassword = internalUserDatabaseEnabled ? + (masterUserPasswordProp ?? createMasterUserPassword()) + : undefined; + + const encryptionAtRestEnabled = + props.encryptionAtRest?.enabled ?? (props.encryptionAtRest?.kmsKey != null || unsignedBasicAuthEnabled); + const nodeToNodeEncryptionEnabled = props.nodeToNodeEncryption ?? unsignedBasicAuthEnabled; + const volumeSize = props.ebs?.volumeSize ?? 10; + const volumeType = props.ebs?.volumeType ?? ec2.EbsDeviceVolumeType.GENERAL_PURPOSE_SSD; + const ebsEnabled = props.ebs?.enabled ?? true; + const enforceHttps = props.enforceHttps ?? unsignedBasicAuthEnabled; + + function isInstanceType(t: string): Boolean { + return dedicatedMasterType.startsWith(t) || instanceType.startsWith(t); + }; + + function isSomeInstanceType(...instanceTypes: string[]): Boolean { + return instanceTypes.some(isInstanceType); + }; + + function isEveryInstanceType(...instanceTypes: string[]): Boolean { + return instanceTypes.some(t => dedicatedMasterType.startsWith(t)) + && instanceTypes.some(t => instanceType.startsWith(t)); + }; + + // Validate feature support for the given Elasticsearch/OpenSearch version, per + // https://docs.aws.amazon.com/opensearch-service/latest/developerguide/features-by-version.html + const { versionNum: versionNum, isElasticsearchVersion } = parseVersion(props.version); + if (isElasticsearchVersion) { + + if ( + versionNum <= 7.7 && + ![ + 1.5, 2.3, 5.1, 5.3, 5.5, 5.6, 6.0, + 6.2, 6.3, 6.4, 6.5, 6.7, 6.8, 7.1, 7.4, + 7.7, + ].includes(versionNum) + ) { + throw new Error(`Unknown Elasticsearch version: ${versionNum}`); + } + + if (versionNum < 5.1) { + if (props.logging?.appLogEnabled) { + throw new Error('Error logs publishing requires Elasticsearch version 5.1 or later or OpenSearch version 1.0 or later.'); + } + if (props.encryptionAtRest?.enabled) { + throw new Error('Encryption of data at rest requires Elasticsearch version 5.1 or later or OpenSearch version 1.0 or later.'); + } + if (props.cognitoDashboardsAuth != null) { + throw new Error('Cognito authentication for OpenSearch Dashboards requires Elasticsearch version 5.1 or later or OpenSearch version 1.0 or later.'); + } + if (isSomeInstanceType('c5', 'i3', 'm5', 'r5')) { + throw new Error('C5, I3, M5, and R5 instance types require Elasticsearch version 5.1 or later or OpenSearch version 1.0 or later.'); + } + } + + if (versionNum < 6.0) { + if (props.nodeToNodeEncryption) { + throw new Error('Node-to-node encryption requires Elasticsearch version 6.0 or later or OpenSearch version 1.0 or later.'); + } + } + + if (versionNum < 6.7) { + if (unsignedBasicAuthEnabled) { + throw new Error('Using unsigned basic auth requires Elasticsearch version 6.7 or later or OpenSearch version 1.0 or later.'); + } + if (advancedSecurityEnabled) { + throw new Error('Fine-grained access control requires Elasticsearch version 6.7 or later or OpenSearch version 1.0 or later.'); + } + } + + if (versionNum < 6.8 && warmEnabled) { + throw new Error('UltraWarm requires Elasticsearch version 6.8 or later or OpenSearch version 1.0 or later.'); + } + } + + // Validate against instance type restrictions, per + // https://docs.aws.amazon.com/opensearch-service/latest/developerguide/supported-instance-types.html + if (isSomeInstanceType('i3', 'r6gd') && ebsEnabled) { + throw new Error('I3 and R6GD instance types do not support EBS storage volumes.'); + } + + if (isSomeInstanceType('m3', 'r3', 't2') && encryptionAtRestEnabled) { + throw new Error('M3, R3, and T2 instance types do not support encryption of data at rest.'); + } + + if (isInstanceType('t2.micro') && !(isElasticsearchVersion && versionNum <= 2.3)) { + throw new Error('The t2.micro.search instance type supports only Elasticsearch versions 1.5 and 2.3.'); + } + + if (isSomeInstanceType('t2', 't3') && warmEnabled) { + throw new Error('T2 and T3 instance types do not support UltraWarm storage.'); + } + + // Only R3, I3 and r6gd support instance storage, per + // https://aws.amazon.com/opensearch-service/pricing/ + if (!ebsEnabled && !isEveryInstanceType('r3', 'i3', 'r6gd')) { + throw new Error('EBS volumes are required when using instance types other than r3, i3 or r6gd.'); + } + + // Fine-grained access control requires node-to-node encryption, encryption at rest, + // and enforced HTTPS. + if (advancedSecurityEnabled) { + if (!nodeToNodeEncryptionEnabled) { + throw new Error('Node-to-node encryption is required when fine-grained access control is enabled.'); + } + if (!encryptionAtRestEnabled) { + throw new Error('Encryption-at-rest is required when fine-grained access control is enabled.'); + } + if (!enforceHttps) { + throw new Error('Enforce HTTPS is required when fine-grained access control is enabled.'); + } + } + + // Validate fine grained access control enabled for audit logs, per + // https://aws.amazon.com/about-aws/whats-new/2020/09/elasticsearch-audit-logs-now-available-on-amazon-elasticsearch-service/ + if (props.logging?.auditLogEnabled && !advancedSecurityEnabled) { + throw new Error('Fine-grained access control is required when audit logs publishing is enabled.'); + } + + // Validate UltraWarm requirement for dedicated master nodes, per + // https://docs.aws.amazon.com/opensearch-service/latest/developerguide/ultrawarm.html + if (warmEnabled && !dedicatedMasterEnabled) { + throw new Error('Dedicated master node is required when UltraWarm storage is enabled.'); + } + + let cfnVpcOptions: CfnDomain.VPCOptionsProperty | undefined; + + if (securityGroups && subnets) { + cfnVpcOptions = { + securityGroupIds: securityGroups.map((sg) => sg.securityGroupId), + subnetIds: subnets.map((subnet) => subnet.subnetId), + }; + } + + // Setup logging + const logGroups: logs.ILogGroup[] = []; + + if (props.logging?.slowSearchLogEnabled) { + this.slowSearchLogGroup = props.logging.slowSearchLogGroup ?? + new logs.LogGroup(this, 'SlowSearchLogs', { + retention: logs.RetentionDays.ONE_MONTH, + }); + + logGroups.push(this.slowSearchLogGroup); + }; + + if (props.logging?.slowIndexLogEnabled) { + this.slowIndexLogGroup = props.logging.slowIndexLogGroup ?? + new logs.LogGroup(this, 'SlowIndexLogs', { + retention: logs.RetentionDays.ONE_MONTH, + }); + + logGroups.push(this.slowIndexLogGroup); + }; + + if (props.logging?.appLogEnabled) { + this.appLogGroup = props.logging.appLogGroup ?? + new logs.LogGroup(this, 'AppLogs', { + retention: logs.RetentionDays.ONE_MONTH, + }); + + logGroups.push(this.appLogGroup); + }; + + if (props.logging?.auditLogEnabled) { + this.auditLogGroup = props.logging.auditLogGroup ?? + new logs.LogGroup(this, 'AuditLogs', { + retention: logs.RetentionDays.ONE_MONTH, + }); + + logGroups.push(this.auditLogGroup); + }; + + let logGroupResourcePolicy: LogGroupResourcePolicy | null = null; + if (logGroups.length > 0) { + const logPolicyStatement = new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['logs:PutLogEvents', 'logs:CreateLogStream'], + resources: logGroups.map((lg) => lg.logGroupArn), + principals: [new iam.ServicePrincipal('es.amazonaws.com')], + }); + + // Use a custom resource to set the log group resource policy since it is not supported by CDK and cfn. + // https://github.com/aws/aws-cdk/issues/5343 + logGroupResourcePolicy = new LogGroupResourcePolicy(this, `ESLogGroupPolicy${this.node.addr}`, { + // create a cloudwatch logs resource policy name that is unique to this domain instance + policyName: `ESLogPolicy${this.node.addr}`, + policyStatements: [logPolicyStatement], + }); + } + + const logPublishing: Record = {}; + + if (this.appLogGroup) { + logPublishing.ES_APPLICATION_LOGS = { + enabled: true, + cloudWatchLogsLogGroupArn: this.appLogGroup.logGroupArn, + }; + } + + if (this.slowSearchLogGroup) { + logPublishing.SEARCH_SLOW_LOGS = { + enabled: true, + cloudWatchLogsLogGroupArn: this.slowSearchLogGroup.logGroupArn, + }; + } + + if (this.slowIndexLogGroup) { + logPublishing.INDEX_SLOW_LOGS = { + enabled: true, + cloudWatchLogsLogGroupArn: this.slowIndexLogGroup.logGroupArn, + }; + } + + if (this.auditLogGroup) { + logPublishing.AUDIT_LOGS = { + enabled: this.auditLogGroup != null, + cloudWatchLogsLogGroupArn: this.auditLogGroup?.logGroupArn, + }; + } + + let customEndpointCertificate: acm.ICertificate | undefined; + if (props.customEndpoint) { + if (props.customEndpoint.certificate) { + customEndpointCertificate = props.customEndpoint.certificate; + } else { + customEndpointCertificate = new acm.Certificate(this, 'CustomEndpointCertificate', { + domainName: props.customEndpoint.domainName, + validation: props.customEndpoint.hostedZone ? acm.CertificateValidation.fromDns(props.customEndpoint.hostedZone) : undefined, + }); + } + } + + // Create the domain + this.domain = new CfnDomain(this, 'Resource', { + domainName: this.physicalName, + engineVersion: props.version.version, + clusterConfig: { + dedicatedMasterEnabled, + dedicatedMasterCount: dedicatedMasterEnabled + ? dedicatedMasterCount + : undefined, + dedicatedMasterType: dedicatedMasterEnabled + ? dedicatedMasterType + : undefined, + instanceCount, + instanceType, + warmEnabled: warmEnabled + ? warmEnabled + : undefined, + warmCount: warmEnabled + ? warmCount + : undefined, + warmType: warmEnabled + ? warmType + : undefined, + zoneAwarenessEnabled, + zoneAwarenessConfig: zoneAwarenessEnabled + ? { availabilityZoneCount } + : undefined, + }, + ebsOptions: { + ebsEnabled, + volumeSize: ebsEnabled ? volumeSize : undefined, + volumeType: ebsEnabled ? volumeType : undefined, + iops: ebsEnabled ? props.ebs?.iops : undefined, + }, + encryptionAtRestOptions: { + enabled: encryptionAtRestEnabled, + kmsKeyId: encryptionAtRestEnabled + ? props.encryptionAtRest?.kmsKey?.keyId + : undefined, + }, + nodeToNodeEncryptionOptions: { enabled: nodeToNodeEncryptionEnabled }, + logPublishingOptions: logPublishing, + cognitoOptions: { + enabled: props.cognitoDashboardsAuth != null, + identityPoolId: props.cognitoDashboardsAuth?.identityPoolId, + roleArn: props.cognitoDashboardsAuth?.role.roleArn, + userPoolId: props.cognitoDashboardsAuth?.userPoolId, + }, + vpcOptions: cfnVpcOptions, + snapshotOptions: props.automatedSnapshotStartHour + ? { automatedSnapshotStartHour: props.automatedSnapshotStartHour } + : undefined, + domainEndpointOptions: { + enforceHttps, + tlsSecurityPolicy: props.tlsSecurityPolicy ?? TLSSecurityPolicy.TLS_1_0, + ...props.customEndpoint && { + customEndpointEnabled: true, + customEndpoint: props.customEndpoint.domainName, + customEndpointCertificateArn: customEndpointCertificate!.certificateArn, + }, + }, + advancedSecurityOptions: advancedSecurityEnabled + ? { + enabled: true, + internalUserDatabaseEnabled, + masterUserOptions: { + masterUserArn: masterUserArn, + masterUserName: masterUserName, + masterUserPassword: this.masterUserPassword?.toString(), + }, + } + : undefined, + advancedOptions: props.advancedOptions, + }); + this.domain.applyRemovalPolicy(props.removalPolicy); + + if (props.enableVersionUpgrade) { + this.domain.cfnOptions.updatePolicy = { + ...this.domain.cfnOptions.updatePolicy, + enableVersionUpgrade: props.enableVersionUpgrade, + }; + } + + if (logGroupResourcePolicy) { this.domain.node.addDependency(logGroupResourcePolicy); } + + if (props.domainName) { this.node.addMetadata('aws:cdk:hasPhysicalName', props.domainName); } + + this.domainName = this.getResourceNameAttribute(this.domain.ref); + + this.domainId = this.domain.getAtt('Id').toString(); + + this.domainEndpoint = this.domain.getAtt('DomainEndpoint').toString(); + + this.domainArn = this.getResourceArnAttribute(this.domain.attrArn, { + service: 'es', + resource: 'domain', + resourceName: this.physicalName, + }); + + if (props.customEndpoint?.hostedZone) { + new route53.CnameRecord(this, 'CnameRecord', { + recordName: props.customEndpoint.domainName, + zone: props.customEndpoint.hostedZone, + domainName: this.domainEndpoint, + }); + } + + const accessPolicyStatements: iam.PolicyStatement[] | undefined = unsignedBasicAuthEnabled + ? (props.accessPolicies ?? []).concat(unsignedAccessPolicy) + : props.accessPolicies; + + if (accessPolicyStatements != null) { + const accessPolicy = new OpenSearchAccessPolicy(this, 'Access Policy', { + domainName: this.domainName, + domainArn: this.domainArn, + accessPolicies: accessPolicyStatements, + }); + + if (props.encryptionAtRest?.kmsKey) { + + // https://docs.aws.amazon.com/opensearch-service/latest/developerguide/encryption-at-rest.html + + // these permissions are documented as required during domain creation. + // while not strictly documented for updates as well, it stands to reason that an update + // operation might require these in case the cluster uses a kms key. + // empircal evidence shows this is indeed required: https://github.com/aws/aws-cdk/issues/11412 + accessPolicy.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['kms:List*', 'kms:Describe*', 'kms:CreateGrant'], + resources: [props.encryptionAtRest.kmsKey.keyArn], + effect: iam.Effect.ALLOW, + })); + } + + accessPolicy.node.addDependency(this.domain); + } + } + + /** + * Manages network connections to the domain. This will throw an error in case the domain + * is not placed inside a VPC. + */ + public get connections(): ec2.Connections { + if (!this._connections) { + throw new Error("Connections are only available on VPC enabled domains. Use the 'vpc' property to place a domain inside a VPC"); + } + return this._connections; + } +} + +/** + * Given an Amazon OpenSearch Service domain endpoint, returns a CloudFormation expression that + * extracts the domain name. + * + * Domain endpoints look like this: + * + * https://example-domain-jcjotrt6f7otem4sqcwbch3c4u.us-east-1.es.amazonaws.com + * https://-..es.amazonaws.com + * + * ..which means that in order to extract the domain name from the endpoint, we can + * split the endpoint using "-" and select the component in index 0. + * + * @param domainEndpoint The Amazon OpenSearch Service domain endpoint + */ +function extractNameFromEndpoint(domainEndpoint: string) { + const { hostname } = new URL(domainEndpoint); + const domain = hostname.split('.')[0]; + const suffix = '-' + domain.split('-').slice(-1)[0]; + return domain.split(suffix)[0]; +} + +/** + * Converts an engine version into a into a decimal number with major and minor version i.e x.y. + * + * @param version The engine version object + */ +function parseVersion(version: EngineVersion): { versionNum: number, isElasticsearchVersion: boolean } { + const elasticsearchPrefix = 'Elasticsearch_'; + const openSearchPrefix = 'OpenSearch_'; + const isElasticsearchVersion = version.version.startsWith(elasticsearchPrefix); + const versionStr = isElasticsearchVersion + ? version.version.substring(elasticsearchPrefix.length) + : version.version.substring(openSearchPrefix.length); + const firstDot = versionStr.indexOf('.'); + + if (firstDot < 1) { + throw new Error(`Invalid engine version: ${versionStr}. Version string needs to start with major and minor version (x.y).`); + } + + const secondDot = versionStr.indexOf('.', firstDot + 1); + + try { + if (secondDot == -1) { + return { versionNum: parseFloat(versionStr), isElasticsearchVersion }; + } else { + return { versionNum: parseFloat(versionStr.substring(0, secondDot)), isElasticsearchVersion }; + } + } catch (error) { + throw new Error(`Invalid engine version: ${versionStr}. Version string needs to start with major and minor version (x.y).`); + } +} + +function selectSubnets(vpc: ec2.IVpc, vpcSubnets: ec2.SubnetSelection[]): ec2.ISubnet[] { + const selected = []; + for (const selection of vpcSubnets) { + selected.push(...vpc.selectSubnets(selection).subnets); + } + return selected; +} diff --git a/packages/@aws-cdk/aws-opensearchservice/lib/index.ts b/packages/@aws-cdk/aws-opensearchservice/lib/index.ts new file mode 100644 index 0000000000000..8c90500d9533e --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/lib/index.ts @@ -0,0 +1,5 @@ +export * from './domain'; +export * from './version'; + +// AWS::OpenSearchService CloudFormation Resources: +export * from './opensearchservice.generated'; diff --git a/packages/@aws-cdk/aws-opensearchservice/lib/log-group-resource-policy.ts b/packages/@aws-cdk/aws-opensearchservice/lib/log-group-resource-policy.ts new file mode 100644 index 0000000000000..e53eb9a913540 --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/lib/log-group-resource-policy.ts @@ -0,0 +1,53 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as cr from '@aws-cdk/custom-resources'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +/** + * Construction properties for LogGroupResourcePolicy + */ +export interface LogGroupResourcePolicyProps { + /** + * The log group resource policy name + */ + readonly policyName: string; + /** + * The policy statements for the log group resource logs + */ + readonly policyStatements: [iam.PolicyStatement]; +} + +/** + * Creates LogGroup resource policies. + */ +export class LogGroupResourcePolicy extends cr.AwsCustomResource { + constructor(scope: Construct, id: string, props: LogGroupResourcePolicyProps) { + const policyDocument = new iam.PolicyDocument({ + statements: props.policyStatements, + }); + + super(scope, id, { + resourceType: 'Custom::CloudwatchLogResourcePolicy', + onUpdate: { + service: 'CloudWatchLogs', + action: 'putResourcePolicy', + parameters: { + policyName: props.policyName, + policyDocument: JSON.stringify(policyDocument), + }, + physicalResourceId: cr.PhysicalResourceId.of(id), + }, + onDelete: { + service: 'CloudWatchLogs', + action: 'deleteResourcePolicy', + parameters: { + policyName: props.policyName, + }, + ignoreErrorCodesMatching: '400', + }, + policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ resources: ['*'] }), + }); + } +} diff --git a/packages/@aws-cdk/aws-opensearchservice/lib/opensearch-access-policy.ts b/packages/@aws-cdk/aws-opensearchservice/lib/opensearch-access-policy.ts new file mode 100644 index 0000000000000..1eeb01fb95d78 --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/lib/opensearch-access-policy.ts @@ -0,0 +1,53 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as cr from '@aws-cdk/custom-resources'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +/** + * Construction properties for OpenSearchAccessPolicy + */ +export interface OpenSearchAccessPolicyProps { + /** + * The OpenSearch Domain name + */ + readonly domainName: string; + + /** + * The OpenSearch Domain ARN + */ + readonly domainArn: string; + + /** + * The access policy statements for the OpenSearch cluster + */ + readonly accessPolicies: iam.PolicyStatement[]; +} + +/** + * Creates LogGroup resource policies. + */ +export class OpenSearchAccessPolicy extends cr.AwsCustomResource { + constructor(scope: Construct, id: string, props: OpenSearchAccessPolicyProps) { + const policyDocument = new iam.PolicyDocument({ + statements: props.accessPolicies, + }); + + super(scope, id, { + resourceType: 'Custom::OpenSearchAccessPolicy', + onUpdate: { + action: 'updateDomainConfig', + service: 'OpenSearch', + parameters: { + DomainName: props.domainName, + AccessPolicies: JSON.stringify(policyDocument.toJSON()), + }, + // this is needed to limit the response body, otherwise it exceeds the CFN 4k limit + outputPaths: ['DomainConfig.AccessPolicies'], + physicalResourceId: cr.PhysicalResourceId.of(`${props.domainName}AccessPolicy`), + }, + policy: cr.AwsCustomResourcePolicy.fromStatements([new iam.PolicyStatement({ actions: ['es:UpdateDomainConfig'], resources: [props.domainArn] })]), + }); + } +} diff --git a/packages/@aws-cdk/aws-opensearchservice/lib/perms.ts b/packages/@aws-cdk/aws-opensearchservice/lib/perms.ts new file mode 100644 index 0000000000000..f5804c9f059d5 --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/lib/perms.ts @@ -0,0 +1,16 @@ +export const ES_READ_ACTIONS = [ + 'es:ESHttpGet', + 'es:ESHttpHead', +]; + +export const ES_WRITE_ACTIONS = [ + 'es:ESHttpDelete', + 'es:ESHttpPost', + 'es:ESHttpPut', + 'es:ESHttpPatch', +]; + +export const ES_READ_WRITE_ACTIONS = [ + ...ES_READ_ACTIONS, + ...ES_WRITE_ACTIONS, +]; diff --git a/packages/@aws-cdk/aws-opensearchservice/lib/version.ts b/packages/@aws-cdk/aws-opensearchservice/lib/version.ts new file mode 100644 index 0000000000000..f170504c17338 --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/lib/version.ts @@ -0,0 +1,81 @@ +/** + * OpenSearch version + */ +export class EngineVersion { + /** AWS Elasticsearch 1.5 */ + public static readonly ELASTICSEARCH_1_5 = EngineVersion.elasticsearch('1.5'); + + /** AWS Elasticsearch 2.3 */ + public static readonly ELASTICSEARCH_2_3 = EngineVersion.elasticsearch('2.3'); + + /** AWS Elasticsearch 5.1 */ + public static readonly ELASTICSEARCH_5_1 = EngineVersion.elasticsearch('5.1'); + + /** AWS Elasticsearch 5.3 */ + public static readonly ELASTICSEARCH_5_3 = EngineVersion.elasticsearch('5.3'); + + /** AWS Elasticsearch 5.5 */ + public static readonly ELASTICSEARCH_5_5 = EngineVersion.elasticsearch('5.5'); + + /** AWS Elasticsearch 5.6 */ + public static readonly ELASTICSEARCH_5_6 = EngineVersion.elasticsearch('5.6'); + + /** AWS Elasticsearch 6.0 */ + public static readonly ELASTICSEARCH_6_0 = EngineVersion.elasticsearch('6.0'); + + /** AWS Elasticsearch 6.2 */ + public static readonly ELASTICSEARCH_6_2 = EngineVersion.elasticsearch('6.2'); + + /** AWS Elasticsearch 6.3 */ + public static readonly ELASTICSEARCH_6_3 = EngineVersion.elasticsearch('6.3'); + + /** AWS Elasticsearch 6.4 */ + public static readonly ELASTICSEARCH_6_4 = EngineVersion.elasticsearch('6.4'); + + /** AWS Elasticsearch 6.5 */ + public static readonly ELASTICSEARCH_6_5 = EngineVersion.elasticsearch('6.5'); + + /** AWS Elasticsearch 6.7 */ + public static readonly ELASTICSEARCH_6_7 = EngineVersion.elasticsearch('6.7'); + + /** AWS Elasticsearch 6.8 */ + public static readonly ELASTICSEARCH_6_8 = EngineVersion.elasticsearch('6.8'); + + /** AWS Elasticsearch 7.1 */ + public static readonly ELASTICSEARCH_7_1 = EngineVersion.elasticsearch('7.1'); + + /** AWS Elasticsearch 7.4 */ + public static readonly ELASTICSEARCH_7_4 = EngineVersion.elasticsearch('7.4'); + + /** AWS Elasticsearch 7.7 */ + public static readonly ELASTICSEARCH_7_7 = EngineVersion.elasticsearch('7.7'); + + /** AWS Elasticsearch 7.8 */ + public static readonly ELASTICSEARCH_7_8 = EngineVersion.elasticsearch('7.8'); + + /** AWS Elasticsearch 7.9 */ + public static readonly ELASTICSEARCH_7_9 = EngineVersion.elasticsearch('7.9'); + + /** AWS Elasticsearch 7.10 */ + public static readonly ELASTICSEARCH_7_10 = EngineVersion.elasticsearch('7.10'); + + /** AWS OpenSearch 1.0 */ + public static readonly OPENSEARCH_1_0 = EngineVersion.openSearch('1.0'); + + /** + * Custom ElasticSearch version + * @param version custom version number + */ + public static elasticsearch(version: string) { return new EngineVersion(`Elasticsearch_${version}`); } + + /** + * Custom OpenSearch version + * @param version custom version number + */ + public static openSearch(version: string) { return new EngineVersion(`OpenSearch_${version}`); } + + /** + * @param version engine version identifier + */ + private constructor(public readonly version: string) { } +} diff --git a/packages/@aws-cdk/aws-opensearchservice/package.json b/packages/@aws-cdk/aws-opensearchservice/package.json new file mode 100644 index 0000000000000..e00852181986c --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/package.json @@ -0,0 +1,131 @@ +{ + "name": "@aws-cdk/aws-opensearchservice", + "version": "0.0.0", + "description": "The CDK Construct Library for AWS::OpenSearchService", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "projectReferences": true, + "targets": { + "dotnet": { + "namespace": "Amazon.CDK.AWS.OpenSearchService", + "packageId": "Amazon.CDK.AWS.OpenSearchService", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "java": { + "package": "software.amazon.awscdk.services.opensearchservice", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "opensearchservice" + } + }, + "python": { + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 1" + ], + "distName": "aws-cdk.aws-opensearchservice", + "module": "aws_cdk.aws_opensearchservice" + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-opensearchservice" + }, + "homepage": "https://github.com/aws/aws-cdk", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "cfn2ts": "cfn2ts", + "build+test": "yarn build && yarn test", + "build+test+package": "yarn build+test && yarn package", + "compat": "cdk-compat", + "gen": "cfn2ts", + "rosetta:extract": "yarn --silent jsii-rosetta extract", + "build+extract": "yarn build && yarn rosetta:extract", + "build+test+extract": "yarn build+test && yarn rosetta:extract" + }, + "cdk-build": { + "cloudformation": "AWS::OpenSearchService", + "jest": true, + "env": { + "AWSLINT_BASE_CONSTRUCT": "true" + } + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "AWS::OpenSearchService", + "aws-opensearchservice", + "opensearchservice" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@types/jest": "^26.0.24", + "@aws-cdk/assert-internal": "0.0.0", + "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", + "cfn2ts": "0.0.0", + "pkglint": "0.0.0" + }, + "dependencies": { + "@aws-cdk/aws-certificatemanager": "0.0.0", + "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-route53": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", + "@aws-cdk/custom-resources": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.3.69" + }, + "peerDependencies": { + "@aws-cdk/aws-certificatemanager": "0.0.0", + "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-route53": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", + "@aws-cdk/custom-resources": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.3.69" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "stable", + "maturity": "stable", + "features": [ + { + "name": "Higher level constructs for Domain", + "stability": "Stable" + } + ], + "awscdkio": { + "announce": false + }, + "publishConfig": { + "tag": "latest" + } +} diff --git a/packages/@aws-cdk/aws-opensearchservice/test/domain.test.ts b/packages/@aws-cdk/aws-opensearchservice/test/domain.test.ts new file mode 100644 index 0000000000000..4ad11ac372f97 --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/test/domain.test.ts @@ -0,0 +1,1742 @@ +/* eslint-disable jest/expect-expect */ +import '@aws-cdk/assert-internal/jest'; +import * as assert from '@aws-cdk/assert-internal'; +import * as acm from '@aws-cdk/aws-certificatemanager'; +import { Metric, Statistic } from '@aws-cdk/aws-cloudwatch'; +import { Vpc, EbsDeviceVolumeType, SecurityGroup } from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; +import * as logs from '@aws-cdk/aws-logs'; +import * as route53 from '@aws-cdk/aws-route53'; +import { App, Stack, Duration, SecretValue, CfnParameter } from '@aws-cdk/core'; +import { Domain, EngineVersion } from '../lib'; + +let app: App; +let stack: Stack; + +beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + env: { account: '1234', region: 'testregion' }, + }); + + jest.resetAllMocks(); +}); + +const readActions = ['ESHttpGet', 'ESHttpHead']; +const writeActions = ['ESHttpDelete', 'ESHttpPost', 'ESHttpPut', 'ESHttpPatch']; +const readWriteActions = [ + ...readActions, + ...writeActions, +]; + +const defaultVersion = EngineVersion.OPENSEARCH_1_0; + +test('connections throws if domain is placed inside a vpc', () => { + + expect(() => { + new Domain(stack, 'Domain', { + version: defaultVersion, + }).connections; + }).toThrowError("Connections are only available on VPC enabled domains. Use the 'vpc' property to place a domain inside a VPC"); +}); + +test('subnets and security groups can be provided when vpc is used', () => { + + const vpc = new Vpc(stack, 'Vpc'); + const securityGroup = new SecurityGroup(stack, 'CustomSecurityGroup', { + vpc, + }); + const domain = new Domain(stack, 'Domain', { + version: defaultVersion, + vpc, + vpcSubnets: [{ subnets: [vpc.privateSubnets[0]] }], + securityGroups: [securityGroup], + }); + + expect(domain.connections.securityGroups[0].securityGroupId).toEqual(securityGroup.securityGroupId); + expect(stack).toHaveResource('AWS::OpenSearchService::Domain', { + VPCOptions: { + SecurityGroupIds: [ + { + 'Fn::GetAtt': [ + 'CustomSecurityGroupE5E500E5', + 'GroupId', + ], + }, + ], + SubnetIds: [ + { + Ref: 'VpcPrivateSubnet1Subnet536B997A', + }, + ], + }, + }); + +}); + +test('default subnets and security group when vpc is used', () => { + + const vpc = new Vpc(stack, 'Vpc'); + const domain = new Domain(stack, 'Domain', { + version: defaultVersion, + vpc, + }); + + expect(stack.resolve(domain.connections.securityGroups[0].securityGroupId)).toEqual({ 'Fn::GetAtt': ['DomainSecurityGroup48AA5FD6', 'GroupId'] }); + expect(stack).toHaveResource('AWS::OpenSearchService::Domain', { + VPCOptions: { + SecurityGroupIds: [ + { + 'Fn::GetAtt': [ + 'DomainSecurityGroup48AA5FD6', + 'GroupId', + ], + }, + ], + SubnetIds: [ + { + Ref: 'VpcPrivateSubnet1Subnet536B997A', + }, + { + Ref: 'VpcPrivateSubnet2Subnet3788AAA1', + }, + { + Ref: 'VpcPrivateSubnet3SubnetF258B56E', + }, + ], + }, + }); + +}); + +test('default removalpolicy is retain', () => { + new Domain(stack, 'Domain', { + version: defaultVersion, + }); + + expect(stack).toHaveResource('AWS::OpenSearchService::Domain', { + DeletionPolicy: 'Retain', + }, assert.ResourcePart.CompleteDefinition); +}); + +test('grants kms permissions if needed', () => { + + const key = new kms.Key(stack, 'Key'); + + new Domain(stack, 'Domain', { + version: defaultVersion, + encryptionAtRest: { + kmsKey: key, + }, + // so that the access policy custom resource will be used. + useUnsignedBasicAuth: true, + }); + + const expectedPolicy = { + Statement: [ + { + Action: [ + 'kms:List*', + 'kms:Describe*', + 'kms:CreateGrant', + ], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'Key961B73FD', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }; + + const resources = assert.expect(stack).value.Resources; + expect(resources.AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E.Properties.PolicyDocument).toStrictEqual(expectedPolicy); + +}); + +test('minimal example renders correctly', () => { + new Domain(stack, 'Domain', { version: defaultVersion }); + + expect(stack).toHaveResource('AWS::OpenSearchService::Domain', { + CognitoOptions: { + Enabled: false, + }, + EBSOptions: { + EBSEnabled: true, + VolumeSize: 10, + VolumeType: 'gp2', + }, + ClusterConfig: { + DedicatedMasterEnabled: false, + InstanceCount: 1, + InstanceType: 'r5.large.search', + ZoneAwarenessEnabled: false, + }, + EngineVersion: 'OpenSearch_1.0', + EncryptionAtRestOptions: { + Enabled: false, + }, + LogPublishingOptions: { + AUDIT_LOGS: assert.ABSENT, + ES_APPLICATION_LOGS: assert.ABSENT, + SEARCH_SLOW_LOGS: assert.ABSENT, + INDEX_SLOW_LOGS: assert.ABSENT, + }, + NodeToNodeEncryptionOptions: { + Enabled: false, + }, + }); +}); + +test('can enable version upgrade update policy', () => { + new Domain(stack, 'Domain', { + version: defaultVersion, + enableVersionUpgrade: true, + }); + + expect(stack).toHaveResource('AWS::OpenSearchService::Domain', { + UpdatePolicy: { + EnableVersionUpgrade: true, + }, + }, assert.ResourcePart.CompleteDefinition); +}); + +describe('UltraWarm instances', () => { + + test('can enable UltraWarm instances', () => { + new Domain(stack, 'Domain', { + version: defaultVersion, + capacity: { + masterNodes: 2, + warmNodes: 2, + }, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + ClusterConfig: { + DedicatedMasterEnabled: true, + WarmEnabled: true, + WarmCount: 2, + WarmType: 'ultrawarm1.medium.search', + }, + }); + }); + + test('can enable UltraWarm instances with specific instance type', () => { + new Domain(stack, 'Domain', { + version: defaultVersion, + capacity: { + masterNodes: 2, + warmNodes: 2, + warmInstanceType: 'ultrawarm1.large.search', + }, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + ClusterConfig: { + DedicatedMasterEnabled: true, + WarmEnabled: true, + WarmCount: 2, + WarmType: 'ultrawarm1.large.search', + }, + }); + }); + +}); + +describe('log groups', () => { + + test('slowSearchLogEnabled should create a custom log group', () => { + new Domain(stack, 'Domain', { + version: defaultVersion, + logging: { + slowSearchLogEnabled: true, + }, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + LogPublishingOptions: { + SEARCH_SLOW_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'DomainSlowSearchLogs5B35A97A', + 'Arn', + ], + }, + Enabled: true, + }, + AUDIT_LOGS: assert.ABSENT, + ES_APPLICATION_LOGS: assert.ABSENT, + INDEX_SLOW_LOGS: assert.ABSENT, + }, + }); + }); + + test('slowIndexLogEnabled should create a custom log group', () => { + new Domain(stack, 'Domain', { + version: defaultVersion, + logging: { + slowIndexLogEnabled: true, + }, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + LogPublishingOptions: { + INDEX_SLOW_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'DomainSlowIndexLogsFE2F1061', + 'Arn', + ], + }, + Enabled: true, + }, + AUDIT_LOGS: assert.ABSENT, + ES_APPLICATION_LOGS: assert.ABSENT, + SEARCH_SLOW_LOGS: assert.ABSENT, + }, + }); + }); + + test('appLogEnabled should create a custom log group', () => { + new Domain(stack, 'Domain', { + version: defaultVersion, + logging: { + appLogEnabled: true, + }, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + LogPublishingOptions: { + ES_APPLICATION_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'DomainAppLogs21698C1B', + 'Arn', + ], + }, + Enabled: true, + }, + AUDIT_LOGS: assert.ABSENT, + SEARCH_SLOW_LOGS: assert.ABSENT, + INDEX_SLOW_LOGS: assert.ABSENT, + }, + }); + }); + + test('auditLogEnabled should create a custom log group', () => { + new Domain(stack, 'Domain', { + version: defaultVersion, + logging: { + auditLogEnabled: true, + }, + fineGrainedAccessControl: { + masterUserName: 'username', + }, + nodeToNodeEncryption: true, + encryptionAtRest: { + enabled: true, + }, + enforceHttps: true, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + LogPublishingOptions: { + AUDIT_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'DomainAuditLogs608E0FA6', + 'Arn', + ], + }, + Enabled: true, + }, + ES_APPLICATION_LOGS: assert.ABSENT, + SEARCH_SLOW_LOGS: assert.ABSENT, + INDEX_SLOW_LOGS: assert.ABSENT, + }, + }); + }); + + test('two domains with logging enabled can be created in same stack', () => { + new Domain(stack, 'Domain1', { + version: defaultVersion, + logging: { + appLogEnabled: true, + slowSearchLogEnabled: true, + slowIndexLogEnabled: true, + }, + }); + new Domain(stack, 'Domain2', { + version: defaultVersion, + logging: { + appLogEnabled: true, + slowSearchLogEnabled: true, + slowIndexLogEnabled: true, + }, + }); + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + LogPublishingOptions: { + ES_APPLICATION_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'Domain1AppLogs6E8D1D67', + 'Arn', + ], + }, + Enabled: true, + }, + SEARCH_SLOW_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'Domain1SlowSearchLogs8F3B0506', + 'Arn', + ], + }, + Enabled: true, + }, + INDEX_SLOW_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'Domain1SlowIndexLogs9354D098', + 'Arn', + ], + }, + Enabled: true, + }, + AUDIT_LOGS: assert.ABSENT, + }, + }); + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + LogPublishingOptions: { + ES_APPLICATION_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'Domain2AppLogs810876E2', + 'Arn', + ], + }, + Enabled: true, + }, + SEARCH_SLOW_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'Domain2SlowSearchLogs0C75F64B', + 'Arn', + ], + }, + Enabled: true, + }, + INDEX_SLOW_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'Domain2SlowIndexLogs0CB900D0', + 'Arn', + ], + }, + Enabled: true, + }, + AUDIT_LOGS: assert.ABSENT, + }, + }); + }); + + test('log group policy is uniquely named for each domain', () => { + new Domain(stack, 'Domain1', { + version: defaultVersion, + logging: { + appLogEnabled: true, + }, + }); + new Domain(stack, 'Domain2', { + version: defaultVersion, + logging: { + appLogEnabled: true, + }, + }); + + // Domain1 + expect(stack).toHaveResourceLike('Custom::CloudwatchLogResourcePolicy', { + Create: { + 'Fn::Join': [ + '', + [ + '{"service":"CloudWatchLogs","action":"putResourcePolicy","parameters":{"policyName":"ESLogPolicyc836fd92f07ec41eb70c2f6f08dc4b43cfb7c25391","policyDocument":"{\\"Statement\\":[{\\"Action\\":[\\"logs:PutLogEvents\\",\\"logs:CreateLogStream\\"],\\"Effect\\":\\"Allow\\",\\"Principal\\":{\\"Service\\":\\"es.amazonaws.com\\"},\\"Resource\\":\\"', + { + 'Fn::GetAtt': [ + 'Domain1AppLogs6E8D1D67', + 'Arn', + ], + }, + '\\"}],\\"Version\\":\\"2012-10-17\\"}"},"physicalResourceId":{"id":"ESLogGroupPolicyc836fd92f07ec41eb70c2f6f08dc4b43cfb7c25391"}}', + ], + ], + }, + }); + // Domain2 + expect(stack).toHaveResourceLike('Custom::CloudwatchLogResourcePolicy', { + Create: { + 'Fn::Join': [ + '', + [ + '{"service":"CloudWatchLogs","action":"putResourcePolicy","parameters":{"policyName":"ESLogPolicyc8f05f015be3baf6ec1ee06cd1ee5cc8706ebbe5b2","policyDocument":"{\\"Statement\\":[{\\"Action\\":[\\"logs:PutLogEvents\\",\\"logs:CreateLogStream\\"],\\"Effect\\":\\"Allow\\",\\"Principal\\":{\\"Service\\":\\"es.amazonaws.com\\"},\\"Resource\\":\\"', + { + 'Fn::GetAtt': [ + 'Domain2AppLogs810876E2', + 'Arn', + ], + }, + '\\"}],\\"Version\\":\\"2012-10-17\\"}"},"physicalResourceId":{"id":"ESLogGroupPolicyc8f05f015be3baf6ec1ee06cd1ee5cc8706ebbe5b2"}}', + ], + ], + }, + }); + }); + + test('enabling audit logs throws without fine grained access control enabled', () => { + expect(() => new Domain(stack, 'Domain', { + version: defaultVersion, + logging: { + auditLogEnabled: true, + }, + })).toThrow(/Fine-grained access control is required when audit logs publishing is enabled\./); + }); + + test('slowSearchLogGroup should use a custom log group', () => { + new Domain(stack, 'Domain', { + version: defaultVersion, + logging: { + slowSearchLogEnabled: true, + slowSearchLogGroup: new logs.LogGroup(stack, 'SlowSearchLogs', { + retention: logs.RetentionDays.THREE_MONTHS, + }), + }, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + LogPublishingOptions: { + SEARCH_SLOW_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'SlowSearchLogsE00DC2E7', + 'Arn', + ], + }, + Enabled: true, + }, + AUDIT_LOGS: assert.ABSENT, + ES_APPLICATION_LOGS: assert.ABSENT, + INDEX_SLOW_LOGS: assert.ABSENT, + }, + }); + }); + + test('slowIndexLogEnabled should use a custom log group', () => { + new Domain(stack, 'Domain', { + version: defaultVersion, + logging: { + slowIndexLogEnabled: true, + slowIndexLogGroup: new logs.LogGroup(stack, 'SlowIndexLogs', { + retention: logs.RetentionDays.THREE_MONTHS, + }), + }, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + LogPublishingOptions: { + INDEX_SLOW_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'SlowIndexLogsAD49DED0', + 'Arn', + ], + }, + Enabled: true, + }, + AUDIT_LOGS: assert.ABSENT, + ES_APPLICATION_LOGS: assert.ABSENT, + SEARCH_SLOW_LOGS: assert.ABSENT, + }, + }); + }); + + test('appLogGroup should use a custom log group', () => { + new Domain(stack, 'Domain', { + version: defaultVersion, + logging: { + appLogEnabled: true, + appLogGroup: new logs.LogGroup(stack, 'AppLogs', { + retention: logs.RetentionDays.THREE_MONTHS, + }), + }, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + LogPublishingOptions: { + ES_APPLICATION_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'AppLogsC5DF83A6', + 'Arn', + ], + }, + Enabled: true, + }, + AUDIT_LOGS: assert.ABSENT, + SEARCH_SLOW_LOGS: assert.ABSENT, + INDEX_SLOW_LOGS: assert.ABSENT, + }, + }); + }); + + test('auditLOgGroup should use a custom log group', () => { + new Domain(stack, 'Domain', { + version: defaultVersion, + fineGrainedAccessControl: { + masterUserName: 'username', + }, + nodeToNodeEncryption: true, + encryptionAtRest: { + enabled: true, + }, + enforceHttps: true, + logging: { + auditLogEnabled: true, + auditLogGroup: new logs.LogGroup(stack, 'AuditLogs', { + retention: logs.RetentionDays.THREE_MONTHS, + }), + }, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + LogPublishingOptions: { + AUDIT_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'AuditLogsB945E340', + 'Arn', + ], + }, + Enabled: true, + }, + ES_APPLICATION_LOGS: assert.ABSENT, + SEARCH_SLOW_LOGS: assert.ABSENT, + INDEX_SLOW_LOGS: assert.ABSENT, + }, + }); + }); + +}); + +describe('grants', () => { + + test('"grantRead" allows read actions associated with this domain resource', () => { + testGrant(readActions, (p, d) => d.grantRead(p)); + }); + + test('"grantWrite" allows write actions associated with this domain resource', () => { + testGrant(writeActions, (p, d) => d.grantWrite(p)); + }); + + test('"grantReadWrite" allows read and write actions associated with this domain resource', () => { + testGrant(readWriteActions, (p, d) => d.grantReadWrite(p)); + }); + + test('"grantIndexRead" allows read actions associated with an index in this domain resource', () => { + testGrant( + readActions, + (p, d) => d.grantIndexRead('my-index', p), + false, + ['/my-index', '/my-index/*'], + ); + }); + + test('"grantIndexWrite" allows write actions associated with an index in this domain resource', () => { + testGrant( + writeActions, + (p, d) => d.grantIndexWrite('my-index', p), + false, + ['/my-index', '/my-index/*'], + ); + }); + + test('"grantIndexReadWrite" allows read and write actions associated with an index in this domain resource', () => { + testGrant( + readWriteActions, + (p, d) => d.grantIndexReadWrite('my-index', p), + false, + ['/my-index', '/my-index/*'], + ); + }); + + test('"grantPathRead" allows read actions associated with a given path in this domain resource', () => { + testGrant( + readActions, + (p, d) => d.grantPathRead('my-index/my-path', p), + false, + ['/my-index/my-path'], + ); + }); + + test('"grantPathWrite" allows write actions associated with a given path in this domain resource', () => { + testGrant( + writeActions, + (p, d) => d.grantPathWrite('my-index/my-path', p), + false, + ['/my-index/my-path'], + ); + }); + + test('"grantPathReadWrite" allows read and write actions associated with a given path in this domain resource', () => { + testGrant( + readWriteActions, + (p, d) => d.grantPathReadWrite('my-index/my-path', p), + false, + ['/my-index/my-path'], + ); + }); + + test('"grant" for an imported domain', () => { + const domainEndpoint = 'https://test-domain-2w2x2u3tifly-jcjotrt6f7otem4sqcwbch3c4u.testregion.es.amazonaws.com'; + const domain = Domain.fromDomainEndpoint(stack, 'Domain', domainEndpoint); + const user = new iam.User(stack, 'user'); + + domain.grantReadWrite(user); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'es:ESHttpGet', + 'es:ESHttpHead', + 'es:ESHttpDelete', + 'es:ESHttpPost', + 'es:ESHttpPut', + 'es:ESHttpPatch', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':es:testregion:1234:domain/test-domain-2w2x2u3tifly', + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':es:testregion:1234:domain/test-domain-2w2x2u3tifly/*', + ], + ], + }, + ], + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'userDefaultPolicy083DF682', + Users: [ + { + Ref: 'user2C2B57AE', + }, + ], + }); + }); + +}); + +describe('metrics', () => { + + test('metricClusterStatusRed', () => { + testMetric( + (domain) => domain.metricClusterStatusRed(), + 'ClusterStatus.red', + Statistic.MAXIMUM, + ); + }); + + test('metricClusterStatusYellow', () => { + testMetric( + (domain) => domain.metricClusterStatusYellow(), + 'ClusterStatus.yellow', + Statistic.MAXIMUM, + ); + }); + + test('metricFreeStorageSpace', () => { + testMetric( + (domain) => domain.metricFreeStorageSpace(), + 'FreeStorageSpace', + Statistic.MINIMUM, + ); + }); + + test('metricClusterIndexWriteBlocked', () => { + testMetric( + (domain) => domain.metricClusterIndexWritesBlocked(), + 'ClusterIndexWritesBlocked', + Statistic.MAXIMUM, + Duration.minutes(1), + ); + }); + + test('metricNodes', () => { + testMetric( + (domain) => domain.metricNodes(), + 'Nodes', + Statistic.MINIMUM, + Duration.hours(1), + ); + }); + + test('metricAutomatedSnapshotFailure', () => { + testMetric( + (domain) => domain.metricAutomatedSnapshotFailure(), + 'AutomatedSnapshotFailure', + Statistic.MAXIMUM, + ); + }); + + test('metricCPUUtilization', () => { + testMetric( + (domain) => domain.metricCPUUtilization(), + 'CPUUtilization', + Statistic.MAXIMUM, + ); + }); + + test('metricJVMMemoryPressure', () => { + testMetric( + (domain) => domain.metricJVMMemoryPressure(), + 'JVMMemoryPressure', + Statistic.MAXIMUM, + ); + }); + + test('metricMasterCPUUtilization', () => { + testMetric( + (domain) => domain.metricMasterCPUUtilization(), + 'MasterCPUUtilization', + Statistic.MAXIMUM, + ); + }); + + test('metricMasterJVMMemoryPressure', () => { + testMetric( + (domain) => domain.metricMasterJVMMemoryPressure(), + 'MasterJVMMemoryPressure', + Statistic.MAXIMUM, + ); + }); + + test('metricKMSKeyError', () => { + testMetric( + (domain) => domain.metricKMSKeyError(), + 'KMSKeyError', + Statistic.MAXIMUM, + ); + }); + + test('metricKMSKeyInaccessible', () => { + testMetric( + (domain) => domain.metricKMSKeyInaccessible(), + 'KMSKeyInaccessible', + Statistic.MAXIMUM, + ); + }); + + test('metricSearchableDocuments', () => { + testMetric( + (domain) => domain.metricSearchableDocuments(), + 'SearchableDocuments', + Statistic.MAXIMUM, + ); + }); + + test('metricSearchLatency', () => { + testMetric( + (domain) => domain.metricSearchLatency(), + 'SearchLatency', + 'p99', + ); + }); + + test('metricIndexingLatency', () => { + testMetric( + (domain) => domain.metricIndexingLatency(), + 'IndexingLatency', + 'p99', + ); + }); + +}); + +describe('import', () => { + + test('static fromDomainEndpoint(endpoint) allows importing an external/existing domain', () => { + const domainName = 'test-domain-2w2x2u3tifly'; + const domainEndpoint = `https://${domainName}-jcjotrt6f7otem4sqcwbch3c4u.testregion.es.amazonaws.com`; + const imported = Domain.fromDomainEndpoint(stack, 'Domain', domainEndpoint); + + expect(imported.domainName).toEqual(domainName); + expect(imported.domainArn).toMatch(RegExp(`es:testregion:1234:domain/${domainName}$`)); + + expect(stack).not.toHaveResource('AWS::OpenSearchService::Domain'); + }); + + test('static fromDomainAttributes(attributes) allows importing an external/existing domain', () => { + const domainName = 'test-domain-2w2x2u3tifly'; + const domainArn = `arn:aws:es:testregion:1234:domain/${domainName}`; + const domainEndpoint = `https://${domainName}-jcjotrt6f7otem4sqcwbch3c4u.testregion.es.amazonaws.com`; + const imported = Domain.fromDomainAttributes(stack, 'Domain', { + domainArn, + domainEndpoint, + }); + + expect(imported.domainName).toEqual(domainName); + expect(imported.domainArn).toEqual(domainArn); + + expect(stack).not.toHaveResource('AWS::OpenSearchService::Domain'); + }); + + test('static fromDomainAttributes(attributes) allows importing with token arn and endpoint', () => { + const domainArn = new CfnParameter(stack, 'domainArn', { type: 'String' }).valueAsString; + const domainEndpoint = new CfnParameter(stack, 'domainEndpoint', { type: 'String' }).valueAsString; + const imported = Domain.fromDomainAttributes(stack, 'Domain', { + domainArn, + domainEndpoint, + }); + const expectedDomainName = { + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + '/', + { + 'Fn::Select': [ + 5, + { + 'Fn::Split': [ + ':', + { + Ref: 'domainArn', + }, + ], + }, + ], + }, + ], + }, + ], + }; + + expect(stack.resolve(imported.domainName)).toEqual(expectedDomainName); + expect(imported.domainArn).toEqual(domainArn); + expect(imported.domainEndpoint).toEqual(domainEndpoint); + + expect(stack).not.toHaveResource('AWS::OpenSearchService::Domain'); + }); +}); + +describe('advanced security options', () => { + const masterUserArn = 'arn:aws:iam::123456789012:user/JohnDoe'; + const masterUserName = 'JohnDoe'; + const password = 'password'; + const masterUserPassword = SecretValue.plainText(password); + + test('enable fine-grained access control with a master user ARN', () => { + new Domain(stack, 'Domain', { + version: defaultVersion, + fineGrainedAccessControl: { + masterUserArn, + }, + encryptionAtRest: { + enabled: true, + }, + nodeToNodeEncryption: true, + enforceHttps: true, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + AdvancedSecurityOptions: { + Enabled: true, + InternalUserDatabaseEnabled: false, + MasterUserOptions: { + MasterUserARN: masterUserArn, + }, + }, + EncryptionAtRestOptions: { + Enabled: true, + }, + NodeToNodeEncryptionOptions: { + Enabled: true, + }, + DomainEndpointOptions: { + EnforceHTTPS: true, + }, + }); + }); + + test('enable fine-grained access control with a master user name and password', () => { + new Domain(stack, 'Domain', { + version: defaultVersion, + fineGrainedAccessControl: { + masterUserName, + masterUserPassword, + }, + encryptionAtRest: { + enabled: true, + }, + nodeToNodeEncryption: true, + enforceHttps: true, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + AdvancedSecurityOptions: { + Enabled: true, + InternalUserDatabaseEnabled: true, + MasterUserOptions: { + MasterUserName: masterUserName, + MasterUserPassword: password, + }, + }, + EncryptionAtRestOptions: { + Enabled: true, + }, + NodeToNodeEncryptionOptions: { + Enabled: true, + }, + DomainEndpointOptions: { + EnforceHTTPS: true, + }, + }); + }); + + test('enable fine-grained access control with a master user name and dynamically generated password', () => { + new Domain(stack, 'Domain', { + version: defaultVersion, + fineGrainedAccessControl: { + masterUserName, + }, + encryptionAtRest: { + enabled: true, + }, + nodeToNodeEncryption: true, + enforceHttps: true, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + AdvancedSecurityOptions: { + Enabled: true, + InternalUserDatabaseEnabled: true, + MasterUserOptions: { + MasterUserName: masterUserName, + MasterUserPassword: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'DomainMasterUserBFAFA7D9', + }, + ':SecretString:password::}}', + ], + ], + }, + }, + }, + EncryptionAtRestOptions: { + Enabled: true, + }, + NodeToNodeEncryptionOptions: { + Enabled: true, + }, + DomainEndpointOptions: { + EnforceHTTPS: true, + }, + }); + + expect(stack).toHaveResourceLike('AWS::SecretsManager::Secret', { + GenerateSecretString: { + GenerateStringKey: 'password', + }, + }); + }); + + test('enabling fine-grained access control throws with Elasticsearch version < 6.7', () => { + expect(() => new Domain(stack, 'Domain', { + version: EngineVersion.ELASTICSEARCH_6_5, + fineGrainedAccessControl: { + masterUserArn, + }, + encryptionAtRest: { + enabled: true, + }, + nodeToNodeEncryption: true, + enforceHttps: true, + })).toThrow(/Fine-grained access control requires Elasticsearch version 6\.7 or later or OpenSearch version 1\.0 or later/); + }); + + test('enabling fine-grained access control throws without node-to-node encryption enabled', () => { + expect(() => new Domain(stack, 'Domain', { + version: defaultVersion, + fineGrainedAccessControl: { + masterUserArn, + }, + encryptionAtRest: { + enabled: true, + }, + nodeToNodeEncryption: false, + enforceHttps: true, + })).toThrow(/Node-to-node encryption is required when fine-grained access control is enabled/); + }); + + test('enabling fine-grained access control throws without encryption-at-rest enabled', () => { + expect(() => new Domain(stack, 'Domain', { + version: defaultVersion, + fineGrainedAccessControl: { + masterUserArn, + }, + encryptionAtRest: { + enabled: false, + }, + nodeToNodeEncryption: true, + enforceHttps: true, + })).toThrow(/Encryption-at-rest is required when fine-grained access control is enabled/); + }); + + test('enabling fine-grained access control throws without enforceHttps enabled', () => { + expect(() => new Domain(stack, 'Domain', { + version: defaultVersion, + fineGrainedAccessControl: { + masterUserArn, + }, + encryptionAtRest: { + enabled: true, + }, + nodeToNodeEncryption: true, + enforceHttps: false, + })).toThrow(/Enforce HTTPS is required when fine-grained access control is enabled/); + }); +}); + +describe('custom endpoints', () => { + const customDomainName = 'search.example.com'; + + test('custom domain without hosted zone and default cert', () => { + new Domain(stack, 'Domain', { + version: defaultVersion, + nodeToNodeEncryption: true, + enforceHttps: true, + customEndpoint: { + domainName: customDomainName, + }, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + DomainEndpointOptions: { + EnforceHTTPS: true, + CustomEndpointEnabled: true, + CustomEndpoint: customDomainName, + CustomEndpointCertificateArn: { + Ref: 'DomainCustomEndpointCertificateD080A69E', // Auto-generated certificate + }, + }, + }); + expect(stack).toHaveResourceLike('AWS::CertificateManager::Certificate', { + DomainName: customDomainName, + ValidationMethod: 'EMAIL', + }); + }); + + test('custom domain with hosted zone and default cert', () => { + const zone = new route53.HostedZone(stack, 'DummyZone', { zoneName: 'example.com' }); + new Domain(stack, 'Domain', { + version: defaultVersion, + nodeToNodeEncryption: true, + enforceHttps: true, + customEndpoint: { + domainName: customDomainName, + hostedZone: zone, + }, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + DomainEndpointOptions: { + EnforceHTTPS: true, + CustomEndpointEnabled: true, + CustomEndpoint: customDomainName, + CustomEndpointCertificateArn: { + Ref: 'DomainCustomEndpointCertificateD080A69E', // Auto-generated certificate + }, + }, + }); + expect(stack).toHaveResourceLike('AWS::CertificateManager::Certificate', { + DomainName: customDomainName, + DomainValidationOptions: [ + { + DomainName: customDomainName, + HostedZoneId: { + Ref: 'DummyZone03E0FE81', + }, + }, + ], + ValidationMethod: 'DNS', + }); + expect(stack).toHaveResourceLike('AWS::Route53::RecordSet', { + Name: 'search.example.com.', + Type: 'CNAME', + HostedZoneId: { + Ref: 'DummyZone03E0FE81', + }, + ResourceRecords: [ + { + 'Fn::GetAtt': [ + 'Domain66AC69E0', + 'DomainEndpoint', + ], + }, + ], + }); + }); + + test('custom domain with hosted zone and given cert', () => { + const zone = new route53.HostedZone(stack, 'DummyZone', { + zoneName: 'example.com', + }); + const certificate = new acm.Certificate(stack, 'DummyCert', { + domainName: customDomainName, + }); + + new Domain(stack, 'Domain', { + version: defaultVersion, + nodeToNodeEncryption: true, + enforceHttps: true, + customEndpoint: { + domainName: customDomainName, + hostedZone: zone, + certificate, + }, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + DomainEndpointOptions: { + EnforceHTTPS: true, + CustomEndpointEnabled: true, + CustomEndpoint: customDomainName, + CustomEndpointCertificateArn: { + Ref: 'DummyCertFA37670B', + }, + }, + }); + expect(stack).toHaveResourceLike('AWS::Route53::RecordSet', { + Name: 'search.example.com.', + Type: 'CNAME', + HostedZoneId: { + Ref: 'DummyZone03E0FE81', + }, + ResourceRecords: [ + { + 'Fn::GetAtt': [ + 'Domain66AC69E0', + 'DomainEndpoint', + ], + }, + ], + }); + }); + +}); + +describe('custom error responses', () => { + + test('error when availabilityZoneCount does not match vpcOptions.subnets length', () => { + const vpc = new Vpc(stack, 'Vpc', { + maxAzs: 1, + }); + + expect(() => new Domain(stack, 'Domain', { + version: defaultVersion, + zoneAwareness: { + enabled: true, + availabilityZoneCount: 2, + }, + vpc, + })).toThrow(/you need to provide a subnet for each AZ you are using/); + }); + + test('error when master, data or Ultra Warm instance types do not end with .search', () => { + const error = /instance types must end with ".search"/; + expect(() => new Domain(stack, 'Domain1', { + version: defaultVersion, + capacity: { + masterNodeInstanceType: 'c5.large', + }, + })).toThrow(error); + expect(() => new Domain(stack, 'Domain2', { + version: defaultVersion, + capacity: { + dataNodeInstanceType: 'c5.2xlarge', + }, + })).toThrow(error); + expect(() => new Domain(stack, 'Domain3', { + version: defaultVersion, + capacity: { + warmInstanceType: 'ultrawarm1.medium', + }, + })).toThrow(error); + }); + + test('error when Ultra Warm instance types do not start with ultrawarm', () => { + const error = /UltraWarm node instance type must start with "ultrawarm"./; + expect(() => new Domain(stack, 'Domain1', { + version: defaultVersion, + capacity: { + warmInstanceType: 't3.small.search', + }, + })).toThrow(error); + }); + + test('error when Elasticsearch version is unsupported/unknown', () => { + expect(() => new Domain(stack, 'Domain1', { + version: EngineVersion.elasticsearch('5.4'), + })).toThrow('Unknown Elasticsearch version: 5.4'); + }); + + test('error when error log publishing is enabled for Elasticsearch version < 5.1', () => { + const error = /Error logs publishing requires Elasticsearch version 5.1 or later or OpenSearch version 1.0 or later/; + expect(() => new Domain(stack, 'Domain1', { + version: EngineVersion.ELASTICSEARCH_2_3, + logging: { + appLogEnabled: true, + }, + })).toThrow(error); + }); + + test('error when encryption at rest is enabled for Elasticsearch version < 5.1', () => { + expect(() => new Domain(stack, 'Domain1', { + version: EngineVersion.ELASTICSEARCH_2_3, + encryptionAtRest: { + enabled: true, + }, + })).toThrow(/Encryption of data at rest requires Elasticsearch version 5.1 or later or OpenSearch version 1.0 or later/); + }); + + test('error when cognito for OpenSearch Dashboards is enabled for elasticsearch version < 5.1', () => { + const user = new iam.User(stack, 'user'); + expect(() => new Domain(stack, 'Domain1', { + version: EngineVersion.ELASTICSEARCH_2_3, + cognitoDashboardsAuth: { + identityPoolId: 'test-identity-pool-id', + role: new iam.Role(stack, 'Role', { assumedBy: user }), + userPoolId: 'test-user-pool-id', + }, + })).toThrow(/Cognito authentication for OpenSearch Dashboards requires Elasticsearch version 5.1 or later or OpenSearch version 1.0 or later/); + }); + + test('error when C5, I3, M5, or R5 instance types are specified for Elasticsearch version < 5.1', () => { + const error = /C5, I3, M5, and R5 instance types require Elasticsearch version 5.1 or later or OpenSearch version 1.0 or later/; + expect(() => new Domain(stack, 'Domain1', { + version: EngineVersion.ELASTICSEARCH_2_3, + capacity: { + masterNodeInstanceType: 'c5.medium.search', + }, + })).toThrow(error); + expect(() => new Domain(stack, 'Domain2', { + version: EngineVersion.ELASTICSEARCH_1_5, + capacity: { + dataNodeInstanceType: 'i3.2xlarge.search', + }, + })).toThrow(error); + expect(() => new Domain(stack, 'Domain3', { + version: EngineVersion.ELASTICSEARCH_1_5, + capacity: { + dataNodeInstanceType: 'm5.2xlarge.search', + }, + })).toThrow(error); + expect(() => new Domain(stack, 'Domain4', { + version: EngineVersion.ELASTICSEARCH_1_5, + capacity: { + masterNodeInstanceType: 'r5.2xlarge.search', + }, + })).toThrow(error); + }); + + test('error when node to node encryption is enabled for Elasticsearch version < 6.0', () => { + expect(() => new Domain(stack, 'Domain1', { + version: EngineVersion.ELASTICSEARCH_5_6, + nodeToNodeEncryption: true, + })).toThrow(/Node-to-node encryption requires Elasticsearch version 6.0 or later or OpenSearch version 1.0 or later/); + }); + + test('error when i3 instance types are specified with EBS enabled', () => { + expect(() => new Domain(stack, 'Domain1', { + version: defaultVersion, + capacity: { + dataNodeInstanceType: 'i3.2xlarge.search', + }, + ebs: { + volumeSize: 100, + volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, + }, + })).toThrow(/I3 and R6GD instance types do not support EBS storage volumes/); + }); + + test('error when m3, r3, or t2 instance types are specified with encryption at rest enabled', () => { + const error = /M3, R3, and T2 instance types do not support encryption of data at rest/; + expect(() => new Domain(stack, 'Domain1', { + version: defaultVersion, + capacity: { + masterNodeInstanceType: 'm3.2xlarge.search', + }, + encryptionAtRest: { + enabled: true, + }, + })).toThrow(error); + expect(() => new Domain(stack, 'Domain2', { + version: defaultVersion, + capacity: { + dataNodeInstanceType: 'r3.2xlarge.search', + }, + encryptionAtRest: { + enabled: true, + }, + })).toThrow(error); + expect(() => new Domain(stack, 'Domain3', { + version: defaultVersion, + capacity: { + masterNodeInstanceType: 't2.2xlarge.search', + }, + encryptionAtRest: { + enabled: true, + }, + })).toThrow(error); + }); + + test('error when t2.micro is specified with Elasticsearch version > 2.3', () => { + expect(() => new Domain(stack, 'Domain1', { + version: defaultVersion, + capacity: { + masterNodeInstanceType: 't2.micro.search', + }, + })).toThrow(/t2.micro.search instance type supports only Elasticsearch versions 1.5 and 2.3/); + }); + + test('error when any instance type other than R3, I3 and R6GD are specified without EBS enabled', () => { + expect(() => new Domain(stack, 'Domain1', { + version: defaultVersion, + ebs: { + enabled: false, + }, + capacity: { + masterNodeInstanceType: 'm5.large.search', + }, + })).toThrow(/EBS volumes are required when using instance types other than r3, i3 or r6gd/); + }); + + test('error when availabilityZoneCount is not 2 or 3', () => { + const vpc = new Vpc(stack, 'Vpc'); + + expect(() => new Domain(stack, 'Domain1', { + version: defaultVersion, + vpc, + zoneAwareness: { + availabilityZoneCount: 4, + }, + })).toThrow(/Invalid zone awareness configuration; availabilityZoneCount must be 2 or 3/); + }); + + test('error when UltraWarm instance is used with Elasticsearch version < 6.8', () => { + expect(() => new Domain(stack, 'Domain1', { + version: EngineVersion.ELASTICSEARCH_6_7, + capacity: { + masterNodes: 1, + warmNodes: 1, + }, + })).toThrow(/UltraWarm requires Elasticsearch version 6\.8 or later or OpenSearch version 1.0 or later/); + }); + + test('error when t2 or t3 instance types are specified with UltramWarm enabled', () => { + const error = /T2 and T3 instance types do not support UltraWarm storage/; + expect(() => new Domain(stack, 'Domain1', { + version: defaultVersion, + capacity: { + masterNodeInstanceType: 't2.2xlarge.search', + warmNodes: 1, + }, + })).toThrow(error); + expect(() => new Domain(stack, 'Domain2', { + version: defaultVersion, + capacity: { + masterNodeInstanceType: 't3.2xlarge.search', + warmNodes: 1, + }, + })).toThrow(error); + }); + + test('error when UltraWarm instance is used and no dedicated master instance specified', () => { + expect(() => new Domain(stack, 'Domain1', { + version: defaultVersion, + capacity: { + warmNodes: 1, + masterNodes: 0, + }, + })).toThrow(/Dedicated master node is required when UltraWarm storage is enabled/); + }); + +}); + +test('can specify future version', () => { + new Domain(stack, 'Domain', { version: EngineVersion.elasticsearch('8.2') }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + EngineVersion: 'Elasticsearch_8.2', + }); +}); + +describe('unsigned basic auth', () => { + test('can create a domain with unsigned basic auth', () => { + new Domain(stack, 'Domain', { + version: defaultVersion, + useUnsignedBasicAuth: true, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + AdvancedSecurityOptions: { + Enabled: true, + InternalUserDatabaseEnabled: true, + MasterUserOptions: { + MasterUserName: 'admin', + }, + }, + EncryptionAtRestOptions: { + Enabled: true, + }, + NodeToNodeEncryptionOptions: { + Enabled: true, + }, + DomainEndpointOptions: { + EnforceHTTPS: true, + }, + }); + }); + + test('does not overwrite master user ARN configuration', () => { + const masterUserArn = 'arn:aws:iam::123456789012:user/JohnDoe'; + + new Domain(stack, 'Domain', { + version: defaultVersion, + fineGrainedAccessControl: { + masterUserArn, + }, + useUnsignedBasicAuth: true, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + AdvancedSecurityOptions: { + Enabled: true, + InternalUserDatabaseEnabled: false, + MasterUserOptions: { + MasterUserARN: masterUserArn, + }, + }, + EncryptionAtRestOptions: { + Enabled: true, + }, + NodeToNodeEncryptionOptions: { + Enabled: true, + }, + DomainEndpointOptions: { + EnforceHTTPS: true, + }, + }); + }); + + test('does not overwrite master user name and password', () => { + const masterUserName = 'JohnDoe'; + const password = 'password'; + const masterUserPassword = SecretValue.plainText(password); + + new Domain(stack, 'Domain', { + version: defaultVersion, + fineGrainedAccessControl: { + masterUserName, + masterUserPassword, + }, + useUnsignedBasicAuth: true, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + AdvancedSecurityOptions: { + Enabled: true, + InternalUserDatabaseEnabled: true, + MasterUserOptions: { + MasterUserName: masterUserName, + MasterUserPassword: password, + }, + }, + EncryptionAtRestOptions: { + Enabled: true, + }, + NodeToNodeEncryptionOptions: { + Enabled: true, + }, + DomainEndpointOptions: { + EnforceHTTPS: true, + }, + }); + }); + + test('fails to create a domain with unsigned basic auth when enforce HTTPS is disabled', () => { + expect(() => new Domain(stack, 'Domain', { + version: defaultVersion, + useUnsignedBasicAuth: true, + enforceHttps: false, + })).toThrow(/You cannot disable HTTPS and use unsigned basic auth/); + }); + + test('fails to create a domain with unsigned basic auth when node to node encryption is disabled', () => { + expect(() => new Domain(stack, 'Domain', { + version: defaultVersion, + useUnsignedBasicAuth: true, + nodeToNodeEncryption: false, + })).toThrow(/You cannot disable node to node encryption and use unsigned basic auth/); + }); + + test('fails to create a domain with unsigned basic auth when encryption at rest is disabled', () => { + expect(() => new Domain(stack, 'Domain', { + version: defaultVersion, + useUnsignedBasicAuth: true, + encryptionAtRest: { enabled: false }, + })).toThrow(/You cannot disable encryption at rest and use unsigned basic auth/); + }); + + test('using unsigned basic auth throws with Elasticsearch < 6.7', () => { + expect(() => new Domain(stack, 'Domain', { + version: EngineVersion.ELASTICSEARCH_6_5, + useUnsignedBasicAuth: true, + })).toThrow(/Using unsigned basic auth requires Elasticsearch version 6\.7 or later or OpenSearch version 1.0 or later/); + }); +}); + +describe('advanced options', () => { + test('use advanced options', () => { + new Domain(stack, 'Domain', { + version: defaultVersion, + advancedOptions: { + 'rest.action.multi.allow_explicit_index': 'true', + 'indices.fielddata.cache.size': '50', + }, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + AdvancedOptions: { + 'rest.action.multi.allow_explicit_index': 'true', + 'indices.fielddata.cache.size': '50', + }, + }); + }); + + test('advanced options absent by default', () => { + new Domain(stack, 'Domain', { + version: defaultVersion, + }); + + expect(stack).toHaveResourceLike('AWS::OpenSearchService::Domain', { + AdvancedOptions: assert.ABSENT, + }); + }); +}); + +function testGrant( + expectedActions: string[], + invocation: (user: iam.IPrincipal, domain: Domain) => void, + appliesToDomainRoot: Boolean = true, + paths: string[] = ['/*'], +) { + const domain = new Domain(stack, 'Domain', { version: defaultVersion }); + const user = new iam.User(stack, 'user'); + + invocation(user, domain); + + const action = expectedActions.length > 1 ? expectedActions.map(a => `es:${a}`) : `es:${expectedActions[0]}`; + const domainArn = { + 'Fn::GetAtt': [ + 'Domain66AC69E0', + 'Arn', + ], + }; + const resolvedPaths = paths.map(path => { + return { + 'Fn::Join': [ + '', + [ + domainArn, + path, + ], + ], + }; + }); + const resource = appliesToDomainRoot + ? [domainArn, ...resolvedPaths] + : resolvedPaths.length > 1 + ? resolvedPaths + : resolvedPaths[0]; + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: action, + Effect: 'Allow', + Resource: resource, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'userDefaultPolicy083DF682', + Users: [ + { + Ref: 'user2C2B57AE', + }, + ], + }); +} + +function testMetric( + invocation: (domain: Domain) => Metric, + metricName: string, + statistic: string = Statistic.SUM, + period: Duration = Duration.minutes(5), +) { + const domain = new Domain(stack, 'Domain', { version: defaultVersion }); + + const metric = invocation(domain); + + expect(metric).toMatchObject({ + metricName, + namespace: 'AWS/ES', + period, + statistic, + dimensions: { + ClientId: '1234', + }, + }); + expect(metric.dimensions).toHaveProperty('DomainName'); +} diff --git a/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.advancedsecurity.expected.json b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.advancedsecurity.expected.json new file mode 100644 index 0000000000000..5b80dc6907424 --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.advancedsecurity.expected.json @@ -0,0 +1,52 @@ +{ + "Resources": { + "User00B015A1": { + "Type": "AWS::IAM::User" + }, + "Domain66AC69E0": { + "Type": "AWS::OpenSearchService::Domain", + "Properties": { + "AdvancedSecurityOptions": { + "Enabled": true, + "MasterUserOptions": { + "MasterUserARN": { + "Fn::GetAtt": [ + "User00B015A1", + "Arn" + ] + } + }, + "InternalUserDatabaseEnabled": false + }, + "ClusterConfig": { + "InstanceCount": 1, + "DedicatedMasterEnabled": false, + "InstanceType": "r5.large.search", + "ZoneAwarenessEnabled": false + }, + "CognitoOptions": { + "Enabled": false + }, + "DomainEndpointOptions": { + "EnforceHTTPS": true, + "TLSSecurityPolicy": "Policy-Min-TLS-1-0-2019-07" + }, + "EBSOptions": { + "EBSEnabled": true, + "VolumeType": "gp2", + "VolumeSize": 10 + }, + "EncryptionAtRestOptions": { + "Enabled": true + }, + "EngineVersion": "Elasticsearch_7.1", + "LogPublishingOptions": {}, + "NodeToNodeEncryptionOptions": { + "Enabled": true + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.advancedsecurity.ts b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.advancedsecurity.ts new file mode 100644 index 0000000000000..f72ad9eaa48ef --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.advancedsecurity.ts @@ -0,0 +1,29 @@ +import { User } from '@aws-cdk/aws-iam'; +import { App, RemovalPolicy, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as opensearch from '../lib'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const user = new User(this, 'User'); + + new opensearch.Domain(this, 'Domain', { + removalPolicy: RemovalPolicy.DESTROY, + version: opensearch.EngineVersion.ELASTICSEARCH_7_1, + fineGrainedAccessControl: { + masterUserArn: user.userArn, + }, + encryptionAtRest: { + enabled: true, + }, + nodeToNodeEncryption: true, + enforceHttps: true, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-opensearch-advancedsecurity'); +app.synth(); diff --git a/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.custom-kms-key.expected.json b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.custom-kms-key.expected.json new file mode 100644 index 0000000000000..5a2791399497d --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.custom-kms-key.expected.json @@ -0,0 +1,301 @@ +{ + "Resources": { + "Key961B73FD": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "Domain66AC69E0": { + "Type": "AWS::OpenSearchService::Domain", + "Properties": { + "ClusterConfig": { + "InstanceCount": 1, + "DedicatedMasterEnabled": false, + "InstanceType": "r5.large.search", + "ZoneAwarenessEnabled": false + }, + "CognitoOptions": { + "Enabled": false + }, + "DomainEndpointOptions": { + "EnforceHTTPS": false, + "TLSSecurityPolicy": "Policy-Min-TLS-1-0-2019-07" + }, + "EBSOptions": { + "EBSEnabled": true, + "VolumeType": "gp2", + "VolumeSize": 10 + }, + "EncryptionAtRestOptions": { + "KmsKeyId": { + "Ref": "Key961B73FD" + }, + "Enabled": true + }, + "EngineVersion": "Elasticsearch_7.1", + "LogPublishingOptions": {}, + "NodeToNodeEncryptionOptions": { + "Enabled": false + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "DomainAccessPolicyCustomResourcePolicy107E31EB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "es:UpdateDomainConfig", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Domain66AC69E0", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "DomainAccessPolicyCustomResourcePolicy107E31EB", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + }, + "DependsOn": [ + "Domain66AC69E0" + ] + }, + "DomainAccessPolicyFCD6BE37": { + "Type": "Custom::OpenSearchAccessPolicy", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", + "Arn" + ] + }, + "Create": { + "Fn::Join": [ + "", + [ + "{\"action\":\"updateDomainConfig\",\"service\":\"OpenSearch\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain66AC69E0" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPaths\":[\"DomainConfig.AccessPolicies\"],\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain66AC69E0" + }, + "AccessPolicy\"}}" + ] + ] + }, + "Update": { + "Fn::Join": [ + "", + [ + "{\"action\":\"updateDomainConfig\",\"service\":\"OpenSearch\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain66AC69E0" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPaths\":[\"DomainConfig.AccessPolicies\"],\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain66AC69E0" + }, + "AccessPolicy\"}}" + ] + ] + }, + "InstallLatestAwsSdk": true + }, + "DependsOn": [ + "DomainAccessPolicyCustomResourcePolicy107E31EB", + "Domain66AC69E0" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "kms:List*", + "kms:Describe*", + "kms:CreateGrant" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Key961B73FD", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + } + }, + "AWS679f53fac002430cb0da5b7982bd22872D164C4C": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters5d4cb1694db181d27240b1dabc7e4d79a7e1051022a41b773652782433c07f7dS3BucketF2AC65B6" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5d4cb1694db181d27240b1dabc7e4d79a7e1051022a41b773652782433c07f7dS3VersionKey8025D5E5" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5d4cb1694db181d27240b1dabc7e4d79a7e1051022a41b773652782433c07f7dS3VersionKey8025D5E5" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x", + "Timeout": 120 + }, + "DependsOn": [ + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E", + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + ] + } + }, + "Parameters": { + "AssetParameters5d4cb1694db181d27240b1dabc7e4d79a7e1051022a41b773652782433c07f7dS3BucketF2AC65B6": { + "Type": "String", + "Description": "S3 bucket for asset \"5d4cb1694db181d27240b1dabc7e4d79a7e1051022a41b773652782433c07f7d\"" + }, + "AssetParameters5d4cb1694db181d27240b1dabc7e4d79a7e1051022a41b773652782433c07f7dS3VersionKey8025D5E5": { + "Type": "String", + "Description": "S3 key for asset version \"5d4cb1694db181d27240b1dabc7e4d79a7e1051022a41b773652782433c07f7d\"" + }, + "AssetParameters5d4cb1694db181d27240b1dabc7e4d79a7e1051022a41b773652782433c07f7dArtifactHash30B93F3B": { + "Type": "String", + "Description": "Artifact hash for asset \"5d4cb1694db181d27240b1dabc7e4d79a7e1051022a41b773652782433c07f7d\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.custom-kms-key.ts b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.custom-kms-key.ts new file mode 100644 index 0000000000000..533300c2cc4fe --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.custom-kms-key.ts @@ -0,0 +1,37 @@ +/// !cdk-integ pragma:ignore-assets +import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; +import { App, RemovalPolicy, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as opensearch from '../lib'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const key = new kms.Key(this, 'Key'); + + const domainProps: opensearch.DomainProps = { + removalPolicy: RemovalPolicy.DESTROY, + version: opensearch.EngineVersion.ELASTICSEARCH_7_1, + encryptionAtRest: { + enabled: true, + kmsKey: key, + }, + accessPolicies: [ + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['es:ESHttp*'], + principals: [new iam.AccountRootPrincipal()], + resources: ['*'], + }), + ], + }; + + new opensearch.Domain(this, 'Domain', domainProps); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-opensearch-custom-kms-key'); +app.synth(); diff --git a/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.expected.json b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.expected.json new file mode 100644 index 0000000000000..f317e3a7eb835 --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.expected.json @@ -0,0 +1,624 @@ +{ + "Resources": { + "Domain1SlowSearchLogs8F3B0506": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 30 + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "Domain1AppLogs6E8D1D67": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 30 + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "Domain1ESLogGroupPolicyc881416c4fcb1ec2b4bf7f47a5cde4097f01ec50fcCustomResourcePolicyBE9BFE5D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "logs:PutResourcePolicy", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "logs:DeleteResourcePolicy", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "Domain1ESLogGroupPolicyc881416c4fcb1ec2b4bf7f47a5cde4097f01ec50fcCustomResourcePolicyBE9BFE5D", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + } + }, + "Domain1ESLogGroupPolicyc881416c4fcb1ec2b4bf7f47a5cde4097f01ec50fc3D726D58": { + "Type": "Custom::CloudwatchLogResourcePolicy", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", + "Arn" + ] + }, + "Create": { + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc881416c4fcb1ec2b4bf7f47a5cde4097f01ec50fc\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"es.amazonaws.com\\\"},\\\"Resource\\\":[\\\"", + { + "Fn::GetAtt": [ + "Domain1SlowSearchLogs8F3B0506", + "Arn" + ] + }, + "\\\",\\\"", + { + "Fn::GetAtt": [ + "Domain1AppLogs6E8D1D67", + "Arn" + ] + }, + "\\\"]}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"ESLogGroupPolicyc881416c4fcb1ec2b4bf7f47a5cde4097f01ec50fc\"}}" + ] + ] + }, + "Update": { + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc881416c4fcb1ec2b4bf7f47a5cde4097f01ec50fc\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"es.amazonaws.com\\\"},\\\"Resource\\\":[\\\"", + { + "Fn::GetAtt": [ + "Domain1SlowSearchLogs8F3B0506", + "Arn" + ] + }, + "\\\",\\\"", + { + "Fn::GetAtt": [ + "Domain1AppLogs6E8D1D67", + "Arn" + ] + }, + "\\\"]}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"ESLogGroupPolicyc881416c4fcb1ec2b4bf7f47a5cde4097f01ec50fc\"}}" + ] + ] + }, + "Delete": "{\"service\":\"CloudWatchLogs\",\"action\":\"deleteResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc881416c4fcb1ec2b4bf7f47a5cde4097f01ec50fc\"},\"ignoreErrorCodesMatching\":\"400\"}", + "InstallLatestAwsSdk": true + }, + "DependsOn": [ + "Domain1ESLogGroupPolicyc881416c4fcb1ec2b4bf7f47a5cde4097f01ec50fcCustomResourcePolicyBE9BFE5D" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "Domain19FCBCB91": { + "Type": "AWS::OpenSearchService::Domain", + "Properties": { + "AdvancedOptions": { + "rest.action.multi.allow_explicit_index": "false", + "indices.fielddata.cache.size": "25", + "indices.query.bool.max_clause_count": "2048" + }, + "ClusterConfig": { + "InstanceCount": 1, + "DedicatedMasterEnabled": false, + "InstanceType": "r5.large.search", + "ZoneAwarenessEnabled": false + }, + "CognitoOptions": { + "Enabled": false + }, + "DomainEndpointOptions": { + "EnforceHTTPS": false, + "TLSSecurityPolicy": "Policy-Min-TLS-1-0-2019-07" + }, + "EBSOptions": { + "EBSEnabled": true, + "VolumeType": "gp2", + "VolumeSize": 10 + }, + "EncryptionAtRestOptions": { + "Enabled": true + }, + "EngineVersion": "Elasticsearch_7.1", + "LogPublishingOptions": { + "ES_APPLICATION_LOGS": { + "CloudWatchLogsLogGroupArn": { + "Fn::GetAtt": [ + "Domain1AppLogs6E8D1D67", + "Arn" + ] + }, + "Enabled": true + }, + "SEARCH_SLOW_LOGS": { + "CloudWatchLogsLogGroupArn": { + "Fn::GetAtt": [ + "Domain1SlowSearchLogs8F3B0506", + "Arn" + ] + }, + "Enabled": true + } + }, + "NodeToNodeEncryptionOptions": { + "Enabled": true + } + }, + "DependsOn": [ + "Domain1ESLogGroupPolicyc881416c4fcb1ec2b4bf7f47a5cde4097f01ec50fcCustomResourcePolicyBE9BFE5D", + "Domain1ESLogGroupPolicyc881416c4fcb1ec2b4bf7f47a5cde4097f01ec50fc3D726D58" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "Domain1AccessPolicyCustomResourcePolicy3BDE9B82": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "es:UpdateDomainConfig", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Domain19FCBCB91", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "Domain1AccessPolicyCustomResourcePolicy3BDE9B82", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + }, + "DependsOn": [ + "Domain19FCBCB91" + ] + }, + "Domain1AccessPolicy4A36C656": { + "Type": "Custom::OpenSearchAccessPolicy", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", + "Arn" + ] + }, + "Create": { + "Fn::Join": [ + "", + [ + "{\"action\":\"updateDomainConfig\",\"service\":\"OpenSearch\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain19FCBCB91" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPaths\":[\"DomainConfig.AccessPolicies\"],\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain19FCBCB91" + }, + "AccessPolicy\"}}" + ] + ] + }, + "Update": { + "Fn::Join": [ + "", + [ + "{\"action\":\"updateDomainConfig\",\"service\":\"OpenSearch\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain19FCBCB91" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPaths\":[\"DomainConfig.AccessPolicies\"],\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain19FCBCB91" + }, + "AccessPolicy\"}}" + ] + ] + }, + "InstallLatestAwsSdk": true + }, + "DependsOn": [ + "Domain1AccessPolicyCustomResourcePolicy3BDE9B82", + "Domain19FCBCB91" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "AWS679f53fac002430cb0da5b7982bd22872D164C4C": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters0036020ad5459434aa7b01d7769825c4b189b2a7722cc47c4a1bc6dbd9ccf85eS3BucketF2C16119" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0036020ad5459434aa7b01d7769825c4b189b2a7722cc47c4a1bc6dbd9ccf85eS3VersionKey60E022E2" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0036020ad5459434aa7b01d7769825c4b189b2a7722cc47c4a1bc6dbd9ccf85eS3VersionKey60E022E2" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x", + "Timeout": 120 + }, + "DependsOn": [ + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + ] + }, + "Domain2SlowSearchLogs0C75F64B": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 30 + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "Domain2AppLogs810876E2": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 30 + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "Domain2ESLogGroupPolicyc80140a7754e9c0dd4e81167ef19e15da5b55dca02CustomResourcePolicy2DB46870": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "logs:PutResourcePolicy", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "logs:DeleteResourcePolicy", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "Domain2ESLogGroupPolicyc80140a7754e9c0dd4e81167ef19e15da5b55dca02CustomResourcePolicy2DB46870", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + } + }, + "Domain2ESLogGroupPolicyc80140a7754e9c0dd4e81167ef19e15da5b55dca0286FF1B15": { + "Type": "Custom::CloudwatchLogResourcePolicy", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", + "Arn" + ] + }, + "Create": { + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc80140a7754e9c0dd4e81167ef19e15da5b55dca02\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"es.amazonaws.com\\\"},\\\"Resource\\\":[\\\"", + { + "Fn::GetAtt": [ + "Domain2SlowSearchLogs0C75F64B", + "Arn" + ] + }, + "\\\",\\\"", + { + "Fn::GetAtt": [ + "Domain2AppLogs810876E2", + "Arn" + ] + }, + "\\\"]}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"ESLogGroupPolicyc80140a7754e9c0dd4e81167ef19e15da5b55dca02\"}}" + ] + ] + }, + "Update": { + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc80140a7754e9c0dd4e81167ef19e15da5b55dca02\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"es.amazonaws.com\\\"},\\\"Resource\\\":[\\\"", + { + "Fn::GetAtt": [ + "Domain2SlowSearchLogs0C75F64B", + "Arn" + ] + }, + "\\\",\\\"", + { + "Fn::GetAtt": [ + "Domain2AppLogs810876E2", + "Arn" + ] + }, + "\\\"]}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"ESLogGroupPolicyc80140a7754e9c0dd4e81167ef19e15da5b55dca02\"}}" + ] + ] + }, + "Delete": "{\"service\":\"CloudWatchLogs\",\"action\":\"deleteResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc80140a7754e9c0dd4e81167ef19e15da5b55dca02\"},\"ignoreErrorCodesMatching\":\"400\"}", + "InstallLatestAwsSdk": true + }, + "DependsOn": [ + "Domain2ESLogGroupPolicyc80140a7754e9c0dd4e81167ef19e15da5b55dca02CustomResourcePolicy2DB46870" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "Domain2644FE48C": { + "Type": "AWS::OpenSearchService::Domain", + "Properties": { + "AdvancedOptions": { + "rest.action.multi.allow_explicit_index": "false", + "indices.fielddata.cache.size": "25", + "indices.query.bool.max_clause_count": "2048" + }, + "ClusterConfig": { + "InstanceCount": 1, + "DedicatedMasterEnabled": false, + "InstanceType": "r5.large.search", + "ZoneAwarenessEnabled": false + }, + "CognitoOptions": { + "Enabled": false + }, + "DomainEndpointOptions": { + "EnforceHTTPS": false, + "TLSSecurityPolicy": "Policy-Min-TLS-1-0-2019-07" + }, + "EBSOptions": { + "EBSEnabled": true, + "VolumeType": "gp2", + "VolumeSize": 10 + }, + "EncryptionAtRestOptions": { + "Enabled": true + }, + "EngineVersion": "Elasticsearch_7.1", + "LogPublishingOptions": { + "ES_APPLICATION_LOGS": { + "CloudWatchLogsLogGroupArn": { + "Fn::GetAtt": [ + "Domain2AppLogs810876E2", + "Arn" + ] + }, + "Enabled": true + }, + "SEARCH_SLOW_LOGS": { + "CloudWatchLogsLogGroupArn": { + "Fn::GetAtt": [ + "Domain2SlowSearchLogs0C75F64B", + "Arn" + ] + }, + "Enabled": true + } + }, + "NodeToNodeEncryptionOptions": { + "Enabled": true + } + }, + "DependsOn": [ + "Domain2ESLogGroupPolicyc80140a7754e9c0dd4e81167ef19e15da5b55dca02CustomResourcePolicy2DB46870", + "Domain2ESLogGroupPolicyc80140a7754e9c0dd4e81167ef19e15da5b55dca0286FF1B15" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "Domain2AccessPolicyCustomResourcePolicy1FB37294": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "es:UpdateDomainConfig", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Domain2644FE48C", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "Domain2AccessPolicyCustomResourcePolicy1FB37294", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + }, + "DependsOn": [ + "Domain2644FE48C" + ] + }, + "Domain2AccessPolicy8AB4B908": { + "Type": "Custom::OpenSearchAccessPolicy", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", + "Arn" + ] + }, + "Create": { + "Fn::Join": [ + "", + [ + "{\"action\":\"updateDomainConfig\",\"service\":\"OpenSearch\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain2644FE48C" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPaths\":[\"DomainConfig.AccessPolicies\"],\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain2644FE48C" + }, + "AccessPolicy\"}}" + ] + ] + }, + "Update": { + "Fn::Join": [ + "", + [ + "{\"action\":\"updateDomainConfig\",\"service\":\"OpenSearch\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain2644FE48C" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPaths\":[\"DomainConfig.AccessPolicies\"],\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain2644FE48C" + }, + "AccessPolicy\"}}" + ] + ] + }, + "InstallLatestAwsSdk": true + }, + "DependsOn": [ + "Domain2AccessPolicyCustomResourcePolicy1FB37294", + "Domain2644FE48C" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + }, + "Parameters": { + "AssetParameters0036020ad5459434aa7b01d7769825c4b189b2a7722cc47c4a1bc6dbd9ccf85eS3BucketF2C16119": { + "Type": "String", + "Description": "S3 bucket for asset \"0036020ad5459434aa7b01d7769825c4b189b2a7722cc47c4a1bc6dbd9ccf85e\"" + }, + "AssetParameters0036020ad5459434aa7b01d7769825c4b189b2a7722cc47c4a1bc6dbd9ccf85eS3VersionKey60E022E2": { + "Type": "String", + "Description": "S3 key for asset version \"0036020ad5459434aa7b01d7769825c4b189b2a7722cc47c4a1bc6dbd9ccf85e\"" + }, + "AssetParameters0036020ad5459434aa7b01d7769825c4b189b2a7722cc47c4a1bc6dbd9ccf85eArtifactHash4E1710B0": { + "Type": "String", + "Description": "Artifact hash for asset \"0036020ad5459434aa7b01d7769825c4b189b2a7722cc47c4a1bc6dbd9ccf85e\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.ts b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.ts new file mode 100644 index 0000000000000..ffa07c27995fc --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.ts @@ -0,0 +1,51 @@ +/// !cdk-integ pragma:ignore-assets +import { EbsDeviceVolumeType } from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import { App, RemovalPolicy, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as opensearch from '../lib'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const domainProps: opensearch.DomainProps = { + removalPolicy: RemovalPolicy.DESTROY, + version: opensearch.EngineVersion.ELASTICSEARCH_7_1, + ebs: { + volumeSize: 10, + volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, + }, + logging: { + slowSearchLogEnabled: true, + appLogEnabled: true, + }, + nodeToNodeEncryption: true, + encryptionAtRest: { + enabled: true, + }, + advancedOptions: { + 'rest.action.multi.allow_explicit_index': 'false', + 'indices.fielddata.cache.size': '25', + 'indices.query.bool.max_clause_count': '2048', + }, + // test the access policies custom resource works + accessPolicies: [ + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['es:ESHttp*'], + principals: [new iam.AccountRootPrincipal()], + resources: ['*'], + }), + ], + }; + + // create 2 domains to ensure that Cloudwatch Log Group policy names dont conflict + new opensearch.Domain(this, 'Domain1', domainProps); + new opensearch.Domain(this, 'Domain2', domainProps); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-opensearch'); +app.synth(); diff --git a/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.ultrawarm.expected.json b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.ultrawarm.expected.json new file mode 100644 index 0000000000000..b41dbfb657b05 --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.ultrawarm.expected.json @@ -0,0 +1,42 @@ +{ + "Resources": { + "Domain66AC69E0": { + "Type": "AWS::OpenSearchService::Domain", + "Properties": { + "ClusterConfig": { + "InstanceCount": 1, + "WarmEnabled": true, + "WarmCount": 2, + "DedicatedMasterEnabled": true, + "DedicatedMasterCount": 2, + "InstanceType": "r5.large.search", + "WarmType": "ultrawarm1.medium.search", + "ZoneAwarenessEnabled": false, + "DedicatedMasterType": "r5.large.search" + }, + "CognitoOptions": { + "Enabled": false + }, + "DomainEndpointOptions": { + "EnforceHTTPS": false, + "TLSSecurityPolicy": "Policy-Min-TLS-1-0-2019-07" + }, + "EBSOptions": { + "EBSEnabled": true, + "VolumeType": "gp2", + "VolumeSize": 10 + }, + "EncryptionAtRestOptions": { + "Enabled": false + }, + "EngineVersion": "Elasticsearch_7.1", + "LogPublishingOptions": {}, + "NodeToNodeEncryptionOptions": { + "Enabled": false + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.ultrawarm.ts b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.ultrawarm.ts new file mode 100644 index 0000000000000..79d349478212a --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.ultrawarm.ts @@ -0,0 +1,22 @@ +import { App, RemovalPolicy, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as opensearch from '../lib'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + new opensearch.Domain(this, 'Domain', { + removalPolicy: RemovalPolicy.DESTROY, + version: opensearch.EngineVersion.ELASTICSEARCH_7_1, + capacity: { + masterNodes: 2, + warmNodes: 2, + }, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-opensearch-ultrawarm'); +app.synth(); diff --git a/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.unsignedbasicauth.expected.json b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.unsignedbasicauth.expected.json new file mode 100644 index 0000000000000..c2e8d28a0d35f --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.unsignedbasicauth.expected.json @@ -0,0 +1,259 @@ +{ + "Resources": { + "DomainMasterUserBFAFA7D9": { + "Type": "AWS::SecretsManager::Secret", + "Properties": { + "GenerateSecretString": { + "ExcludeCharacters": "{}'\\*[]()`", + "GenerateStringKey": "password", + "SecretStringTemplate": "{\"username\":\"admin\"}" + } + } + }, + "Domain66AC69E0": { + "Type": "AWS::OpenSearchService::Domain", + "Properties": { + "AdvancedSecurityOptions": { + "Enabled": true, + "MasterUserOptions": { + "MasterUserPassword": { + "Fn::Join": [ + "", + [ + "{{resolve:secretsmanager:", + { + "Ref": "DomainMasterUserBFAFA7D9" + }, + ":SecretString:password::}}" + ] + ] + }, + "MasterUserName": "admin" + }, + "InternalUserDatabaseEnabled": true + }, + "ClusterConfig": { + "InstanceCount": 1, + "DedicatedMasterEnabled": false, + "InstanceType": "r5.large.search", + "ZoneAwarenessEnabled": false + }, + "CognitoOptions": { + "Enabled": false + }, + "DomainEndpointOptions": { + "EnforceHTTPS": true, + "TLSSecurityPolicy": "Policy-Min-TLS-1-0-2019-07" + }, + "EBSOptions": { + "EBSEnabled": true, + "VolumeType": "gp2", + "VolumeSize": 10 + }, + "EncryptionAtRestOptions": { + "Enabled": true + }, + "EngineVersion": "Elasticsearch_7.1", + "LogPublishingOptions": {}, + "NodeToNodeEncryptionOptions": { + "Enabled": true + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "DomainAccessPolicyCustomResourcePolicy107E31EB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "es:UpdateDomainConfig", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Domain66AC69E0", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "DomainAccessPolicyCustomResourcePolicy107E31EB", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + }, + "DependsOn": [ + "Domain66AC69E0" + ] + }, + "DomainAccessPolicyFCD6BE37": { + "Type": "Custom::OpenSearchAccessPolicy", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", + "Arn" + ] + }, + "Create": { + "Fn::Join": [ + "", + [ + "{\"action\":\"updateDomainConfig\",\"service\":\"OpenSearch\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain66AC69E0" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"*\\\"},\\\"Resource\\\":\\\"", + { + "Fn::GetAtt": [ + "Domain66AC69E0", + "Arn" + ] + }, + "/*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPaths\":[\"DomainConfig.AccessPolicies\"],\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain66AC69E0" + }, + "AccessPolicy\"}}" + ] + ] + }, + "Update": { + "Fn::Join": [ + "", + [ + "{\"action\":\"updateDomainConfig\",\"service\":\"OpenSearch\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain66AC69E0" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"*\\\"},\\\"Resource\\\":\\\"", + { + "Fn::GetAtt": [ + "Domain66AC69E0", + "Arn" + ] + }, + "/*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPaths\":[\"DomainConfig.AccessPolicies\"],\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain66AC69E0" + }, + "AccessPolicy\"}}" + ] + ] + }, + "InstallLatestAwsSdk": true + }, + "DependsOn": [ + "DomainAccessPolicyCustomResourcePolicy107E31EB", + "Domain66AC69E0" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "AWS679f53fac002430cb0da5b7982bd22872D164C4C": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters5d4cb1694db181d27240b1dabc7e4d79a7e1051022a41b773652782433c07f7dS3BucketF2AC65B6" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5d4cb1694db181d27240b1dabc7e4d79a7e1051022a41b773652782433c07f7dS3VersionKey8025D5E5" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5d4cb1694db181d27240b1dabc7e4d79a7e1051022a41b773652782433c07f7dS3VersionKey8025D5E5" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x", + "Timeout": 120 + }, + "DependsOn": [ + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + ] + } + }, + "Parameters": { + "AssetParameters5d4cb1694db181d27240b1dabc7e4d79a7e1051022a41b773652782433c07f7dS3BucketF2AC65B6": { + "Type": "String", + "Description": "S3 bucket for asset \"5d4cb1694db181d27240b1dabc7e4d79a7e1051022a41b773652782433c07f7d\"" + }, + "AssetParameters5d4cb1694db181d27240b1dabc7e4d79a7e1051022a41b773652782433c07f7dS3VersionKey8025D5E5": { + "Type": "String", + "Description": "S3 key for asset version \"5d4cb1694db181d27240b1dabc7e4d79a7e1051022a41b773652782433c07f7d\"" + }, + "AssetParameters5d4cb1694db181d27240b1dabc7e4d79a7e1051022a41b773652782433c07f7dArtifactHash30B93F3B": { + "Type": "String", + "Description": "Artifact hash for asset \"5d4cb1694db181d27240b1dabc7e4d79a7e1051022a41b773652782433c07f7d\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.unsignedbasicauth.ts b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.unsignedbasicauth.ts new file mode 100644 index 0000000000000..7678bee89c365 --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.unsignedbasicauth.ts @@ -0,0 +1,20 @@ +/// !cdk-integ pragma:ignore-assets +import { App, RemovalPolicy, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as opensearch from '../lib'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + new opensearch.Domain(this, 'Domain', { + removalPolicy: RemovalPolicy.DESTROY, + version: opensearch.EngineVersion.ELASTICSEARCH_7_1, + useUnsignedBasicAuth: true, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-opensearch-unsignedbasicauth'); +app.synth(); diff --git a/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.vpc.expected.json b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.vpc.expected.json new file mode 100644 index 0000000000000..0f3f524059ab4 --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.vpc.expected.json @@ -0,0 +1,591 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "cdk-integ-opensearch-vpc/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "cdk-integ-opensearch-vpc/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cdk-integ-opensearch-vpc/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "cdk-integ-opensearch-vpc/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "cdk-integ-opensearch-vpc/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "cdk-integ-opensearch-vpc/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cdk-integ-opensearch-vpc/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "cdk-integ-opensearch-vpc/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "cdk-integ-opensearch-vpc/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet3SubnetBE12F0B6": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "cdk-integ-opensearch-vpc/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTable93458DBB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cdk-integ-opensearch-vpc/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + } + } + }, + "VpcPublicSubnet3DefaultRoute4697774F": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet3EIP3A666A23": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "cdk-integ-opensearch-vpc/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3NATGateway7640CD1D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet3EIP3A666A23", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "cdk-integ-opensearch-vpc/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "cdk-integ-opensearch-vpc/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cdk-integ-opensearch-vpc/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "cdk-integ-opensearch-vpc/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cdk-integ-opensearch-vpc/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcPrivateSubnet3SubnetF258B56E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "cdk-integ-opensearch-vpc/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableD98824C7": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cdk-integ-opensearch-vpc/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + } + }, + "VpcPrivateSubnet3DefaultRoute94B74F0D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet3NATGateway7640CD1D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "cdk-integ-opensearch-vpc/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "DomainSecurityGroup48AA5FD6": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Security group for domain Domain", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "Domain66AC69E0": { + "Type": "AWS::OpenSearchService::Domain", + "Properties": { + "ClusterConfig": { + "InstanceCount": 2, + "DedicatedMasterEnabled": false, + "ZoneAwarenessConfig": { + "AvailabilityZoneCount": 2 + }, + "InstanceType": "r5.large.search", + "ZoneAwarenessEnabled": true + }, + "CognitoOptions": { + "Enabled": false + }, + "DomainEndpointOptions": { + "EnforceHTTPS": false, + "TLSSecurityPolicy": "Policy-Min-TLS-1-0-2019-07" + }, + "EBSOptions": { + "EBSEnabled": true, + "VolumeType": "gp2", + "VolumeSize": 10 + }, + "EncryptionAtRestOptions": { + "Enabled": false + }, + "EngineVersion": "Elasticsearch_7.1", + "LogPublishingOptions": {}, + "NodeToNodeEncryptionOptions": { + "Enabled": false + }, + "VPCOptions": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "DomainSecurityGroup48AA5FD6", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.vpc.ts b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.vpc.ts new file mode 100644 index 0000000000000..9bcbdf8ae2d5b --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/test/integ.opensearch.vpc.ts @@ -0,0 +1,28 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import { App, Stack, StackProps, RemovalPolicy } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as opensearch from '../lib'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const vpc = new ec2.Vpc(this, 'Vpc'); + const domainProps: opensearch.DomainProps = { + version: opensearch.EngineVersion.ELASTICSEARCH_7_1, + removalPolicy: RemovalPolicy.DESTROY, + vpc, + zoneAwareness: { + enabled: true, + }, + capacity: { + dataNodes: 2, + }, + }; + new opensearch.Domain(this, 'Domain', domainProps); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-opensearch-vpc'); +app.synth(); diff --git a/packages/@aws-cdk/aws-opensearchservice/test/log-group-resource-policy.test.ts b/packages/@aws-cdk/aws-opensearchservice/test/log-group-resource-policy.test.ts new file mode 100644 index 0000000000000..68518297588c9 --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/test/log-group-resource-policy.test.ts @@ -0,0 +1,65 @@ +import '@aws-cdk/assert-internal/jest'; +import * as iam from '@aws-cdk/aws-iam'; +import { App, Stack } from '@aws-cdk/core'; +import { LogGroupResourcePolicy } from '../lib/log-group-resource-policy'; + +let app: App; +let stack: Stack; + +beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + env: { account: '1234', region: 'testregion' }, + }); +}); + +test('minimal example renders correctly', () => { + new LogGroupResourcePolicy(stack, 'LogGroupResourcePolicy', { + policyName: 'TestPolicy', + policyStatements: [new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['logs:PutLogEvents', 'logs:CreateLogStream'], + resources: ['*'], + principals: [new iam.ServicePrincipal('es.amazonaws.com')], + })], + }); + + expect(stack).toHaveResource('Custom::CloudwatchLogResourcePolicy', { + ServiceToken: { + 'Fn::GetAtt': [ + 'AWS679f53fac002430cb0da5b7982bd22872D164C4C', + 'Arn', + ], + }, + Create: JSON.stringify({ + service: 'CloudWatchLogs', + action: 'putResourcePolicy', + parameters: { + policyName: 'TestPolicy', + policyDocument: '{"Statement":[{"Action":["logs:PutLogEvents","logs:CreateLogStream"],"Effect":"Allow","Principal":{"Service":"es.amazonaws.com"},"Resource":"*"}],"Version":"2012-10-17"}', + }, + physicalResourceId: { + id: 'LogGroupResourcePolicy', + }, + }), + Update: JSON.stringify({ + service: 'CloudWatchLogs', + action: 'putResourcePolicy', + parameters: { + policyName: 'TestPolicy', + policyDocument: '{"Statement":[{"Action":["logs:PutLogEvents","logs:CreateLogStream"],"Effect":"Allow","Principal":{"Service":"es.amazonaws.com"},"Resource":"*"}],"Version":"2012-10-17"}', + }, + physicalResourceId: { + id: 'LogGroupResourcePolicy', + }, + }), + Delete: JSON.stringify({ + service: 'CloudWatchLogs', + action: 'deleteResourcePolicy', + parameters: { + policyName: 'TestPolicy', + }, + ignoreErrorCodesMatching: '400', + }), + }); +}); diff --git a/packages/@aws-cdk/aws-opensearchservice/test/opensearch-access-policy.test.ts b/packages/@aws-cdk/aws-opensearchservice/test/opensearch-access-policy.test.ts new file mode 100644 index 0000000000000..62e4361f76e46 --- /dev/null +++ b/packages/@aws-cdk/aws-opensearchservice/test/opensearch-access-policy.test.ts @@ -0,0 +1,68 @@ +import '@aws-cdk/assert-internal/jest'; +import * as iam from '@aws-cdk/aws-iam'; +import { App, Stack } from '@aws-cdk/core'; +import { OpenSearchAccessPolicy } from '../lib/opensearch-access-policy'; + +let app: App; +let stack: Stack; + +beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + env: { account: '1234', region: 'testregion' }, + }); +}); + +test('minimal example renders correctly', () => { + const domainArn = 'test:arn'; + + new OpenSearchAccessPolicy(stack, 'ElasticsearchAccessPolicy', { + domainName: 'TestDomain', + domainArn: domainArn, + accessPolicies: [new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['es:ESHttp*'], + principals: [new iam.AnyPrincipal()], + resources: [domainArn], + + })], + }); + + expect(stack).toHaveResource('Custom::OpenSearchAccessPolicy', { + ServiceToken: { + 'Fn::GetAtt': [ + 'AWS679f53fac002430cb0da5b7982bd22872D164C4C', + 'Arn', + ], + }, + Create: JSON.stringify({ + action: 'updateDomainConfig', + service: 'OpenSearch', + parameters: { + DomainName: 'TestDomain', + AccessPolicies: '{"Statement":[{"Action":"es:ESHttp*","Effect":"Allow","Principal":{"AWS":"*"},"Resource":"test:arn"}],"Version":"2012-10-17"}', + }, + outputPaths: ['DomainConfig.AccessPolicies'], + physicalResourceId: { id: 'TestDomainAccessPolicy' }, + }), + Update: JSON.stringify({ + action: 'updateDomainConfig', + service: 'OpenSearch', + parameters: { + DomainName: 'TestDomain', + AccessPolicies: '{"Statement":[{"Action":"es:ESHttp*","Effect":"Allow","Principal":{"AWS":"*"},"Resource":"test:arn"}],"Version":"2012-10-17"}', + }, + outputPaths: ['DomainConfig.AccessPolicies'], + physicalResourceId: { id: 'TestDomainAccessPolicy' }, + }), + }); + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Action: 'es:UpdateDomainConfig', + Effect: 'Allow', + Resource: domainArn, + }], + }, + }); +}); diff --git a/packages/@aws-cdk/cfnspec/spec-source/902_OpenSearch_Domain_patch.json b/packages/@aws-cdk/cfnspec/spec-source/902_OpenSearch_Domain_patch.json new file mode 100644 index 0000000000000..483ea17a40392 --- /dev/null +++ b/packages/@aws-cdk/cfnspec/spec-source/902_OpenSearch_Domain_patch.json @@ -0,0 +1,500 @@ +{ + "PropertyTypes": { + "patch": { + "description": "Add OpenSearch property types", + "operations": [ + { + "op": "add", + "path": "/AWS::OpenSearchService::Domain.ZoneAwarenessConfig", + "value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-zoneawarenessconfig.html", + "Properties": { + "AvailabilityZoneCount": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-zoneawarenessconfig.html#cfn-opensearchservice-domain-zoneawarenessconfig-availabilityzonecount", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "Integer" + } + } + } + }, + { + "op": "add", + "path": "/AWS::OpenSearchService::Domain.ClusterConfig", + "value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-clusterconfig.html", + "Properties": { + "InstanceCount": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-clusterconfig.html#cfn-opensearchservice-domain-clusterconfig-instancecount", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "Integer" + }, + "WarmEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-clusterconfig.html#cfn-opensearchservice-domain-clusterconfig-warmenabled", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "Boolean" + }, + "WarmCount": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-clusterconfig.html#cfn-opensearchservice-domain-clusterconfig-warmcount", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "Integer" + }, + "DedicatedMasterEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-clusterconfig.html#cfn-opensearchservice-domain-clusterconfig-dedicatedmasterenabled", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "Boolean" + }, + "ZoneAwarenessConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-clusterconfig.html#cfn-opensearchservice-domain-clusterconfig-zoneawarenessconfig", + "UpdateType": "Mutable", + "Required": false, + "Type": "ZoneAwarenessConfig" + }, + "DedicatedMasterCount": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-clusterconfig.html#cfn-opensearchservice-domain-clusterconfig-dedicatedmastercount", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "Integer" + }, + "InstanceType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-clusterconfig.html#cfn-opensearchservice-domain-clusterconfig-instancetype", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "String" + }, + "WarmType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-clusterconfig.html#cfn-opensearchservice-domain-clusterconfig-warmtype", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "String" + }, + "ZoneAwarenessEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-clusterconfig.html#cfn-opensearchservice-domain-clusterconfig-zoneawarenessenabled", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "Boolean" + }, + "DedicatedMasterType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-clusterconfig.html#cfn-opensearchservice-domain-clusterconfig-dedicatedmastertype", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "String" + } + } + } + }, + { + "op": "add", + "path": "/AWS::OpenSearchService::Domain.LogPublishingOption", + "value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-logpublishingoption.html", + "Properties": { + "CloudWatchLogsLogGroupArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-logpublishingoption.html#cfn-opensearchservice-domain-logpublishingoption-cloudwatchlogsloggrouparn", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "String" + }, + "Enabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-logpublishingoption.html#cfn-opensearchservice-domain-logpublishingoption-enabled", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "Boolean" + } + } + } + }, + { + "op": "add", + "path": "/AWS::OpenSearchService::Domain.SnapshotOptions", + "value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-snapshotoptions.html", + "Properties": { + "AutomatedSnapshotStartHour": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-snapshotoptions.html#cfn-opensearchservice-domain-snapshotoptions-automatedsnapshotstarthour", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "Integer" + } + } + } + }, + { + "op": "add", + "path": "/AWS::OpenSearchService::Domain.VPCOptions", + "value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-vpcoptions.html", + "Properties": { + "SecurityGroupIds": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-vpcoptions.html#cfn-opensearchservice-domain-vpcoptions-securitygroupids", + "UpdateType": "Mutable", + "Required": false, + "Type": "List", + "PrimitiveItemType": "String", + "DuplicatesAllowed": false + }, + "SubnetIds": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-vpcoptions.html#cfn-opensearchservice-domain-vpcoptions-subnetids", + "UpdateType": "Mutable", + "Required": false, + "Type": "List", + "PrimitiveItemType": "String", + "DuplicatesAllowed": false + } + } + } + }, + { + "op": "add", + "path": "/AWS::OpenSearchService::Domain.NodeToNodeEncryptionOptions", + "value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-nodetonodeencryptionoptions.html", + "Properties": { + "Enabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-nodetonodeencryptionoptions.html#cfn-opensearchservice-domain-nodetonodeencryptionoptions-enabled", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "Boolean" + } + } + } + }, + { + "op": "add", + "path": "/AWS::OpenSearchService::Domain.DomainEndpointOptions", + "value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-domainendpointoptions.html", + "Properties": { + "CustomEndpointCertificateArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-domainendpointoptions.html#cfn-opensearchservice-domain-domainendpointoptions-customendpointcertificatearn", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "String" + }, + "CustomEndpointEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-domainendpointoptions.html#cfn-opensearchservice-domain-domainendpointoptions-customendpointenabled", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "Boolean" + }, + "EnforceHTTPS": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-domainendpointoptions.html#cfn-opensearchservice-domain-domainendpointoptions-enforcehttps", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "Boolean" + }, + "CustomEndpoint": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-domainendpointoptions.html#cfn-opensearchservice-domain-domainendpointoptions-customendpoint", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "String" + }, + "TLSSecurityPolicy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-domainendpointoptions.html#cfn-opensearchservice-domain-domainendpointoptions-tlssecuritypolicy", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "String" + } + } + } + }, + { + "op": "add", + "path": "/AWS::OpenSearchService::Domain.CognitoOptions", + "value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-cognitooptions.html", + "Properties": { + "Enabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-cognitooptions.html#cfn-opensearchservice-domain-cognitooptions-enabled", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "Boolean" + }, + "IdentityPoolId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-cognitooptions.html#cfn-opensearchservice-domain-cognitooptions-identitypoolid", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "String" + }, + "UserPoolId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-cognitooptions.html#cfn-opensearchservice-domain-cognitooptions-userpoolid", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "String" + }, + "RoleArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-cognitooptions.html#cfn-opensearchservice-domain-cognitooptions-rolearn", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "String" + } + } + } + }, + { + "op": "add", + "path": "/AWS::OpenSearchService::Domain.MasterUserOptions", + "value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-masteruseroptions.html", + "Properties": { + "MasterUserPassword": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-masteruseroptions.html#cfn-opensearchservice-domain-masteruseroptions-masteruserpassword", + "UpdateType": "Immutable", + "Required": false, + "PrimitiveType": "String" + }, + "MasterUserName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-masteruseroptions.html#cfn-opensearchservice-domain-masteruseroptions-masterusername", + "UpdateType": "Immutable", + "Required": false, + "PrimitiveType": "String" + }, + "MasterUserARN": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-masteruseroptions.html#cfn-opensearchservice-domain-masteruseroptions-masteruserarn", + "UpdateType": "Immutable", + "Required": false, + "PrimitiveType": "String" + } + } + } + }, + { + "op": "add", + "path": "/AWS::OpenSearchService::Domain.AdvancedSecurityOptionsInput", + "value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-advancedsecurityoptionsinput.html", + "Properties": { + "Enabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-advancedsecurityoptionsinput.html#cfn-opensearchservice-domain-advancedsecurityoptionsinput-enabled", + "UpdateType": "Immutable", + "Required": false, + "PrimitiveType": "Boolean" + }, + "MasterUserOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-advancedsecurityoptionsinput.html#cfn-opensearchservice-domain-advancedsecurityoptionsinput-masteruseroptions", + "UpdateType": "Immutable", + "Required": false, + "Type": "MasterUserOptions" + }, + "InternalUserDatabaseEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-advancedsecurityoptionsinput.html#cfn-opensearchservice-domain-advancedsecurityoptionsinput-internaluserdatabaseenabled", + "UpdateType": "Immutable", + "Required": false, + "PrimitiveType": "Boolean" + } + } + } + }, + { + "op": "add", + "path": "/AWS::OpenSearchService::Domain.EBSOptions", + "value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-ebsoptions.html", + "Properties": { + "EBSEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-ebsoptions.html#cfn-opensearchservice-domain-ebsoptions-ebsenabled", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "Boolean" + }, + "VolumeType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-ebsoptions.html#cfn-opensearchservice-domain-ebsoptions-volumetype", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "String" + }, + "Iops": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-ebsoptions.html#cfn-opensearchservice-domain-ebsoptions-iops", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "Integer" + }, + "VolumeSize": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-ebsoptions.html#cfn-opensearchservice-domain-ebsoptions-volumesize", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "Integer" + } + } + } + }, + { + "op": "add", + "path": "/AWS::OpenSearchService::Domain.EncryptionAtRestOptions", + "value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-encryptionatrestoptions.html", + "Properties": { + "KmsKeyId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-encryptionatrestoptions.html#cfn-opensearchservice-domain-encryptionatrestoptions-kmskeyid", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "String" + }, + "Enabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-encryptionatrestoptions.html#cfn-opensearchservice-domain-encryptionatrestoptions-enabled", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "Boolean" + } + } + } + }, + { + "op": "add", + "path": "/Tag", + "value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html", + "Properties": { + "Key": { + "Required": true, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html#cfn-resource-tags-key", + "PrimitiveType": "String", + "UpdateType": "Mutable" + }, + "Value": { + "Required": true, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html#cfn-resource-tags-value", + "PrimitiveType": "String", + "UpdateType": "Mutable" + } + } + } + } + ] + } + }, + "ResourceTypes": { + "patch": { + "description": "Add OpenSearch resource types", + "operations": [ + { + "op": "add", + "path": "/AWS::OpenSearchService::Domain", + "value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html", + "Properties": { + "ClusterConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html#cfn-opensearchservice-domain-clusterconfig", + "UpdateType": "Mutable", + "Required": false, + "Type": "ClusterConfig" + }, + "DomainName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html#cfn-opensearchservice-domain-domainname", + "UpdateType": "Immutable", + "Required": false, + "PrimitiveType": "String" + }, + "AccessPolicies": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html#cfn-opensearchservice-domain-accesspolicies", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "Json" + }, + "EngineVersion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html#cfn-opensearchservice-domain-engineversion", + "UpdateType": "Mutable", + "Required": false, + "PrimitiveType": "String" + }, + "AdvancedOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html#cfn-opensearchservice-domain-advancedoptions", + "UpdateType": "Mutable", + "Required": false, + "Type": "Map", + "PrimitiveItemType": "String" + }, + "LogPublishingOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html#cfn-opensearchservice-domain-logpublishingoptions", + "UpdateType": "Mutable", + "Required": false, + "Type": "Map", + "ItemType": "LogPublishingOption" + }, + "SnapshotOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html#cfn-opensearchservice-domain-snapshotoptions", + "UpdateType": "Mutable", + "Required": false, + "Type": "SnapshotOptions" + }, + "VPCOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html#cfn-opensearchservice-domain-vpcoptions", + "UpdateType": "Mutable", + "Required": false, + "Type": "VPCOptions" + }, + "NodeToNodeEncryptionOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html#cfn-opensearchservice-domain-nodetonodeencryptionoptions", + "UpdateType": "Mutable", + "Required": false, + "Type": "NodeToNodeEncryptionOptions" + }, + "DomainEndpointOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html#cfn-opensearchservice-domain-domainendpointoptions", + "UpdateType": "Mutable", + "Required": false, + "Type": "DomainEndpointOptions" + }, + "CognitoOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html#cfn-opensearchservice-domain-cognitooptions", + "UpdateType": "Mutable", + "Required": false, + "Type": "CognitoOptions" + }, + "AdvancedSecurityOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html#cfn-opensearchservice-domain-advancedsecurityoptions", + "UpdateType": "Immutable", + "Required": false, + "Type": "AdvancedSecurityOptionsInput" + }, + "EBSOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html#cfn-opensearchservice-domain-ebsoptions", + "UpdateType": "Mutable", + "Required": false, + "Type": "EBSOptions" + }, + "EncryptionAtRestOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html#cfn-opensearchservice-domain-encryptionatrestoptions", + "UpdateType": "Mutable", + "Required": false, + "Type": "EncryptionAtRestOptions" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchservice-domain.html#cfn-opensearchservice-domain-tags", + "UpdateType": "Mutable", + "Required": false, + "Type": "List", + "ItemType": "Tag", + "DuplicatesAllowed": false + } + }, + "Attributes": { + "DomainEndpoint": { + "PrimitiveType": "String" + }, + "Id": { + "PrimitiveType": "String" + }, + "Arn": { + "PrimitiveType": "String" + } + }, + "AllowCreationPolicy": "false", + "AllowUpdatePolicy": "false", + "DeletionPolicy": { + "Default": "Delete", + "AllowedValues": [ + "Delete", + "Retain" + ] + } + } + } + ] + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index 71fcc718b4e3d..b911414e71473 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -189,6 +189,7 @@ "@aws-cdk/aws-networkfirewall": "0.0.0", "@aws-cdk/aws-networkmanager": "0.0.0", "@aws-cdk/aws-nimblestudio": "0.0.0", + "@aws-cdk/aws-opensearchservice": "0.0.0", "@aws-cdk/aws-opsworks": "0.0.0", "@aws-cdk/aws-opsworkscm": "0.0.0", "@aws-cdk/aws-pinpoint": "0.0.0", @@ -359,6 +360,7 @@ "@aws-cdk/aws-networkfirewall": "0.0.0", "@aws-cdk/aws-networkmanager": "0.0.0", "@aws-cdk/aws-nimblestudio": "0.0.0", + "@aws-cdk/aws-opensearchservice": "0.0.0", "@aws-cdk/aws-opsworks": "0.0.0", "@aws-cdk/aws-opsworkscm": "0.0.0", "@aws-cdk/aws-pinpoint": "0.0.0", diff --git a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/aws-sdk-patch/opensearch-2021-01-01.paginators.json b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/aws-sdk-patch/opensearch-2021-01-01.paginators.json new file mode 100644 index 0000000000000..ca82b13bee5a0 --- /dev/null +++ b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/aws-sdk-patch/opensearch-2021-01-01.paginators.json @@ -0,0 +1,64 @@ +{ + "pagination": { + "DescribeDomainAutoTunes": { + "input_token": "NextToken", + "output_token": "NextToken", + "limit_key": "MaxResults" + }, + "DescribeInboundConnections": { + "input_token": "NextToken", + "output_token": "NextToken", + "limit_key": "MaxResults" + }, + "DescribeOutboundConnections": { + "input_token": "NextToken", + "output_token": "NextToken", + "limit_key": "MaxResults" + }, + "DescribePackages": { + "input_token": "NextToken", + "output_token": "NextToken", + "limit_key": "MaxResults" + }, + "DescribeReservedInstanceOfferings": { + "input_token": "NextToken", + "output_token": "NextToken", + "limit_key": "MaxResults" + }, + "DescribeReservedInstances": { + "input_token": "NextToken", + "output_token": "NextToken", + "limit_key": "MaxResults" + }, + "GetPackageVersionHistory": { + "input_token": "NextToken", + "output_token": "NextToken", + "limit_key": "MaxResults" + }, + "GetUpgradeHistory": { + "input_token": "NextToken", + "output_token": "NextToken", + "limit_key": "MaxResults" + }, + "ListDomainsForPackage": { + "input_token": "NextToken", + "output_token": "NextToken", + "limit_key": "MaxResults" + }, + "ListInstanceTypeDetails": { + "input_token": "NextToken", + "output_token": "NextToken", + "limit_key": "MaxResults" + }, + "ListPackagesForDomain": { + "input_token": "NextToken", + "output_token": "NextToken", + "limit_key": "MaxResults" + }, + "ListVersions": { + "input_token": "NextToken", + "output_token": "NextToken", + "limit_key": "MaxResults" + } + } +} diff --git a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/aws-sdk-patch/opensearch-2021-01-01.service.json b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/aws-sdk-patch/opensearch-2021-01-01.service.json new file mode 100644 index 0000000000000..a60efd0d1fc12 --- /dev/null +++ b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/aws-sdk-patch/opensearch-2021-01-01.service.json @@ -0,0 +1,4080 @@ +{ + "version":"2.0", + "metadata":{ + "apiVersion":"2021-01-01", + "endpointPrefix":"es", + "protocol":"rest-json", + "serviceFullName":"Amazon OpenSearch Service", + "serviceId":"OpenSearch", + "signatureVersion":"v4", + "uid":"opensearch-2021-01-01" + }, + "operations":{ + "AcceptInboundConnection":{ + "name":"AcceptInboundConnection", + "http":{ + "method":"PUT", + "requestUri":"/2021-01-01/opensearch/cc/inboundConnection/{ConnectionId}/accept" + }, + "input":{"shape":"AcceptInboundConnectionRequest"}, + "output":{"shape":"AcceptInboundConnectionResponse"}, + "errors":[ + {"shape":"ResourceNotFoundException"}, + {"shape":"LimitExceededException"}, + {"shape":"DisabledOperationException"} + ], + "documentation":"

Allows the remote domain owner to accept an inbound cross-cluster connection request.

" + }, + "AddTags":{ + "name":"AddTags", + "http":{ + "method":"POST", + "requestUri":"/2021-01-01/tags" + }, + "input":{"shape":"AddTagsRequest"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"LimitExceededException"}, + {"shape":"ValidationException"}, + {"shape":"InternalException"} + ], + "documentation":"

Attaches tags to an existing domain. Tags are a set of case-sensitive key value pairs. An domain can have up to 10 tags. See Tagging Amazon OpenSearch Service domains for more information.

" + }, + "AssociatePackage":{ + "name":"AssociatePackage", + "http":{ + "method":"POST", + "requestUri":"/2021-01-01/packages/associate/{PackageID}/{DomainName}" + }, + "input":{"shape":"AssociatePackageRequest"}, + "output":{"shape":"AssociatePackageResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"InternalException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"AccessDeniedException"}, + {"shape":"ValidationException"}, + {"shape":"ConflictException"} + ], + "documentation":"

Associates a package with an Amazon OpenSearch Service domain.

" + }, + "CancelServiceSoftwareUpdate":{ + "name":"CancelServiceSoftwareUpdate", + "http":{ + "method":"POST", + "requestUri":"/2021-01-01/opensearch/serviceSoftwareUpdate/cancel" + }, + "input":{"shape":"CancelServiceSoftwareUpdateRequest"}, + "output":{"shape":"CancelServiceSoftwareUpdateResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"InternalException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"ValidationException"} + ], + "documentation":"

Cancels a scheduled service software update for an Amazon OpenSearch Service domain. You can only perform this operation before the AutomatedUpdateDate and when the UpdateStatus is in the PENDING_UPDATE state.

" + }, + "CreateDomain":{ + "name":"CreateDomain", + "http":{ + "method":"POST", + "requestUri":"/2021-01-01/opensearch/domain" + }, + "input":{"shape":"CreateDomainRequest"}, + "output":{"shape":"CreateDomainResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"DisabledOperationException"}, + {"shape":"InternalException"}, + {"shape":"InvalidTypeException"}, + {"shape":"LimitExceededException"}, + {"shape":"ResourceAlreadyExistsException"}, + {"shape":"ValidationException"} + ], + "documentation":"

Creates a new Amazon OpenSearch Service domain. For more information, see Creating and managing Amazon OpenSearch Service domains in the Amazon OpenSearch Service Developer Guide.

" + }, + "CreateOutboundConnection":{ + "name":"CreateOutboundConnection", + "http":{ + "method":"POST", + "requestUri":"/2021-01-01/opensearch/cc/outboundConnection" + }, + "input":{"shape":"CreateOutboundConnectionRequest"}, + "output":{"shape":"CreateOutboundConnectionResponse"}, + "errors":[ + {"shape":"LimitExceededException"}, + {"shape":"InternalException"}, + {"shape":"ResourceAlreadyExistsException"}, + {"shape":"DisabledOperationException"} + ], + "documentation":"

Creates a new cross-cluster connection from a local OpenSearch domain to a remote OpenSearch domain.

" + }, + "CreatePackage":{ + "name":"CreatePackage", + "http":{ + "method":"POST", + "requestUri":"/2021-01-01/packages" + }, + "input":{"shape":"CreatePackageRequest"}, + "output":{"shape":"CreatePackageResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"InternalException"}, + {"shape":"LimitExceededException"}, + {"shape":"InvalidTypeException"}, + {"shape":"ResourceAlreadyExistsException"}, + {"shape":"AccessDeniedException"}, + {"shape":"ValidationException"} + ], + "documentation":"

Create a package for use with Amazon OpenSearch Service domains.

" + }, + "DeleteDomain":{ + "name":"DeleteDomain", + "http":{ + "method":"DELETE", + "requestUri":"/2021-01-01/opensearch/domain/{DomainName}" + }, + "input":{"shape":"DeleteDomainRequest"}, + "output":{"shape":"DeleteDomainResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"InternalException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"ValidationException"} + ], + "documentation":"

Permanently deletes the specified domain and all of its data. Once a domain is deleted, it cannot be recovered.

" + }, + "DeleteInboundConnection":{ + "name":"DeleteInboundConnection", + "http":{ + "method":"DELETE", + "requestUri":"/2021-01-01/opensearch/cc/inboundConnection/{ConnectionId}" + }, + "input":{"shape":"DeleteInboundConnectionRequest"}, + "output":{"shape":"DeleteInboundConnectionResponse"}, + "errors":[ + {"shape":"ResourceNotFoundException"}, + {"shape":"DisabledOperationException"} + ], + "documentation":"

Allows the remote domain owner to delete an existing inbound cross-cluster connection.

" + }, + "DeleteOutboundConnection":{ + "name":"DeleteOutboundConnection", + "http":{ + "method":"DELETE", + "requestUri":"/2021-01-01/opensearch/cc/outboundConnection/{ConnectionId}" + }, + "input":{"shape":"DeleteOutboundConnectionRequest"}, + "output":{"shape":"DeleteOutboundConnectionResponse"}, + "errors":[ + {"shape":"ResourceNotFoundException"}, + {"shape":"DisabledOperationException"} + ], + "documentation":"

Allows the local domain owner to delete an existing outbound cross-cluster connection.

" + }, + "DeletePackage":{ + "name":"DeletePackage", + "http":{ + "method":"DELETE", + "requestUri":"/2021-01-01/packages/{PackageID}" + }, + "input":{"shape":"DeletePackageRequest"}, + "output":{"shape":"DeletePackageResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"InternalException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"AccessDeniedException"}, + {"shape":"ValidationException"}, + {"shape":"ConflictException"} + ], + "documentation":"

Deletes the package.

" + }, + "DescribeDomain":{ + "name":"DescribeDomain", + "http":{ + "method":"GET", + "requestUri":"/2021-01-01/opensearch/domain/{DomainName}" + }, + "input":{"shape":"DescribeDomainRequest"}, + "output":{"shape":"DescribeDomainResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"InternalException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"ValidationException"} + ], + "documentation":"

Returns domain configuration information about the specified domain, including the domain ID, domain endpoint, and domain ARN.

" + }, + "DescribeDomainAutoTunes":{ + "name":"DescribeDomainAutoTunes", + "http":{ + "method":"GET", + "requestUri":"/2021-01-01/opensearch/domain/{DomainName}/autoTunes" + }, + "input":{"shape":"DescribeDomainAutoTunesRequest"}, + "output":{"shape":"DescribeDomainAutoTunesResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"InternalException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"ValidationException"} + ], + "documentation":"

Provides scheduled Auto-Tune action details for the domain, such as Auto-Tune action type, description, severity, and scheduled date.

" + }, + "DescribeDomainConfig":{ + "name":"DescribeDomainConfig", + "http":{ + "method":"GET", + "requestUri":"/2021-01-01/opensearch/domain/{DomainName}/config" + }, + "input":{"shape":"DescribeDomainConfigRequest"}, + "output":{"shape":"DescribeDomainConfigResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"InternalException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"ValidationException"} + ], + "documentation":"

Provides cluster configuration information about the specified domain, such as the state, creation date, update version, and update date for cluster options.

" + }, + "DescribeDomains":{ + "name":"DescribeDomains", + "http":{ + "method":"POST", + "requestUri":"/2021-01-01/opensearch/domain-info" + }, + "input":{"shape":"DescribeDomainsRequest"}, + "output":{"shape":"DescribeDomainsResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"InternalException"}, + {"shape":"ValidationException"} + ], + "documentation":"

Returns domain configuration information about the specified domains, including the domain ID, domain endpoint, and domain ARN.

" + }, + "DescribeInboundConnections":{ + "name":"DescribeInboundConnections", + "http":{ + "method":"POST", + "requestUri":"/2021-01-01/opensearch/cc/inboundConnection/search" + }, + "input":{"shape":"DescribeInboundConnectionsRequest"}, + "output":{"shape":"DescribeInboundConnectionsResponse"}, + "errors":[ + {"shape":"InvalidPaginationTokenException"}, + {"shape":"DisabledOperationException"} + ], + "documentation":"

Lists all the inbound cross-cluster connections for a remote domain.

" + }, + "DescribeInstanceTypeLimits":{ + "name":"DescribeInstanceTypeLimits", + "http":{ + "method":"GET", + "requestUri":"/2021-01-01/opensearch/instanceTypeLimits/{EngineVersion}/{InstanceType}" + }, + "input":{"shape":"DescribeInstanceTypeLimitsRequest"}, + "output":{"shape":"DescribeInstanceTypeLimitsResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"InternalException"}, + {"shape":"InvalidTypeException"}, + {"shape":"LimitExceededException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"ValidationException"} + ], + "documentation":"

Describe the limits for a given instance type and OpenSearch or Elasticsearch version. When modifying an existing domain, specify the DomainName to see which limits you can modify.

" + }, + "DescribeOutboundConnections":{ + "name":"DescribeOutboundConnections", + "http":{ + "method":"POST", + "requestUri":"/2021-01-01/opensearch/cc/outboundConnection/search" + }, + "input":{"shape":"DescribeOutboundConnectionsRequest"}, + "output":{"shape":"DescribeOutboundConnectionsResponse"}, + "errors":[ + {"shape":"InvalidPaginationTokenException"}, + {"shape":"DisabledOperationException"} + ], + "documentation":"

Lists all the outbound cross-cluster connections for a local domain.

" + }, + "DescribePackages":{ + "name":"DescribePackages", + "http":{ + "method":"POST", + "requestUri":"/2021-01-01/packages/describe" + }, + "input":{"shape":"DescribePackagesRequest"}, + "output":{"shape":"DescribePackagesResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"InternalException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"AccessDeniedException"}, + {"shape":"ValidationException"} + ], + "documentation":"

Describes all packages available to Amazon OpenSearch Service domains. Includes options for filtering, limiting the number of results, and pagination.

" + }, + "DescribeReservedInstanceOfferings":{ + "name":"DescribeReservedInstanceOfferings", + "http":{ + "method":"GET", + "requestUri":"/2021-01-01/opensearch/reservedInstanceOfferings" + }, + "input":{"shape":"DescribeReservedInstanceOfferingsRequest"}, + "output":{"shape":"DescribeReservedInstanceOfferingsResponse"}, + "errors":[ + {"shape":"ResourceNotFoundException"}, + {"shape":"ValidationException"}, + {"shape":"DisabledOperationException"}, + {"shape":"InternalException"} + ], + "documentation":"

Lists available reserved OpenSearch instance offerings.

" + }, + "DescribeReservedInstances":{ + "name":"DescribeReservedInstances", + "http":{ + "method":"GET", + "requestUri":"/2021-01-01/opensearch/reservedInstances" + }, + "input":{"shape":"DescribeReservedInstancesRequest"}, + "output":{"shape":"DescribeReservedInstancesResponse"}, + "errors":[ + {"shape":"ResourceNotFoundException"}, + {"shape":"InternalException"}, + {"shape":"ValidationException"}, + {"shape":"DisabledOperationException"} + ], + "documentation":"

Returns information about reserved OpenSearch instances for this account.

" + }, + "DissociatePackage":{ + "name":"DissociatePackage", + "http":{ + "method":"POST", + "requestUri":"/2021-01-01/packages/dissociate/{PackageID}/{DomainName}" + }, + "input":{"shape":"DissociatePackageRequest"}, + "output":{"shape":"DissociatePackageResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"InternalException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"AccessDeniedException"}, + {"shape":"ValidationException"}, + {"shape":"ConflictException"} + ], + "documentation":"

Dissociates a package from the Amazon OpenSearch Service domain.

" + }, + "GetCompatibleVersions":{ + "name":"GetCompatibleVersions", + "http":{ + "method":"GET", + "requestUri":"/2021-01-01/opensearch/compatibleVersions" + }, + "input":{"shape":"GetCompatibleVersionsRequest"}, + "output":{"shape":"GetCompatibleVersionsResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"DisabledOperationException"}, + {"shape":"ValidationException"}, + {"shape":"InternalException"} + ], + "documentation":"

Returns a list of upgrade-compatible versions of OpenSearch/Elasticsearch. You can optionally pass a DomainName to get all upgrade-compatible versions of OpenSearch/Elasticsearch for that specific domain.

" + }, + "GetPackageVersionHistory":{ + "name":"GetPackageVersionHistory", + "http":{ + "method":"GET", + "requestUri":"/2021-01-01/packages/{PackageID}/history" + }, + "input":{"shape":"GetPackageVersionHistoryRequest"}, + "output":{"shape":"GetPackageVersionHistoryResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"InternalException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"AccessDeniedException"}, + {"shape":"ValidationException"} + ], + "documentation":"

Returns a list of package versions, along with their creation time and commit message.

" + }, + "GetUpgradeHistory":{ + "name":"GetUpgradeHistory", + "http":{ + "method":"GET", + "requestUri":"/2021-01-01/opensearch/upgradeDomain/{DomainName}/history" + }, + "input":{"shape":"GetUpgradeHistoryRequest"}, + "output":{"shape":"GetUpgradeHistoryResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"DisabledOperationException"}, + {"shape":"ValidationException"}, + {"shape":"InternalException"} + ], + "documentation":"

Retrieves the complete history of the last 10 upgrades performed on the domain.

" + }, + "GetUpgradeStatus":{ + "name":"GetUpgradeStatus", + "http":{ + "method":"GET", + "requestUri":"/2021-01-01/opensearch/upgradeDomain/{DomainName}/status" + }, + "input":{"shape":"GetUpgradeStatusRequest"}, + "output":{"shape":"GetUpgradeStatusResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"DisabledOperationException"}, + {"shape":"ValidationException"}, + {"shape":"InternalException"} + ], + "documentation":"

Retrieves the latest status of the last upgrade or upgrade eligibility check performed on the domain.

" + }, + "ListDomainNames":{ + "name":"ListDomainNames", + "http":{ + "method":"GET", + "requestUri":"/2021-01-01/domain" + }, + "output":{"shape":"ListDomainNamesResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"ValidationException"} + ], + "documentation":"

Returns the names of all domains owned by the current user's account.

" + }, + "ListDomainsForPackage":{ + "name":"ListDomainsForPackage", + "http":{ + "method":"GET", + "requestUri":"/2021-01-01/packages/{PackageID}/domains" + }, + "input":{"shape":"ListDomainsForPackageRequest"}, + "output":{"shape":"ListDomainsForPackageResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"InternalException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"AccessDeniedException"}, + {"shape":"ValidationException"} + ], + "documentation":"

Lists all Amazon OpenSearch Service domains associated with the package.

" + }, + "ListInstanceTypeDetails":{ + "name":"ListInstanceTypeDetails", + "http":{ + "method":"GET", + "requestUri":"/2021-01-01/opensearch/instanceTypeDetails/{EngineVersion}" + }, + "input":{"shape":"ListInstanceTypeDetailsRequest"}, + "output":{"shape":"ListInstanceTypeDetailsResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"InternalException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"ValidationException"} + ] + }, + "ListPackagesForDomain":{ + "name":"ListPackagesForDomain", + "http":{ + "method":"GET", + "requestUri":"/2021-01-01/domain/{DomainName}/packages" + }, + "input":{"shape":"ListPackagesForDomainRequest"}, + "output":{"shape":"ListPackagesForDomainResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"InternalException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"AccessDeniedException"}, + {"shape":"ValidationException"} + ], + "documentation":"

Lists all packages associated with the Amazon OpenSearch Service domain.

" + }, + "ListTags":{ + "name":"ListTags", + "http":{ + "method":"GET", + "requestUri":"/2021-01-01/tags/" + }, + "input":{"shape":"ListTagsRequest"}, + "output":{"shape":"ListTagsResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"ValidationException"}, + {"shape":"InternalException"} + ], + "documentation":"

Returns all tags for the given domain.

" + }, + "ListVersions":{ + "name":"ListVersions", + "http":{ + "method":"GET", + "requestUri":"/2021-01-01/opensearch/versions" + }, + "input":{"shape":"ListVersionsRequest"}, + "output":{"shape":"ListVersionsResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"InternalException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"ValidationException"} + ], + "documentation":"

List all supported versions of OpenSearch and Elasticsearch.

" + }, + "PurchaseReservedInstanceOffering":{ + "name":"PurchaseReservedInstanceOffering", + "http":{ + "method":"POST", + "requestUri":"/2021-01-01/opensearch/purchaseReservedInstanceOffering" + }, + "input":{"shape":"PurchaseReservedInstanceOfferingRequest"}, + "output":{"shape":"PurchaseReservedInstanceOfferingResponse"}, + "errors":[ + {"shape":"ResourceNotFoundException"}, + {"shape":"ResourceAlreadyExistsException"}, + {"shape":"LimitExceededException"}, + {"shape":"DisabledOperationException"}, + {"shape":"ValidationException"}, + {"shape":"InternalException"} + ], + "documentation":"

Allows you to purchase reserved OpenSearch instances.

" + }, + "RejectInboundConnection":{ + "name":"RejectInboundConnection", + "http":{ + "method":"PUT", + "requestUri":"/2021-01-01/opensearch/cc/inboundConnection/{ConnectionId}/reject" + }, + "input":{"shape":"RejectInboundConnectionRequest"}, + "output":{"shape":"RejectInboundConnectionResponse"}, + "errors":[ + {"shape":"ResourceNotFoundException"}, + {"shape":"DisabledOperationException"} + ], + "documentation":"

Allows the remote domain owner to reject an inbound cross-cluster connection request.

" + }, + "RemoveTags":{ + "name":"RemoveTags", + "http":{ + "method":"POST", + "requestUri":"/2021-01-01/tags-removal" + }, + "input":{"shape":"RemoveTagsRequest"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"ValidationException"}, + {"shape":"InternalException"} + ], + "documentation":"

Removes the specified set of tags from the given domain.

" + }, + "StartServiceSoftwareUpdate":{ + "name":"StartServiceSoftwareUpdate", + "http":{ + "method":"POST", + "requestUri":"/2021-01-01/opensearch/serviceSoftwareUpdate/start" + }, + "input":{"shape":"StartServiceSoftwareUpdateRequest"}, + "output":{"shape":"StartServiceSoftwareUpdateResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"InternalException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"ValidationException"} + ], + "documentation":"

Schedules a service software update for an Amazon OpenSearch Service domain.

" + }, + "UpdateDomainConfig":{ + "name":"UpdateDomainConfig", + "http":{ + "method":"POST", + "requestUri":"/2021-01-01/opensearch/domain/{DomainName}/config" + }, + "input":{"shape":"UpdateDomainConfigRequest"}, + "output":{"shape":"UpdateDomainConfigResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"InternalException"}, + {"shape":"InvalidTypeException"}, + {"shape":"LimitExceededException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"ValidationException"} + ], + "documentation":"

Modifies the cluster configuration of the specified domain, such as setting the instance type and the number of instances.

" + }, + "UpdatePackage":{ + "name":"UpdatePackage", + "http":{ + "method":"POST", + "requestUri":"/2021-01-01/packages/update" + }, + "input":{"shape":"UpdatePackageRequest"}, + "output":{"shape":"UpdatePackageResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"InternalException"}, + {"shape":"LimitExceededException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"AccessDeniedException"}, + {"shape":"ValidationException"} + ], + "documentation":"

Updates a package for use with Amazon OpenSearch Service domains.

" + }, + "UpgradeDomain":{ + "name":"UpgradeDomain", + "http":{ + "method":"POST", + "requestUri":"/2021-01-01/opensearch/upgradeDomain" + }, + "input":{"shape":"UpgradeDomainRequest"}, + "output":{"shape":"UpgradeDomainResponse"}, + "errors":[ + {"shape":"BaseException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"ResourceAlreadyExistsException"}, + {"shape":"DisabledOperationException"}, + {"shape":"ValidationException"}, + {"shape":"InternalException"} + ], + "documentation":"

Allows you to either upgrade your domain or perform an upgrade eligibility check to a compatible version of OpenSearch or Elasticsearch.

" + } + }, + "shapes":{ + "ARN":{ + "type":"string", + "documentation":"

The Amazon Resource Name (ARN) of the domain. See Identifiers for IAM Entities in Using AWS Identity and Access Management for more information.

", + "max":2048, + "min":20, + "pattern":".*" + }, + "AWSDomainInformation":{ + "type":"structure", + "required":["DomainName"], + "members":{ + "OwnerId":{"shape":"OwnerId"}, + "DomainName":{"shape":"DomainName"}, + "Region":{"shape":"Region"} + } + }, + "AcceptInboundConnectionRequest":{ + "type":"structure", + "required":["ConnectionId"], + "members":{ + "ConnectionId":{ + "shape":"ConnectionId", + "documentation":"

The ID of the inbound connection you want to accept.

", + "location":"uri", + "locationName":"ConnectionId" + } + }, + "documentation":"

Container for the parameters to the AcceptInboundConnection operation.

" + }, + "AcceptInboundConnectionResponse":{ + "type":"structure", + "members":{ + "Connection":{ + "shape":"InboundConnection", + "documentation":"

The InboundConnection of the accepted inbound connection.

" + } + }, + "documentation":"

The result of an AcceptInboundConnection operation. Contains details about the accepted inbound connection.

" + }, + "AccessPoliciesStatus":{ + "type":"structure", + "required":[ + "Options", + "Status" + ], + "members":{ + "Options":{ + "shape":"PolicyDocument", + "documentation":"

The access policy configured for the domain. Access policies can be resource-based, IP-based, or IAM-based. See Configuring access policiesfor more information.

" + }, + "Status":{ + "shape":"OptionStatus", + "documentation":"

The status of the access policy for the domain. See OptionStatus for the status information that's included.

" + } + }, + "documentation":"

The configured access rules for the domain's document and search endpoints, and the current status of those rules.

" + }, + "AddTagsRequest":{ + "type":"structure", + "required":[ + "ARN", + "TagList" + ], + "members":{ + "ARN":{ + "shape":"ARN", + "documentation":"

Specify the ARN of the domain you want to add tags to.

" + }, + "TagList":{ + "shape":"TagList", + "documentation":"

List of Tag to add to the domain.

" + } + }, + "documentation":"

Container for the parameters to the AddTags operation. Specifies the tags to attach to the domain.

" + }, + "AdditionalLimit":{ + "type":"structure", + "members":{ + "LimitName":{ + "shape":"LimitName", + "documentation":"

Additional limit is specific to a given InstanceType and for each of its InstanceRole etc.
Attributes and their details:

  • MaximumNumberOfDataNodesSupported
  • This attribute is present on the master node only to specify how much data nodes up to which given ESPartitionInstanceType can support as master node.
  • MaximumNumberOfDataNodesWithoutMasterNode
  • This attribute is present on data node only to specify how much data nodes of given ESPartitionInstanceType up to which you don't need any master nodes to govern them.

" + }, + "LimitValues":{ + "shape":"LimitValueList", + "documentation":"

Value for a given AdditionalLimit$LimitName .

" + } + }, + "documentation":"

List of limits that are specific to a given InstanceType and for each of its InstanceRole .

" + }, + "AdditionalLimitList":{ + "type":"list", + "member":{"shape":"AdditionalLimit"} + }, + "AdvancedOptions":{ + "type":"map", + "key":{"shape":"String"}, + "value":{"shape":"String"}, + "documentation":"

Exposes select native OpenSearch configuration values from opensearch.yml. Currently, the following advanced options are available:

  • Option to allow references to indices in an HTTP request body. Must be false when configuring access to individual sub-resources. By default, the value is true. See Advanced cluster parameters for more information.
  • Option to specify the percentage of heap space allocated to field data. By default, this setting is unbounded.

For more information, see Advanced cluster parameters.

" + }, + "AdvancedOptionsStatus":{ + "type":"structure", + "required":[ + "Options", + "Status" + ], + "members":{ + "Options":{ + "shape":"AdvancedOptions", + "documentation":"

The status of advanced options for the specified domain.

" + }, + "Status":{ + "shape":"OptionStatus", + "documentation":"

The OptionStatus for advanced options for the specified domain.

" + } + }, + "documentation":"

Status of the advanced options for the specified domain. Currently, the following advanced options are available:

  • Option to allow references to indices in an HTTP request body. Must be false when configuring access to individual sub-resources. By default, the value is true. See Advanced cluster parameters for more information.
  • Option to specify the percentage of heap space allocated to field data. By default, this setting is unbounded.

For more information, see Advanced cluster parameters.

" + }, + "AdvancedSecurityOptions":{ + "type":"structure", + "members":{ + "Enabled":{ + "shape":"Boolean", + "documentation":"

True if advanced security is enabled.

" + }, + "InternalUserDatabaseEnabled":{ + "shape":"Boolean", + "documentation":"

True if the internal user database is enabled.

" + }, + "SAMLOptions":{ + "shape":"SAMLOptionsOutput", + "documentation":"

Describes the SAML application configured for a domain.

" + } + }, + "documentation":"

The advanced security configuration: whether advanced security is enabled, whether the internal database option is enabled.

" + }, + "AdvancedSecurityOptionsInput":{ + "type":"structure", + "members":{ + "Enabled":{ + "shape":"Boolean", + "documentation":"

True if advanced security is enabled.

" + }, + "InternalUserDatabaseEnabled":{ + "shape":"Boolean", + "documentation":"

True if the internal user database is enabled.

" + }, + "MasterUserOptions":{ + "shape":"MasterUserOptions", + "documentation":"

Credentials for the master user: username and password, ARN, or both.

" + }, + "SAMLOptions":{ + "shape":"SAMLOptionsInput", + "documentation":"

The SAML application configuration for the domain.

" + } + }, + "documentation":"

The advanced security configuration: whether advanced security is enabled, whether the internal database option is enabled, master username and password (if internal database is enabled), and master user ARN (if IAM is enabled).

" + }, + "AdvancedSecurityOptionsStatus":{ + "type":"structure", + "required":[ + "Options", + "Status" + ], + "members":{ + "Options":{ + "shape":"AdvancedSecurityOptions", + "documentation":"

Advanced security options for the specified domain.

" + }, + "Status":{ + "shape":"OptionStatus", + "documentation":"

Status of the advanced security options for the specified domain.

" + } + }, + "documentation":"

The status of advanced security options for the specified domain.

" + }, + "AssociatePackageRequest":{ + "type":"structure", + "required":[ + "PackageID", + "DomainName" + ], + "members":{ + "PackageID":{ + "shape":"PackageID", + "documentation":"

Internal ID of the package to associate with a domain. Use DescribePackages to find this value.

", + "location":"uri", + "locationName":"PackageID" + }, + "DomainName":{ + "shape":"DomainName", + "documentation":"

The name of the domain to associate the package with.

", + "location":"uri", + "locationName":"DomainName" + } + }, + "documentation":"

Container for the request parameters to the AssociatePackage operation.

" + }, + "AssociatePackageResponse":{ + "type":"structure", + "members":{ + "DomainPackageDetails":{ + "shape":"DomainPackageDetails", + "documentation":"

DomainPackageDetails

" + } + }, + "documentation":"

Container for the response returned by AssociatePackage operation.

" + }, + "AutoTune":{ + "type":"structure", + "members":{ + "AutoTuneType":{ + "shape":"AutoTuneType", + "documentation":"

Specifies the Auto-Tune type. Valid value is SCHEDULED_ACTION.

" + }, + "AutoTuneDetails":{ + "shape":"AutoTuneDetails", + "documentation":"

Specifies details about the Auto-Tune action. See Auto-Tune for Amazon OpenSearch Service for more information.

" + } + }, + "documentation":"

Specifies the Auto-Tune type and Auto-Tune action details.

" + }, + "AutoTuneDate":{ + "type":"timestamp", + "documentation":"

The timestamp of the Auto-Tune action scheduled for the domain.

" + }, + "AutoTuneDesiredState":{ + "type":"string", + "documentation":"

The Auto-Tune desired state. Valid values are ENABLED and DISABLED.

", + "enum":[ + "ENABLED", + "DISABLED" + ] + }, + "AutoTuneDetails":{ + "type":"structure", + "members":{ + "ScheduledAutoTuneDetails":{"shape":"ScheduledAutoTuneDetails"} + }, + "documentation":"

Specifies details about the Auto-Tune action. See Auto-Tune for Amazon OpenSearch Service for more information.

" + }, + "AutoTuneList":{ + "type":"list", + "member":{"shape":"AutoTune"} + }, + "AutoTuneMaintenanceSchedule":{ + "type":"structure", + "members":{ + "StartAt":{ + "shape":"StartAt", + "documentation":"

The timestamp at which the Auto-Tune maintenance schedule starts.

" + }, + "Duration":{ + "shape":"Duration", + "documentation":"

Specifies maintenance schedule duration: duration value and duration unit. See Auto-Tune for Amazon OpenSearch Service for more information.

" + }, + "CronExpressionForRecurrence":{ + "shape":"String", + "documentation":"

A cron expression for a recurring maintenance schedule. See Auto-Tune for Amazon OpenSearch Service for more information.

" + } + }, + "documentation":"

Specifies the Auto-Tune maintenance schedule. See Auto-Tune for Amazon OpenSearch Service for more information.

" + }, + "AutoTuneMaintenanceScheduleList":{ + "type":"list", + "member":{"shape":"AutoTuneMaintenanceSchedule"}, + "max":100 + }, + "AutoTuneOptions":{ + "type":"structure", + "members":{ + "DesiredState":{ + "shape":"AutoTuneDesiredState", + "documentation":"

The Auto-Tune desired state. Valid values are ENABLED and DISABLED.

" + }, + "RollbackOnDisable":{ + "shape":"RollbackOnDisable", + "documentation":"

The rollback state while disabling Auto-Tune for the domain. Valid values are NO_ROLLBACK and DEFAULT_ROLLBACK.

" + }, + "MaintenanceSchedules":{ + "shape":"AutoTuneMaintenanceScheduleList", + "documentation":"

A list of maintenance schedules. See Auto-Tune for Amazon OpenSearch Service for more information.

" + } + }, + "documentation":"

The Auto-Tune options: the Auto-Tune desired state for the domain, rollback state when disabling Auto-Tune options and list of maintenance schedules.

" + }, + "AutoTuneOptionsInput":{ + "type":"structure", + "members":{ + "DesiredState":{ + "shape":"AutoTuneDesiredState", + "documentation":"

The Auto-Tune desired state. Valid values are ENABLED and DISABLED.

" + }, + "MaintenanceSchedules":{ + "shape":"AutoTuneMaintenanceScheduleList", + "documentation":"

A list of maintenance schedules. See Auto-Tune for Amazon OpenSearch Service for more information.

" + } + }, + "documentation":"

The Auto-Tune options: the Auto-Tune desired state for the domain and list of maintenance schedules.

" + }, + "AutoTuneOptionsOutput":{ + "type":"structure", + "members":{ + "State":{ + "shape":"AutoTuneState", + "documentation":"

The AutoTuneState for the domain.

" + }, + "ErrorMessage":{ + "shape":"String", + "documentation":"

The error message while enabling or disabling Auto-Tune.

" + } + }, + "documentation":"

The Auto-Tune options: the Auto-Tune desired state for the domain and list of maintenance schedules.

" + }, + "AutoTuneOptionsStatus":{ + "type":"structure", + "members":{ + "Options":{ + "shape":"AutoTuneOptions", + "documentation":"

Specifies Auto-Tune options for the domain.

" + }, + "Status":{ + "shape":"AutoTuneStatus", + "documentation":"

The status of the Auto-Tune options for the domain.

" + } + }, + "documentation":"

The Auto-Tune status for the domain.

" + }, + "AutoTuneState":{ + "type":"string", + "documentation":"

The Auto-Tune state for the domain. For valid states see Auto-Tune for Amazon OpenSearch Service.

", + "enum":[ + "ENABLED", + "DISABLED", + "ENABLE_IN_PROGRESS", + "DISABLE_IN_PROGRESS", + "DISABLED_AND_ROLLBACK_SCHEDULED", + "DISABLED_AND_ROLLBACK_IN_PROGRESS", + "DISABLED_AND_ROLLBACK_COMPLETE", + "DISABLED_AND_ROLLBACK_ERROR", + "ERROR" + ] + }, + "AutoTuneStatus":{ + "type":"structure", + "required":[ + "CreationDate", + "UpdateDate", + "State" + ], + "members":{ + "CreationDate":{ + "shape":"UpdateTimestamp", + "documentation":"

The timestamp of the Auto-Tune options creation date.

" + }, + "UpdateDate":{ + "shape":"UpdateTimestamp", + "documentation":"

The timestamp of when the Auto-Tune options were last updated.

" + }, + "UpdateVersion":{ + "shape":"UIntValue", + "documentation":"

The latest version of the Auto-Tune options.

" + }, + "State":{ + "shape":"AutoTuneState", + "documentation":"

The AutoTuneState for the domain.

" + }, + "ErrorMessage":{ + "shape":"String", + "documentation":"

The error message while enabling or disabling Auto-Tune.

" + }, + "PendingDeletion":{ + "shape":"Boolean", + "documentation":"

Indicates whether the domain is being deleted.

" + } + }, + "documentation":"

Provides the current Auto-Tune status for the domain.

" + }, + "AutoTuneType":{ + "type":"string", + "documentation":"

Specifies the Auto-Tune type. Valid value is SCHEDULED_ACTION.

", + "enum":["SCHEDULED_ACTION"] + }, + "BackendRole":{ + "type":"string", + "max":256, + "min":1 + }, + "Boolean":{"type":"boolean"}, + "CancelServiceSoftwareUpdateRequest":{ + "type":"structure", + "required":["DomainName"], + "members":{ + "DomainName":{ + "shape":"DomainName", + "documentation":"

The name of the domain that you want to stop the latest service software update on.

" + } + }, + "documentation":"

Container for the parameters to the CancelServiceSoftwareUpdate operation. Specifies the name of the domain that you wish to cancel a service software update on.

" + }, + "CancelServiceSoftwareUpdateResponse":{ + "type":"structure", + "members":{ + "ServiceSoftwareOptions":{ + "shape":"ServiceSoftwareOptions", + "documentation":"

The current status of the OpenSearch service software update.

" + } + }, + "documentation":"

The result of a CancelServiceSoftwareUpdate operation. Contains the status of the update.

" + }, + "CloudWatchLogsLogGroupArn":{ + "type":"string", + "documentation":"

ARN of the Cloudwatch log group to publish logs to.

", + "max":2048, + "min":20, + "pattern":".*" + }, + "ClusterConfig":{ + "type":"structure", + "members":{ + "InstanceType":{ + "shape":"OpenSearchPartitionInstanceType", + "documentation":"

The instance type for an OpenSearch cluster. UltraWarm instance types are not supported for data instances.

" + }, + "InstanceCount":{ + "shape":"IntegerClass", + "documentation":"

The number of instances in the specified domain cluster.

" + }, + "DedicatedMasterEnabled":{ + "shape":"Boolean", + "documentation":"

A boolean value to indicate whether a dedicated master node is enabled. See Dedicated master nodes in Amazon OpenSearch Service for more information.

" + }, + "ZoneAwarenessEnabled":{ + "shape":"Boolean", + "documentation":"

A boolean value to indicate whether zone awareness is enabled. See Configuring a multi-AZ domain in Amazon OpenSearch Service for more information.

" + }, + "ZoneAwarenessConfig":{ + "shape":"ZoneAwarenessConfig", + "documentation":"

The zone awareness configuration for a domain when zone awareness is enabled.

" + }, + "DedicatedMasterType":{ + "shape":"OpenSearchPartitionInstanceType", + "documentation":"

The instance type for a dedicated master node.

" + }, + "DedicatedMasterCount":{ + "shape":"IntegerClass", + "documentation":"

Total number of dedicated master nodes, active and on standby, for the cluster.

" + }, + "WarmEnabled":{ + "shape":"Boolean", + "documentation":"

True to enable UltraWarm storage.

" + }, + "WarmType":{ + "shape":"OpenSearchWarmPartitionInstanceType", + "documentation":"

The instance type for the OpenSearch cluster's warm nodes.

" + }, + "WarmCount":{ + "shape":"IntegerClass", + "documentation":"

The number of UltraWarm nodes in the cluster.

" + }, + "ColdStorageOptions":{"shape":"ColdStorageOptions"} + }, + "documentation":"

The configuration for the domain cluster, such as the type and number of instances.

" + }, + "ClusterConfigStatus":{ + "type":"structure", + "required":[ + "Options", + "Status" + ], + "members":{ + "Options":{ + "shape":"ClusterConfig", + "documentation":"

The cluster configuration for the specified domain.

" + }, + "Status":{ + "shape":"OptionStatus", + "documentation":"

The cluster configuration status for the specified domain.

" + } + }, + "documentation":"

The configuration status for the specified domain.

" + }, + "CognitoOptions":{ + "type":"structure", + "members":{ + "Enabled":{ + "shape":"Boolean", + "documentation":"

The option to enable Cognito for OpenSearch Dashboards authentication.

" + }, + "UserPoolId":{ + "shape":"UserPoolId", + "documentation":"

The Cognito user pool ID for OpenSearch Dashboards authentication.

" + }, + "IdentityPoolId":{ + "shape":"IdentityPoolId", + "documentation":"

The Cognito identity pool ID for OpenSearch Dashboards authentication.

" + }, + "RoleArn":{ + "shape":"RoleArn", + "documentation":"

The role ARN that provides OpenSearch permissions for accessing Cognito resources.

" + } + }, + "documentation":"

Options to specify the Cognito user and identity pools for OpenSearch Dashboards authentication. For more information, see Configuring Amazon Cognito authentication for OpenSearch Dashboards.

" + }, + "CognitoOptionsStatus":{ + "type":"structure", + "required":[ + "Options", + "Status" + ], + "members":{ + "Options":{ + "shape":"CognitoOptions", + "documentation":"

Cognito options for the specified domain.

" + }, + "Status":{ + "shape":"OptionStatus", + "documentation":"

The status of the Cognito options for the specified domain.

" + } + }, + "documentation":"

The status of the Cognito options for the specified domain.

" + }, + "ColdStorageOptions":{ + "type":"structure", + "required":["Enabled"], + "members":{ + "Enabled":{"shape":"Boolean"} + } + }, + "CommitMessage":{ + "type":"string", + "max":160 + }, + "CompatibleVersionsList":{ + "type":"list", + "member":{"shape":"CompatibleVersionsMap"} + }, + "CompatibleVersionsMap":{ + "type":"structure", + "members":{ + "SourceVersion":{ + "shape":"VersionString", + "documentation":"

The current version of OpenSearch a domain is on.

" + }, + "TargetVersions":{"shape":"VersionList"} + }, + "documentation":"

A map from an EngineVersion to a list of compatible EngineVersion s to which the domain can be upgraded.

" + }, + "ConnectionAlias":{ + "type":"string", + "max":100, + "min":2, + "pattern":"[a-zA-Z][a-zA-Z0-9\\-\\_]+" + }, + "ConnectionId":{ + "type":"string", + "max":256, + "min":10, + "pattern":"[a-z][a-z0-9\\-]+" + }, + "ConnectionStatusMessage":{"type":"string"}, + "CreateDomainRequest":{ + "type":"structure", + "required":["DomainName"], + "members":{ + "DomainName":{ + "shape":"DomainName", + "documentation":"

The name of the Amazon OpenSearch Service domain you're creating. Domain names are unique across the domains owned by an account within an AWS region. Domain names must start with a lowercase letter and can contain the following characters: a-z (lowercase), 0-9, and - (hyphen).

" + }, + "EngineVersion":{ + "shape":"VersionString", + "documentation":"

String of format Elasticsearch_X.Y or OpenSearch_X.Y to specify the engine version for the Amazon OpenSearch Service domain. For example, \"OpenSearch_1.0\" or \"Elasticsearch_7.9\". For more information, see Creating and managing Amazon OpenSearch Service domains .

" + }, + "ClusterConfig":{ + "shape":"ClusterConfig", + "documentation":"

Configuration options for a domain. Specifies the instance type and number of instances in the domain.

" + }, + "EBSOptions":{ + "shape":"EBSOptions", + "documentation":"

Options to enable, disable, and specify the type and size of EBS storage volumes.

" + }, + "AccessPolicies":{ + "shape":"PolicyDocument", + "documentation":"

IAM access policy as a JSON-formatted string.

" + }, + "SnapshotOptions":{ + "shape":"SnapshotOptions", + "documentation":"

Option to set time, in UTC format, of the daily automated snapshot. Default value is 0 hours.

" + }, + "VPCOptions":{ + "shape":"VPCOptions", + "documentation":"

Options to specify the subnets and security groups for a VPC endpoint. For more information, see Launching your Amazon OpenSearch Service domains using a VPC .

" + }, + "CognitoOptions":{ + "shape":"CognitoOptions", + "documentation":"

Options to specify the Cognito user and identity pools for OpenSearch Dashboards authentication. For more information, see Configuring Amazon Cognito authentication for OpenSearch Dashboards.

" + }, + "EncryptionAtRestOptions":{ + "shape":"EncryptionAtRestOptions", + "documentation":"

Options for encryption of data at rest.

" + }, + "NodeToNodeEncryptionOptions":{ + "shape":"NodeToNodeEncryptionOptions", + "documentation":"

Node-to-node encryption options.

" + }, + "AdvancedOptions":{ + "shape":"AdvancedOptions", + "documentation":"

Option to allow references to indices in an HTTP request body. Must be false when configuring access to individual sub-resources. By default, the value is true. See Advanced cluster parameters for more information.

" + }, + "LogPublishingOptions":{ + "shape":"LogPublishingOptions", + "documentation":"

Map of LogType and LogPublishingOption, each containing options to publish a given type of OpenSearch log.

" + }, + "DomainEndpointOptions":{ + "shape":"DomainEndpointOptions", + "documentation":"

Options to specify configurations that will be applied to the domain endpoint.

" + }, + "AdvancedSecurityOptions":{ + "shape":"AdvancedSecurityOptionsInput", + "documentation":"

Specifies advanced security options.

" + }, + "TagList":{ + "shape":"TagList", + "documentation":"

A list of Tag added during domain creation.

" + }, + "AutoTuneOptions":{ + "shape":"AutoTuneOptionsInput", + "documentation":"

Specifies Auto-Tune options.

" + } + } + }, + "CreateDomainResponse":{ + "type":"structure", + "members":{ + "DomainStatus":{ + "shape":"DomainStatus", + "documentation":"

The status of the newly created domain.

" + } + }, + "documentation":"

The result of a CreateDomain operation. Contains the status of the newly created Amazon OpenSearch Service domain.

" + }, + "CreateOutboundConnectionRequest":{ + "type":"structure", + "required":[ + "LocalDomainInfo", + "RemoteDomainInfo", + "ConnectionAlias" + ], + "members":{ + "LocalDomainInfo":{ + "shape":"DomainInformationContainer", + "documentation":"

The AWSDomainInformation for the local OpenSearch domain.

" + }, + "RemoteDomainInfo":{ + "shape":"DomainInformationContainer", + "documentation":"

The AWSDomainInformation for the remote OpenSearch domain.

" + }, + "ConnectionAlias":{ + "shape":"ConnectionAlias", + "documentation":"

The connection alias used used by the customer for this cross-cluster connection.

" + } + }, + "documentation":"

Container for the parameters to the CreateOutboundConnection operation.

" + }, + "CreateOutboundConnectionResponse":{ + "type":"structure", + "members":{ + "LocalDomainInfo":{ + "shape":"DomainInformationContainer", + "documentation":"

The AWSDomainInformation for the local OpenSearch domain.

" + }, + "RemoteDomainInfo":{ + "shape":"DomainInformationContainer", + "documentation":"

The AWSDomainInformation for the remote OpenSearch domain.

" + }, + "ConnectionAlias":{ + "shape":"ConnectionAlias", + "documentation":"

The connection alias provided during the create connection request.

" + }, + "ConnectionStatus":{ + "shape":"OutboundConnectionStatus", + "documentation":"

The OutboundConnectionStatus for the newly created connection.

" + }, + "ConnectionId":{ + "shape":"ConnectionId", + "documentation":"

The unique ID for the created outbound connection, which is used for subsequent operations on the connection.

" + } + }, + "documentation":"

The result of a CreateOutboundConnection request. Contains the details about the newly created cross-cluster connection.

" + }, + "CreatePackageRequest":{ + "type":"structure", + "required":[ + "PackageName", + "PackageType", + "PackageSource" + ], + "members":{ + "PackageName":{ + "shape":"PackageName", + "documentation":"

Unique identifier for the package.

" + }, + "PackageType":{ + "shape":"PackageType", + "documentation":"

Type of package. Currently supports only TXT-DICTIONARY.

" + }, + "PackageDescription":{ + "shape":"PackageDescription", + "documentation":"

Description of the package.

" + }, + "PackageSource":{ + "shape":"PackageSource", + "documentation":"

The Amazon S3 location from which to import the package.

" + } + }, + "documentation":"

Container for request parameters to the CreatePackage operation.

" + }, + "CreatePackageResponse":{ + "type":"structure", + "members":{ + "PackageDetails":{ + "shape":"PackageDetails", + "documentation":"

Information about the package.

" + } + }, + "documentation":"

Container for the response returned by the CreatePackage operation.

" + }, + "CreatedAt":{"type":"timestamp"}, + "DeleteDomainRequest":{ + "type":"structure", + "required":["DomainName"], + "members":{ + "DomainName":{ + "shape":"DomainName", + "documentation":"

The name of the domain you want to permanently delete.

", + "location":"uri", + "locationName":"DomainName" + } + }, + "documentation":"

Container for the parameters to the DeleteDomain operation. Specifies the name of the domain you want to delete.

" + }, + "DeleteDomainResponse":{ + "type":"structure", + "members":{ + "DomainStatus":{ + "shape":"DomainStatus", + "documentation":"

The status of the domain being deleted.

" + } + }, + "documentation":"

The result of a DeleteDomain request. Contains the status of the pending deletion, or a \"domain not found\" error if the domain and all of its resources have been deleted.

" + }, + "DeleteInboundConnectionRequest":{ + "type":"structure", + "required":["ConnectionId"], + "members":{ + "ConnectionId":{ + "shape":"ConnectionId", + "documentation":"

The ID of the inbound connection to permanently delete.

", + "location":"uri", + "locationName":"ConnectionId" + } + }, + "documentation":"

Container for the parameters to the DeleteInboundConnection operation.

" + }, + "DeleteInboundConnectionResponse":{ + "type":"structure", + "members":{ + "Connection":{ + "shape":"InboundConnection", + "documentation":"

The InboundConnection of the deleted inbound connection.

" + } + }, + "documentation":"

The result of a DeleteInboundConnection operation. Contains details about the deleted inbound connection.

" + }, + "DeleteOutboundConnectionRequest":{ + "type":"structure", + "required":["ConnectionId"], + "members":{ + "ConnectionId":{ + "shape":"ConnectionId", + "documentation":"

The ID of the outbound connection you want to permanently delete.

", + "location":"uri", + "locationName":"ConnectionId" + } + }, + "documentation":"

Container for the parameters to the DeleteOutboundConnection operation.

" + }, + "DeleteOutboundConnectionResponse":{ + "type":"structure", + "members":{ + "Connection":{ + "shape":"OutboundConnection", + "documentation":"

The OutboundConnection of the deleted outbound connection.

" + } + }, + "documentation":"

The result of a DeleteOutboundConnection operation. Contains details about the deleted outbound connection.

" + }, + "DeletePackageRequest":{ + "type":"structure", + "required":["PackageID"], + "members":{ + "PackageID":{ + "shape":"PackageID", + "documentation":"

The internal ID of the package you want to delete. Use DescribePackages to find this value.

", + "location":"uri", + "locationName":"PackageID" + } + }, + "documentation":"

Container for the request parameters to the DeletePackage operation.

" + }, + "DeletePackageResponse":{ + "type":"structure", + "members":{ + "PackageDetails":{ + "shape":"PackageDetails", + "documentation":"

PackageDetails

" + } + }, + "documentation":"

Container for the response parameters to the DeletePackage operation.

" + }, + "DeploymentCloseDateTimeStamp":{"type":"timestamp"}, + "DeploymentStatus":{ + "type":"string", + "enum":[ + "PENDING_UPDATE", + "IN_PROGRESS", + "COMPLETED", + "NOT_ELIGIBLE", + "ELIGIBLE" + ] + }, + "DescribeDomainAutoTunesRequest":{ + "type":"structure", + "required":["DomainName"], + "members":{ + "DomainName":{ + "shape":"DomainName", + "documentation":"

The domain name for which you want Auto-Tune action details.

", + "location":"uri", + "locationName":"DomainName" + }, + "MaxResults":{ + "shape":"MaxResults", + "documentation":"

Set this value to limit the number of results returned. If not specified, defaults to 100.

" + }, + "NextToken":{ + "shape":"NextToken", + "documentation":"

NextToken is sent in case the earlier API call results contain the NextToken. Used for pagination.

" + } + }, + "documentation":"

Container for the parameters to the DescribeDomainAutoTunes operation.

" + }, + "DescribeDomainAutoTunesResponse":{ + "type":"structure", + "members":{ + "AutoTunes":{ + "shape":"AutoTuneList", + "documentation":"

The list of setting adjustments that Auto-Tune has made to the domain. See Auto-Tune for Amazon OpenSearch Service for more information.

" + }, + "NextToken":{ + "shape":"NextToken", + "documentation":"

An identifier to allow retrieval of paginated results.

" + } + }, + "documentation":"

The result of a DescribeDomainAutoTunes request. See Auto-Tune for Amazon OpenSearch Service for more information.

" + }, + "DescribeDomainConfigRequest":{ + "type":"structure", + "required":["DomainName"], + "members":{ + "DomainName":{ + "shape":"DomainName", + "documentation":"

The domain you want to get information about.

", + "location":"uri", + "locationName":"DomainName" + } + }, + "documentation":"

Container for the parameters to the DescribeDomainConfig operation. Specifies the domain name for which you want configuration information.

" + }, + "DescribeDomainConfigResponse":{ + "type":"structure", + "required":["DomainConfig"], + "members":{ + "DomainConfig":{ + "shape":"DomainConfig", + "documentation":"

The configuration information of the domain requested in the DescribeDomainConfig request.

" + } + }, + "documentation":"

The result of a DescribeDomainConfig request. Contains the configuration information of the requested domain.

" + }, + "DescribeDomainRequest":{ + "type":"structure", + "required":["DomainName"], + "members":{ + "DomainName":{ + "shape":"DomainName", + "documentation":"

The name of the domain for which you want information.

", + "location":"uri", + "locationName":"DomainName" + } + }, + "documentation":"

Container for the parameters to the DescribeDomain operation.

" + }, + "DescribeDomainResponse":{ + "type":"structure", + "required":["DomainStatus"], + "members":{ + "DomainStatus":{ + "shape":"DomainStatus", + "documentation":"

The current status of the domain.

" + } + }, + "documentation":"

The result of a DescribeDomain request. Contains the status of the domain specified in the request.

" + }, + "DescribeDomainsRequest":{ + "type":"structure", + "required":["DomainNames"], + "members":{ + "DomainNames":{ + "shape":"DomainNameList", + "documentation":"

The domains for which you want information.

" + } + }, + "documentation":"

Container for the parameters to the DescribeDomains operation. By default, the API returns the status of all domains.

" + }, + "DescribeDomainsResponse":{ + "type":"structure", + "required":["DomainStatusList"], + "members":{ + "DomainStatusList":{ + "shape":"DomainStatusList", + "documentation":"

The status of the domains requested in the DescribeDomains request.

" + } + }, + "documentation":"

The result of a DescribeDomains request. Contains the status of the specified domains or all domains owned by the account.

" + }, + "DescribeInboundConnectionsRequest":{ + "type":"structure", + "members":{ + "Filters":{ + "shape":"FilterList", + "documentation":"

A list of filters used to match properties for inbound cross-cluster connections. Available Filter values are:

  • connection-id
  • local-domain-info.domain-name
  • local-domain-info.owner-id
  • local-domain-info.region
  • remote-domain-info.domain-name

" + }, + "MaxResults":{ + "shape":"MaxResults", + "documentation":"

Set this value to limit the number of results returned. If not specified, defaults to 100.

" + }, + "NextToken":{ + "shape":"NextToken", + "documentation":"

If more results are available and NextToken is present, make the next request to the same API with the received NextToken to paginate the remaining results.

" + } + }, + "documentation":"

Container for the parameters to the DescribeInboundConnections operation.

" + }, + "DescribeInboundConnectionsResponse":{ + "type":"structure", + "members":{ + "Connections":{ + "shape":"InboundConnections", + "documentation":"

A list of InboundConnection matching the specified filter criteria.

" + }, + "NextToken":{ + "shape":"NextToken", + "documentation":"

If more results are available and NextToken is present, make the next request to the same API with the received NextToken to paginate the remaining results.

" + } + }, + "documentation":"

The result of a DescribeInboundConnections request. Contains a list of connections matching the filter criteria.

" + }, + "DescribeInstanceTypeLimitsRequest":{ + "type":"structure", + "required":[ + "InstanceType", + "EngineVersion" + ], + "members":{ + "DomainName":{ + "shape":"DomainName", + "documentation":"

The name of the domain you want to modify. Only include this value if you're querying OpenSearch Limits for an existing domain.

", + "location":"querystring", + "locationName":"domainName" + }, + "InstanceType":{ + "shape":"OpenSearchPartitionInstanceType", + "documentation":"

The instance type for an OpenSearch cluster for which OpenSearch Limits are needed.

", + "location":"uri", + "locationName":"InstanceType" + }, + "EngineVersion":{ + "shape":"VersionString", + "documentation":"

Version of OpenSearch for which Limits are needed.

", + "location":"uri", + "locationName":"EngineVersion" + } + }, + "documentation":"

Container for the parameters to the DescribeInstanceTypeLimits operation.

" + }, + "DescribeInstanceTypeLimitsResponse":{ + "type":"structure", + "members":{ + "LimitsByRole":{"shape":"LimitsByRole"} + }, + "documentation":"

Container for the parameters received from the DescribeInstanceTypeLimits operation.

" + }, + "DescribeOutboundConnectionsRequest":{ + "type":"structure", + "members":{ + "Filters":{ + "shape":"FilterList", + "documentation":"

A list of filters used to match properties for outbound cross-cluster connections. Available Filter names for this operation are:

  • connection-id
  • remote-domain-info.domain-name
  • remote-domain-info.owner-id
  • remote-domain-info.region
  • local-domain-info.domain-name

" + }, + "MaxResults":{ + "shape":"MaxResults", + "documentation":"

Set this value to limit the number of results returned. If not specified, defaults to 100.

" + }, + "NextToken":{ + "shape":"NextToken", + "documentation":"

NextToken is sent in case the earlier API call results contain the NextToken parameter. Used for pagination.

" + } + }, + "documentation":"

Container for the parameters to the DescribeOutboundConnections operation.

" + }, + "DescribeOutboundConnectionsResponse":{ + "type":"structure", + "members":{ + "Connections":{ + "shape":"OutboundConnections", + "documentation":"

A list of OutboundConnection matching the specified filter criteria.

" + }, + "NextToken":{ + "shape":"NextToken", + "documentation":"

If more results are available and NextToken is present, make the next request to the same API with the received NextToken to paginate the remaining results.

" + } + }, + "documentation":"

The result of a DescribeOutboundConnections request. Contains the list of connections matching the filter criteria.

" + }, + "DescribePackagesFilter":{ + "type":"structure", + "members":{ + "Name":{ + "shape":"DescribePackagesFilterName", + "documentation":"

Any field from PackageDetails.

" + }, + "Value":{ + "shape":"DescribePackagesFilterValues", + "documentation":"

A list of values for the specified field.

" + } + }, + "documentation":"

A filter to apply to the DescribePackage response.

" + }, + "DescribePackagesFilterList":{ + "type":"list", + "member":{"shape":"DescribePackagesFilter"}, + "documentation":"

A list of DescribePackagesFilter to filter the packages included in a DescribePackages response.

" + }, + "DescribePackagesFilterName":{ + "type":"string", + "enum":[ + "PackageID", + "PackageName", + "PackageStatus" + ] + }, + "DescribePackagesFilterValue":{ + "type":"string", + "pattern":"^[0-9a-zA-Z\\*\\.\\\\/\\?-]*$" + }, + "DescribePackagesFilterValues":{ + "type":"list", + "member":{"shape":"DescribePackagesFilterValue"} + }, + "DescribePackagesRequest":{ + "type":"structure", + "members":{ + "Filters":{ + "shape":"DescribePackagesFilterList", + "documentation":"

Only returns packages that match the DescribePackagesFilterList values.

" + }, + "MaxResults":{ + "shape":"MaxResults", + "documentation":"

Limits results to a maximum number of packages.

" + }, + "NextToken":{ + "shape":"NextToken", + "documentation":"

Used for pagination. Only necessary if a previous API call includes a non-null NextToken value. If provided, returns results for the next page.

" + } + }, + "documentation":"

Container for the request parameters to the DescribePackage operation.

" + }, + "DescribePackagesResponse":{ + "type":"structure", + "members":{ + "PackageDetailsList":{ + "shape":"PackageDetailsList", + "documentation":"

List of PackageDetails objects.

" + }, + "NextToken":{"shape":"String"} + }, + "documentation":"

Container for the response returned by the DescribePackages operation.

" + }, + "DescribeReservedInstanceOfferingsRequest":{ + "type":"structure", + "members":{ + "ReservedInstanceOfferingId":{ + "shape":"GUID", + "documentation":"

The offering identifier filter value. Use this parameter to show only the available offering that matches the specified reservation identifier.

", + "location":"querystring", + "locationName":"offeringId" + }, + "MaxResults":{ + "shape":"MaxResults", + "documentation":"

Set this value to limit the number of results returned. If not specified, defaults to 100.

", + "location":"querystring", + "locationName":"maxResults" + }, + "NextToken":{ + "shape":"NextToken", + "documentation":"

Provides an identifier to allow retrieval of paginated results.

", + "location":"querystring", + "locationName":"nextToken" + } + }, + "documentation":"

Container for parameters to DescribeReservedInstanceOfferings

" + }, + "DescribeReservedInstanceOfferingsResponse":{ + "type":"structure", + "members":{ + "NextToken":{ + "shape":"NextToken", + "documentation":"

Provides an identifier to allow retrieval of paginated results.

" + }, + "ReservedInstanceOfferings":{ + "shape":"ReservedInstanceOfferingList", + "documentation":"

List of reserved OpenSearch instance offerings

" + } + }, + "documentation":"

Container for results from DescribeReservedInstanceOfferings

" + }, + "DescribeReservedInstancesRequest":{ + "type":"structure", + "members":{ + "ReservedInstanceId":{ + "shape":"GUID", + "documentation":"

The reserved instance identifier filter value. Use this parameter to show only the reservation that matches the specified reserved OpenSearch instance ID.

", + "location":"querystring", + "locationName":"reservationId" + }, + "MaxResults":{ + "shape":"MaxResults", + "documentation":"

Set this value to limit the number of results returned. If not specified, defaults to 100.

", + "location":"querystring", + "locationName":"maxResults" + }, + "NextToken":{ + "shape":"NextToken", + "documentation":"

Provides an identifier to allow retrieval of paginated results.

", + "location":"querystring", + "locationName":"nextToken" + } + }, + "documentation":"

Container for parameters to DescribeReservedInstances

" + }, + "DescribeReservedInstancesResponse":{ + "type":"structure", + "members":{ + "NextToken":{ + "shape":"String", + "documentation":"

Provides an identifier to allow retrieval of paginated results.

" + }, + "ReservedInstances":{ + "shape":"ReservedInstanceList", + "documentation":"

List of reserved OpenSearch instances.

" + } + }, + "documentation":"

Container for results from DescribeReservedInstances

" + }, + "DissociatePackageRequest":{ + "type":"structure", + "required":[ + "PackageID", + "DomainName" + ], + "members":{ + "PackageID":{ + "shape":"PackageID", + "documentation":"

The internal ID of the package to associate with a domain. Use DescribePackages to find this value.

", + "location":"uri", + "locationName":"PackageID" + }, + "DomainName":{ + "shape":"DomainName", + "documentation":"

The name of the domain to associate the package with.

", + "location":"uri", + "locationName":"DomainName" + } + }, + "documentation":"

Container for the request parameters to the DissociatePackage operation.

" + }, + "DissociatePackageResponse":{ + "type":"structure", + "members":{ + "DomainPackageDetails":{ + "shape":"DomainPackageDetails", + "documentation":"

DomainPackageDetails

" + } + }, + "documentation":"

Container for the response returned by DissociatePackage operation.

" + }, + "DomainConfig":{ + "type":"structure", + "members":{ + "EngineVersion":{ + "shape":"VersionStatus", + "documentation":"

String of format Elasticsearch_X.Y or OpenSearch_X.Y to specify the engine version for the OpenSearch or Elasticsearch domain.

" + }, + "ClusterConfig":{ + "shape":"ClusterConfigStatus", + "documentation":"

The ClusterConfig for the domain.

" + }, + "EBSOptions":{ + "shape":"EBSOptionsStatus", + "documentation":"

The EBSOptions for the domain.

" + }, + "AccessPolicies":{ + "shape":"AccessPoliciesStatus", + "documentation":"

IAM access policy as a JSON-formatted string.

" + }, + "SnapshotOptions":{ + "shape":"SnapshotOptionsStatus", + "documentation":"

The SnapshotOptions for the domain.

" + }, + "VPCOptions":{ + "shape":"VPCDerivedInfoStatus", + "documentation":"

The VPCOptions for the specified domain. For more information, see Launching your Amazon OpenSearch Service domains using a VPC.

" + }, + "CognitoOptions":{ + "shape":"CognitoOptionsStatus", + "documentation":"

The CognitoOptions for the specified domain. For more information, see Configuring Amazon Cognito authentication for OpenSearch Dashboards.

" + }, + "EncryptionAtRestOptions":{ + "shape":"EncryptionAtRestOptionsStatus", + "documentation":"

The EncryptionAtRestOptions for the domain.

" + }, + "NodeToNodeEncryptionOptions":{ + "shape":"NodeToNodeEncryptionOptionsStatus", + "documentation":"

The NodeToNodeEncryptionOptions for the domain.

" + }, + "AdvancedOptions":{ + "shape":"AdvancedOptionsStatus", + "documentation":"

The AdvancedOptions for the domain. See Advanced options for more information.

" + }, + "LogPublishingOptions":{ + "shape":"LogPublishingOptionsStatus", + "documentation":"

Log publishing options for the given domain.

" + }, + "DomainEndpointOptions":{ + "shape":"DomainEndpointOptionsStatus", + "documentation":"

The DomainEndpointOptions for the domain.

" + }, + "AdvancedSecurityOptions":{ + "shape":"AdvancedSecurityOptionsStatus", + "documentation":"

Specifies AdvancedSecurityOptions for the domain.

" + }, + "AutoTuneOptions":{ + "shape":"AutoTuneOptionsStatus", + "documentation":"

Specifies AutoTuneOptions for the domain.

" + } + }, + "documentation":"

The configuration of a domain.

" + }, + "DomainEndpointOptions":{ + "type":"structure", + "members":{ + "EnforceHTTPS":{ + "shape":"Boolean", + "documentation":"

Whether only HTTPS endpoint should be enabled for the domain.

" + }, + "TLSSecurityPolicy":{ + "shape":"TLSSecurityPolicy", + "documentation":"

Specify the TLS security policy to apply to the HTTPS endpoint of the domain.
Can be one of the following values:

  • Policy-Min-TLS-1-0-2019-07: TLS security policy which supports TLSv1.0 and higher.
  • Policy-Min-TLS-1-2-2019-07: TLS security policy which supports only TLSv1.2

" + }, + "CustomEndpointEnabled":{ + "shape":"Boolean", + "documentation":"

Whether to enable a custom endpoint for the domain.

" + }, + "CustomEndpoint":{ + "shape":"DomainNameFqdn", + "documentation":"

The fully qualified domain for your custom endpoint.

" + }, + "CustomEndpointCertificateArn":{ + "shape":"ARN", + "documentation":"

The ACM certificate ARN for your custom endpoint.

" + } + }, + "documentation":"

Options to configure the endpoint for the domain.

" + }, + "DomainEndpointOptionsStatus":{ + "type":"structure", + "required":[ + "Options", + "Status" + ], + "members":{ + "Options":{ + "shape":"DomainEndpointOptions", + "documentation":"

Options to configure the endpoint for the domain.

" + }, + "Status":{ + "shape":"OptionStatus", + "documentation":"

The status of the endpoint options for the domain. See OptionStatus for the status information that's included.

" + } + }, + "documentation":"

The configured endpoint options for the domain and their current status.

" + }, + "DomainId":{ + "type":"string", + "documentation":"

Unique identifier for the domain.

", + "max":64, + "min":1 + }, + "DomainInfo":{ + "type":"structure", + "members":{ + "DomainName":{ + "shape":"DomainName", + "documentation":"

The DomainName.

" + } + } + }, + "DomainInfoList":{ + "type":"list", + "member":{"shape":"DomainInfo"}, + "documentation":"

Contains the list of domain information.

" + }, + "DomainInformationContainer":{ + "type":"structure", + "members":{ + "AWSDomainInformation":{"shape":"AWSDomainInformation"} + } + }, + "DomainName":{ + "type":"string", + "documentation":"

The name of an domain. Domain names are unique across the domains owned by an account within an AWS region. Domain names start with a letter or number and can contain the following characters: a-z (lowercase), 0-9, and - (hyphen).

", + "max":28, + "min":3, + "pattern":"[a-z][a-z0-9\\-]+" + }, + "DomainNameFqdn":{ + "type":"string", + "max":255, + "min":1, + "pattern":"^(((?!-)[A-Za-z0-9-]{0,62}[A-Za-z0-9])\\.)+((?!-)[A-Za-z0-9-]{1,62}[A-Za-z0-9])$" + }, + "DomainNameList":{ + "type":"list", + "member":{"shape":"DomainName"}, + "documentation":"

A list of domain names.

" + }, + "DomainPackageDetails":{ + "type":"structure", + "members":{ + "PackageID":{ + "shape":"PackageID", + "documentation":"

The internal ID of the package.

" + }, + "PackageName":{ + "shape":"PackageName", + "documentation":"

User-specified name of the package.

" + }, + "PackageType":{ + "shape":"PackageType", + "documentation":"

Currently supports only TXT-DICTIONARY.

" + }, + "LastUpdated":{ + "shape":"LastUpdated", + "documentation":"

The timestamp of the most recent update to the package association status.

" + }, + "DomainName":{ + "shape":"DomainName", + "documentation":"

The name of the domain you've associated a package with.

" + }, + "DomainPackageStatus":{ + "shape":"DomainPackageStatus", + "documentation":"

State of the association. Values are ASSOCIATING, ASSOCIATION_FAILED, ACTIVE, DISSOCIATING, and DISSOCIATION_FAILED.

" + }, + "PackageVersion":{"shape":"PackageVersion"}, + "ReferencePath":{ + "shape":"ReferencePath", + "documentation":"

The relative path on Amazon OpenSearch Service nodes, which can be used as synonym_path when the package is a synonym file.

" + }, + "ErrorDetails":{ + "shape":"ErrorDetails", + "documentation":"

Additional information if the package is in an error state. Null otherwise.

" + } + }, + "documentation":"

Information on a package associated with a domain.

" + }, + "DomainPackageDetailsList":{ + "type":"list", + "member":{"shape":"DomainPackageDetails"} + }, + "DomainPackageStatus":{ + "type":"string", + "enum":[ + "ASSOCIATING", + "ASSOCIATION_FAILED", + "ACTIVE", + "DISSOCIATING", + "DISSOCIATION_FAILED" + ] + }, + "DomainStatus":{ + "type":"structure", + "required":[ + "DomainId", + "DomainName", + "ARN", + "ClusterConfig" + ], + "members":{ + "DomainId":{ + "shape":"DomainId", + "documentation":"

The unique identifier for the specified domain.

" + }, + "DomainName":{ + "shape":"DomainName", + "documentation":"

The name of a domain. Domain names are unique across the domains owned by an account within an AWS region. Domain names start with a letter or number and can contain the following characters: a-z (lowercase), 0-9, and - (hyphen).

" + }, + "ARN":{ + "shape":"ARN", + "documentation":"

The Amazon Resource Name (ARN) of a domain. See IAM identifiers in the AWS Identity and Access Management User Guide for more information.

" + }, + "Created":{ + "shape":"Boolean", + "documentation":"

The domain creation status. True if the creation of a domain is complete. False if domain creation is still in progress.

" + }, + "Deleted":{ + "shape":"Boolean", + "documentation":"

The domain deletion status. True if a delete request has been received for the domain but resource cleanup is still in progress. False if the domain has not been deleted. Once domain deletion is complete, the status of the domain is no longer returned.

" + }, + "Endpoint":{ + "shape":"ServiceUrl", + "documentation":"

The domain endpoint that you use to submit index and search requests.

" + }, + "Endpoints":{ + "shape":"EndpointsMap", + "documentation":"

Map containing the domain endpoints used to submit index and search requests. Example key, value: 'vpc','vpc-endpoint-h2dsd34efgyghrtguk5gt6j2foh4.us-east-1.es.amazonaws.com'.

" + }, + "Processing":{ + "shape":"Boolean", + "documentation":"

The status of the domain configuration. True if Amazon OpenSearch Service is processing configuration changes. False if the configuration is active.

" + }, + "UpgradeProcessing":{ + "shape":"Boolean", + "documentation":"

The status of a domain version upgrade. True if Amazon OpenSearch Service is undergoing a version upgrade. False if the configuration is active.

" + }, + "EngineVersion":{"shape":"VersionString"}, + "ClusterConfig":{ + "shape":"ClusterConfig", + "documentation":"

The type and number of instances in the domain.

" + }, + "EBSOptions":{ + "shape":"EBSOptions", + "documentation":"

The EBSOptions for the specified domain.

" + }, + "AccessPolicies":{ + "shape":"PolicyDocument", + "documentation":"

IAM access policy as a JSON-formatted string.

" + }, + "SnapshotOptions":{ + "shape":"SnapshotOptions", + "documentation":"

The status of the SnapshotOptions.

" + }, + "VPCOptions":{ + "shape":"VPCDerivedInfo", + "documentation":"

The VPCOptions for the specified domain. For more information, see Launching your Amazon OpenSearch Service domains using a VPC.

" + }, + "CognitoOptions":{ + "shape":"CognitoOptions", + "documentation":"

The CognitoOptions for the specified domain. For more information, see Configuring Amazon Cognito authentication for OpenSearch Dashboards.

" + }, + "EncryptionAtRestOptions":{ + "shape":"EncryptionAtRestOptions", + "documentation":"

The status of the EncryptionAtRestOptions.

" + }, + "NodeToNodeEncryptionOptions":{ + "shape":"NodeToNodeEncryptionOptions", + "documentation":"

The status of the NodeToNodeEncryptionOptions.

" + }, + "AdvancedOptions":{ + "shape":"AdvancedOptions", + "documentation":"

The status of the AdvancedOptions.

" + }, + "LogPublishingOptions":{ + "shape":"LogPublishingOptions", + "documentation":"

Log publishing options for the given domain.

" + }, + "ServiceSoftwareOptions":{ + "shape":"ServiceSoftwareOptions", + "documentation":"

The current status of the domain's service software.

" + }, + "DomainEndpointOptions":{ + "shape":"DomainEndpointOptions", + "documentation":"

The current status of the domain's endpoint options.

" + }, + "AdvancedSecurityOptions":{ + "shape":"AdvancedSecurityOptions", + "documentation":"

The current status of the domain's advanced security options.

" + }, + "AutoTuneOptions":{ + "shape":"AutoTuneOptionsOutput", + "documentation":"

The current status of the domain's Auto-Tune options.

" + } + }, + "documentation":"

The current status of a domain.

" + }, + "DomainStatusList":{ + "type":"list", + "member":{"shape":"DomainStatus"}, + "documentation":"

A list that contains the status of each requested domain.

" + }, + "Double":{"type":"double"}, + "Duration":{ + "type":"structure", + "members":{ + "Value":{ + "shape":"DurationValue", + "documentation":"

Integer to specify the value of a maintenance schedule duration. See Auto-Tune for Amazon OpenSearch Service for more information.

" + }, + "Unit":{ + "shape":"TimeUnit", + "documentation":"

The unit of a maintenance schedule duration. Valid value is HOURS. See Auto-Tune for Amazon OpenSearch Service for more information.

" + } + }, + "documentation":"

The maintenance schedule duration: duration value and duration unit. See Auto-Tune for Amazon OpenSearch Service for more information.

" + }, + "DurationValue":{ + "type":"long", + "documentation":"

Integer to specify the value of a maintenance schedule duration. See Auto-Tune for Amazon OpenSearch Service for more information.

", + "max":24, + "min":1 + }, + "EBSOptions":{ + "type":"structure", + "members":{ + "EBSEnabled":{ + "shape":"Boolean", + "documentation":"

Whether EBS-based storage is enabled.

" + }, + "VolumeType":{ + "shape":"VolumeType", + "documentation":"

The volume type for EBS-based storage.

" + }, + "VolumeSize":{ + "shape":"IntegerClass", + "documentation":"

Integer to specify the size of an EBS volume.

" + }, + "Iops":{ + "shape":"IntegerClass", + "documentation":"

The IOPD for a Provisioned IOPS EBS volume (SSD).

" + } + }, + "documentation":"

Options to enable, disable, and specify the properties of EBS storage volumes.

" + }, + "EBSOptionsStatus":{ + "type":"structure", + "required":[ + "Options", + "Status" + ], + "members":{ + "Options":{ + "shape":"EBSOptions", + "documentation":"

The EBS options for the specified domain.

" + }, + "Status":{ + "shape":"OptionStatus", + "documentation":"

The status of the EBS options for the specified domain.

" + } + }, + "documentation":"

Status of the EBS options for the specified domain.

" + }, + "EncryptionAtRestOptions":{ + "type":"structure", + "members":{ + "Enabled":{ + "shape":"Boolean", + "documentation":"

The option to enable encryption at rest.

" + }, + "KmsKeyId":{ + "shape":"KmsKeyId", + "documentation":"

The KMS key ID for encryption at rest options.

" + } + }, + "documentation":"

Specifies encryption at rest options.

" + }, + "EncryptionAtRestOptionsStatus":{ + "type":"structure", + "required":[ + "Options", + "Status" + ], + "members":{ + "Options":{ + "shape":"EncryptionAtRestOptions", + "documentation":"

The Encryption At Rest options for the specified domain.

" + }, + "Status":{ + "shape":"OptionStatus", + "documentation":"

The status of the Encryption At Rest options for the specified domain.

" + } + }, + "documentation":"

Status of the encryption At Rest options for the specified domain.

" + }, + "EndpointsMap":{ + "type":"map", + "key":{"shape":"String"}, + "value":{"shape":"ServiceUrl"} + }, + "ErrorDetails":{ + "type":"structure", + "members":{ + "ErrorType":{"shape":"ErrorType"}, + "ErrorMessage":{"shape":"ErrorMessage"} + } + }, + "ErrorMessage":{"type":"string"}, + "ErrorType":{"type":"string"}, + "Filter":{ + "type":"structure", + "members":{ + "Name":{ + "shape":"NonEmptyString", + "documentation":"

The name of the filter.

" + }, + "Values":{ + "shape":"ValueStringList", + "documentation":"

Contains one or more values for the filter.

" + } + }, + "documentation":"

A filter used to limit results when describing inbound or outbound cross-cluster connections. Multiple values can be specified per filter. A cross-cluster connection must match at least one of the specified values for it to be returned from an operation.

" + }, + "FilterList":{ + "type":"list", + "member":{"shape":"Filter"} + }, + "GUID":{ + "type":"string", + "max":36, + "min":36, + "pattern":"\\p{XDigit}{8}-\\p{XDigit}{4}-\\p{XDigit}{4}-\\p{XDigit}{4}-\\p{XDigit}{12}" + }, + "GetCompatibleVersionsRequest":{ + "type":"structure", + "members":{ + "DomainName":{ + "shape":"DomainName", + "location":"querystring", + "locationName":"domainName" + } + }, + "documentation":"

Container for the request parameters to GetCompatibleVersions operation.

" + }, + "GetCompatibleVersionsResponse":{ + "type":"structure", + "members":{ + "CompatibleVersions":{ + "shape":"CompatibleVersionsList", + "documentation":"

A map of compatible OpenSearch versions returned as part of the GetCompatibleVersions operation.

" + } + }, + "documentation":"

Container for the response returned by the GetCompatibleVersions operation.

" + }, + "GetPackageVersionHistoryRequest":{ + "type":"structure", + "required":["PackageID"], + "members":{ + "PackageID":{ + "shape":"PackageID", + "documentation":"

Returns an audit history of package versions.

", + "location":"uri", + "locationName":"PackageID" + }, + "MaxResults":{ + "shape":"MaxResults", + "documentation":"

Limits results to a maximum number of package versions.

", + "location":"querystring", + "locationName":"maxResults" + }, + "NextToken":{ + "shape":"NextToken", + "documentation":"

Used for pagination. Only necessary if a previous API call includes a non-null NextToken value. If provided, returns results for the next page.

", + "location":"querystring", + "locationName":"nextToken" + } + }, + "documentation":"

Container for the request parameters to the GetPackageVersionHistory operation.

" + }, + "GetPackageVersionHistoryResponse":{ + "type":"structure", + "members":{ + "PackageID":{"shape":"PackageID"}, + "PackageVersionHistoryList":{ + "shape":"PackageVersionHistoryList", + "documentation":"

List of PackageVersionHistory objects.

" + }, + "NextToken":{"shape":"String"} + }, + "documentation":"

Container for response returned by GetPackageVersionHistory operation.

" + }, + "GetUpgradeHistoryRequest":{ + "type":"structure", + "required":["DomainName"], + "members":{ + "DomainName":{ + "shape":"DomainName", + "location":"uri", + "locationName":"DomainName" + }, + "MaxResults":{ + "shape":"MaxResults", + "location":"querystring", + "locationName":"maxResults" + }, + "NextToken":{ + "shape":"NextToken", + "location":"querystring", + "locationName":"nextToken" + } + }, + "documentation":"

Container for the request parameters to the GetUpgradeHistory operation.

" + }, + "GetUpgradeHistoryResponse":{ + "type":"structure", + "members":{ + "UpgradeHistories":{ + "shape":"UpgradeHistoryList", + "documentation":"

A list of UpgradeHistory objects corresponding to each upgrade or upgrade eligibility check performed on a domain returned as part of the GetUpgradeHistoryResponse object.

" + }, + "NextToken":{ + "shape":"String", + "documentation":"

Pagination token that needs to be supplied to the next call to get the next page of results.

" + } + }, + "documentation":"

Container for the response returned by the GetUpgradeHistory operation.

" + }, + "GetUpgradeStatusRequest":{ + "type":"structure", + "required":["DomainName"], + "members":{ + "DomainName":{ + "shape":"DomainName", + "location":"uri", + "locationName":"DomainName" + } + }, + "documentation":"

Container for the request parameters to the GetUpgradeStatus operation.

" + }, + "GetUpgradeStatusResponse":{ + "type":"structure", + "members":{ + "UpgradeStep":{ + "shape":"UpgradeStep", + "documentation":"

One of three steps an upgrade or upgrade eligibility check goes through:

  • PreUpgradeCheck
  • Snapshot
  • Upgrade

" + }, + "StepStatus":{ + "shape":"UpgradeStatus", + "documentation":"

One of four statuses an upgrade have, returned as part of the GetUpgradeStatusResponse object. The status can take one of the following values:

  • In Progress
  • Succeeded
  • Succeeded with Issues
  • Failed

" + }, + "UpgradeName":{ + "shape":"UpgradeName", + "documentation":"

A string that briefly describes the update.

" + } + }, + "documentation":"

Container for the response returned by the GetUpgradeStatus operation.

" + }, + "IdentityPoolId":{ + "type":"string", + "max":55, + "min":1, + "pattern":"[\\w-]+:[0-9a-f-]+" + }, + "InboundConnection":{ + "type":"structure", + "members":{ + "LocalDomainInfo":{ + "shape":"DomainInformationContainer", + "documentation":"

The AWSDomainInformation for the local OpenSearch domain.

" + }, + "RemoteDomainInfo":{ + "shape":"DomainInformationContainer", + "documentation":"

The AWSDomainInformation for the remote OpenSearch domain.

" + }, + "ConnectionId":{ + "shape":"ConnectionId", + "documentation":"

The connection ID for the inbound cross-cluster connection.

" + }, + "ConnectionStatus":{ + "shape":"InboundConnectionStatus", + "documentation":"

The InboundConnectionStatus for the outbound connection.

" + } + }, + "documentation":"

Details of an inbound connection.

" + }, + "InboundConnectionStatus":{ + "type":"structure", + "members":{ + "StatusCode":{ + "shape":"InboundConnectionStatusCode", + "documentation":"

The state code for the inbound connection. Can be one of the following:

  • PENDING_ACCEPTANCE: Inbound connection is not yet accepted by the remote domain owner.
  • APPROVED: Inbound connection is pending acceptance by the remote domain owner.
  • PROVISIONING: Inbound connection provisioning is in progress.
  • ACTIVE: Inbound connection is active and ready to use.
  • REJECTING: Inbound connection rejection is in process.
  • REJECTED: Inbound connection is rejected.
  • DELETING: Inbound connection deletion is in progress.
  • DELETED: Inbound connection is deleted and can no longer be used.
" + }, + "Message":{ + "shape":"ConnectionStatusMessage", + "documentation":"

Verbose information for the inbound connection status.

" + } + }, + "documentation":"

The connection status of an inbound cross-cluster connection.

" + }, + "InboundConnectionStatusCode":{ + "type":"string", + "enum":[ + "PENDING_ACCEPTANCE", + "APPROVED", + "PROVISIONING", + "ACTIVE", + "REJECTING", + "REJECTED", + "DELETING", + "DELETED" + ] + }, + "InboundConnections":{ + "type":"list", + "member":{"shape":"InboundConnection"} + }, + "InstanceCount":{ + "type":"integer", + "documentation":"

The number of EC2 instances in the domain.

", + "min":1 + }, + "InstanceCountLimits":{ + "type":"structure", + "members":{ + "MinimumInstanceCount":{"shape":"MinimumInstanceCount"}, + "MaximumInstanceCount":{"shape":"MaximumInstanceCount"} + }, + "documentation":"

InstanceCountLimits represents the limits on the number of instances that can be created in Amazon OpenSearch Service for a given InstanceType.

" + }, + "InstanceLimits":{ + "type":"structure", + "members":{ + "InstanceCountLimits":{"shape":"InstanceCountLimits"} + }, + "documentation":"

InstanceLimits represents the list of instance-related attributes that are available for a given InstanceType.

" + }, + "InstanceRole":{"type":"string"}, + "InstanceRoleList":{ + "type":"list", + "member":{"shape":"InstanceRole"} + }, + "InstanceTypeDetails":{ + "type":"structure", + "members":{ + "InstanceType":{"shape":"OpenSearchPartitionInstanceType"}, + "EncryptionEnabled":{"shape":"Boolean"}, + "CognitoEnabled":{"shape":"Boolean"}, + "AppLogsEnabled":{"shape":"Boolean"}, + "AdvancedSecurityEnabled":{"shape":"Boolean"}, + "WarmEnabled":{"shape":"Boolean"}, + "InstanceRole":{"shape":"InstanceRoleList"} + } + }, + "InstanceTypeDetailsList":{ + "type":"list", + "member":{"shape":"InstanceTypeDetails"} + }, + "Integer":{"type":"integer"}, + "IntegerClass":{"type":"integer"}, + "Issue":{"type":"string"}, + "Issues":{ + "type":"list", + "member":{"shape":"Issue"} + }, + "KmsKeyId":{ + "type":"string", + "max":500, + "min":1, + "pattern":".*" + }, + "LastUpdated":{"type":"timestamp"}, + "LimitName":{"type":"string"}, + "LimitValue":{"type":"string"}, + "LimitValueList":{ + "type":"list", + "member":{"shape":"LimitValue"} + }, + "Limits":{ + "type":"structure", + "members":{ + "StorageTypes":{ + "shape":"StorageTypeList", + "documentation":"

Storage-related types and attributes that are available for a given InstanceType.

" + }, + "InstanceLimits":{"shape":"InstanceLimits"}, + "AdditionalLimits":{ + "shape":"AdditionalLimitList", + "documentation":"

List of additional limits that are specific to a given InstanceType and for each of its InstanceRole .

" + } + }, + "documentation":"

Limits for a given InstanceType and for each of its roles.
Limits contains the following: StorageTypes , InstanceLimits , and AdditionalLimits

" + }, + "LimitsByRole":{ + "type":"map", + "key":{"shape":"InstanceRole"}, + "value":{"shape":"Limits"}, + "documentation":"

The role of a given instance and all applicable limits. The role performed by a given OpenSearch instance can be one of the following:

  • data: If the given InstanceType is used as a data node
  • master: If the given InstanceType is used as a master node
  • ultra_warm: If the given InstanceType is used as a warm node

" + }, + "ListDomainNamesResponse":{ + "type":"structure", + "members":{ + "DomainNames":{ + "shape":"DomainInfoList", + "documentation":"

List of domain names.

" + } + }, + "documentation":"

The result of a ListDomainNames operation. Contains the names of all domains owned by this account.

" + }, + "ListDomainsForPackageRequest":{ + "type":"structure", + "required":["PackageID"], + "members":{ + "PackageID":{ + "shape":"PackageID", + "documentation":"

The package for which to list associated domains.

", + "location":"uri", + "locationName":"PackageID" + }, + "MaxResults":{ + "shape":"MaxResults", + "documentation":"

Limits the results to a maximum number of domains.

", + "location":"querystring", + "locationName":"maxResults" + }, + "NextToken":{ + "shape":"NextToken", + "documentation":"

Used for pagination. Only necessary if a previous API call includes a non-null NextToken value. If provided, returns results for the next page.

", + "location":"querystring", + "locationName":"nextToken" + } + }, + "documentation":"

Container for the request parameters to the ListDomainsForPackage operation.

" + }, + "ListDomainsForPackageResponse":{ + "type":"structure", + "members":{ + "DomainPackageDetailsList":{ + "shape":"DomainPackageDetailsList", + "documentation":"

List of DomainPackageDetails objects.

" + }, + "NextToken":{"shape":"String"} + }, + "documentation":"

Container for the response parameters to the ListDomainsForPackage operation.

" + }, + "ListInstanceTypeDetailsRequest":{ + "type":"structure", + "required":["EngineVersion"], + "members":{ + "EngineVersion":{ + "shape":"VersionString", + "location":"uri", + "locationName":"EngineVersion" + }, + "DomainName":{ + "shape":"DomainName", + "location":"querystring", + "locationName":"domainName" + }, + "MaxResults":{ + "shape":"MaxResults", + "location":"querystring", + "locationName":"maxResults" + }, + "NextToken":{ + "shape":"NextToken", + "location":"querystring", + "locationName":"nextToken" + } + } + }, + "ListInstanceTypeDetailsResponse":{ + "type":"structure", + "members":{ + "InstanceTypeDetails":{"shape":"InstanceTypeDetailsList"}, + "NextToken":{"shape":"NextToken"} + } + }, + "ListPackagesForDomainRequest":{ + "type":"structure", + "required":["DomainName"], + "members":{ + "DomainName":{ + "shape":"DomainName", + "documentation":"

The name of the domain for which you want to list associated packages.

", + "location":"uri", + "locationName":"DomainName" + }, + "MaxResults":{ + "shape":"MaxResults", + "documentation":"

Limits results to a maximum number of packages.

", + "location":"querystring", + "locationName":"maxResults" + }, + "NextToken":{ + "shape":"NextToken", + "documentation":"

Used for pagination. Only necessary if a previous API call includes a non-null NextToken value. If provided, returns results for the next page.

", + "location":"querystring", + "locationName":"nextToken" + } + }, + "documentation":"

Container for the request parameters to the ListPackagesForDomain operation.

" + }, + "ListPackagesForDomainResponse":{ + "type":"structure", + "members":{ + "DomainPackageDetailsList":{ + "shape":"DomainPackageDetailsList", + "documentation":"

List of DomainPackageDetails objects.

" + }, + "NextToken":{ + "shape":"String", + "documentation":"

Pagination token to supply to the next call to get the next page of results.

" + } + }, + "documentation":"

Container for the response parameters to the ListPackagesForDomain operation.

" + }, + "ListTagsRequest":{ + "type":"structure", + "required":["ARN"], + "members":{ + "ARN":{ + "shape":"ARN", + "documentation":"

Specify the ARN of the domain that the tags you want to view are attached to.

", + "location":"querystring", + "locationName":"arn" + } + }, + "documentation":"

Container for the parameters to the ListTags operation. Specify the ARN of the domain that the tags you want to view are attached to.

" + }, + "ListTagsResponse":{ + "type":"structure", + "members":{ + "TagList":{ + "shape":"TagList", + "documentation":"

List of Tag for the requested domain.

" + } + }, + "documentation":"

The result of a ListTags operation. Contains tags for all requested domains.

" + }, + "ListVersionsRequest":{ + "type":"structure", + "members":{ + "MaxResults":{ + "shape":"MaxResults", + "documentation":"

Set this value to limit the number of results returned. Value must be greater than 10 or it won't be honored.

", + "location":"querystring", + "locationName":"maxResults" + }, + "NextToken":{ + "shape":"NextToken", + "location":"querystring", + "locationName":"nextToken" + } + }, + "documentation":"

Container for the parameters to the ListVersions operation.

Use MaxResults to control the maximum number of results to retrieve in a single call.

Use NextToken in response to retrieve more results. If the received response does not contain a NextToken, there are no more results to retrieve.

" + }, + "ListVersionsResponse":{ + "type":"structure", + "members":{ + "Versions":{"shape":"VersionList"}, + "NextToken":{"shape":"NextToken"} + }, + "documentation":"

Container for the parameters for response received from the ListVersions operation.

" + }, + "LogPublishingOption":{ + "type":"structure", + "members":{ + "CloudWatchLogsLogGroupArn":{"shape":"CloudWatchLogsLogGroupArn"}, + "Enabled":{ + "shape":"Boolean", + "documentation":"

Whether the given log publishing option is enabled or not.

" + } + }, + "documentation":"

Log Publishing option that is set for a given domain.
Attributes and their details:

  • CloudWatchLogsLogGroupArn: ARN of the Cloudwatch log group to publish logs to.
  • Enabled: Whether the log publishing for a given log type is enabled or not.

" + }, + "LogPublishingOptions":{ + "type":"map", + "key":{"shape":"LogType"}, + "value":{"shape":"LogPublishingOption"} + }, + "LogPublishingOptionsStatus":{ + "type":"structure", + "members":{ + "Options":{ + "shape":"LogPublishingOptions", + "documentation":"

The log publishing options configured for the domain.

" + }, + "Status":{ + "shape":"OptionStatus", + "documentation":"

The status of the log publishing options for the domain. See OptionStatus for the status information that's included.

" + } + }, + "documentation":"

The configured log publishing options for the domain and their current status.

" + }, + "LogType":{ + "type":"string", + "documentation":"

Type of log file. Can be one of the following:

  • INDEX_SLOW_LOGS: Index slow logs contain insert requests that took more time than configured index query log threshold to execute.
  • SEARCH_SLOW_LOGS: Search slow logs contain search queries that took more time than configured search query log threshold to execute.
  • ES_APPLICATION_LOGS: OpenSearch application logs contain information about errors and warnings raised during the operation of the service and can be useful for troubleshooting.
  • AUDIT_LOGS: Audit logs contain records of user requests for access from the domain.

", + "enum":[ + "INDEX_SLOW_LOGS", + "SEARCH_SLOW_LOGS", + "ES_APPLICATION_LOGS", + "AUDIT_LOGS" + ] + }, + "Long":{"type":"long"}, + "MasterUserOptions":{ + "type":"structure", + "members":{ + "MasterUserARN":{ + "shape":"ARN", + "documentation":"

ARN for the master user (if IAM is enabled).

" + }, + "MasterUserName":{ + "shape":"Username", + "documentation":"

The master user's username, which is stored in the Amazon OpenSearch Service domain's internal database.

" + }, + "MasterUserPassword":{ + "shape":"Password", + "documentation":"

The master user's password, which is stored in the Amazon OpenSearch Service domain's internal database.

" + } + }, + "documentation":"

Credentials for the master user: username and password, ARN, or both.

" + }, + "MaxResults":{ + "type":"integer", + "documentation":"

Set this value to limit the number of results returned.

", + "max":100 + }, + "MaximumInstanceCount":{ + "type":"integer", + "documentation":"

Maximum number of instances that can be instantiated for a given InstanceType.

" + }, + "MinimumInstanceCount":{ + "type":"integer", + "documentation":"

Minimum number of instances that can be instantiated for a given InstanceType.

" + }, + "NextToken":{ + "type":"string", + "documentation":"

Paginated APIs accept the NextToken input to return the next page of results and provide a NextToken output in the response, which you can use to retrieve more results.

" + }, + "NodeToNodeEncryptionOptions":{ + "type":"structure", + "members":{ + "Enabled":{ + "shape":"Boolean", + "documentation":"

True to enable node-to-node encryption.

" + } + }, + "documentation":"

The node-to-node encryption options.

" + }, + "NodeToNodeEncryptionOptionsStatus":{ + "type":"structure", + "required":[ + "Options", + "Status" + ], + "members":{ + "Options":{ + "shape":"NodeToNodeEncryptionOptions", + "documentation":"

The node-to-node encryption options for the specified domain.

" + }, + "Status":{ + "shape":"OptionStatus", + "documentation":"

The status of the node-to-node encryption options for the specified domain.

" + } + }, + "documentation":"

Status of the node-to-node encryption options for the specified domain.

" + }, + "NonEmptyString":{ + "type":"string", + "max":100, + "min":1, + "pattern":"[a-zA-Z0-9\\-\\_\\.]+" + }, + "OpenSearchPartitionInstanceType":{ + "type":"string", + "enum":[ + "m3.medium.search", + "m3.large.search", + "m3.xlarge.search", + "m3.2xlarge.search", + "m4.large.search", + "m4.xlarge.search", + "m4.2xlarge.search", + "m4.4xlarge.search", + "m4.10xlarge.search", + "m5.large.search", + "m5.xlarge.search", + "m5.2xlarge.search", + "m5.4xlarge.search", + "m5.12xlarge.search", + "m5.24xlarge.search", + "r5.large.search", + "r5.xlarge.search", + "r5.2xlarge.search", + "r5.4xlarge.search", + "r5.12xlarge.search", + "r5.24xlarge.search", + "c5.large.search", + "c5.xlarge.search", + "c5.2xlarge.search", + "c5.4xlarge.search", + "c5.9xlarge.search", + "c5.18xlarge.search", + "t3.nano.search", + "t3.micro.search", + "t3.small.search", + "t3.medium.search", + "t3.large.search", + "t3.xlarge.search", + "t3.2xlarge.search", + "ultrawarm1.medium.search", + "ultrawarm1.large.search", + "ultrawarm1.xlarge.search", + "t2.micro.search", + "t2.small.search", + "t2.medium.search", + "r3.large.search", + "r3.xlarge.search", + "r3.2xlarge.search", + "r3.4xlarge.search", + "r3.8xlarge.search", + "i2.xlarge.search", + "i2.2xlarge.search", + "d2.xlarge.search", + "d2.2xlarge.search", + "d2.4xlarge.search", + "d2.8xlarge.search", + "c4.large.search", + "c4.xlarge.search", + "c4.2xlarge.search", + "c4.4xlarge.search", + "c4.8xlarge.search", + "r4.large.search", + "r4.xlarge.search", + "r4.2xlarge.search", + "r4.4xlarge.search", + "r4.8xlarge.search", + "r4.16xlarge.search", + "i3.large.search", + "i3.xlarge.search", + "i3.2xlarge.search", + "i3.4xlarge.search", + "i3.8xlarge.search", + "i3.16xlarge.search", + "r6g.large.search", + "r6g.xlarge.search", + "r6g.2xlarge.search", + "r6g.4xlarge.search", + "r6g.8xlarge.search", + "r6g.12xlarge.search", + "m6g.large.search", + "m6g.xlarge.search", + "m6g.2xlarge.search", + "m6g.4xlarge.search", + "m6g.8xlarge.search", + "m6g.12xlarge.search", + "c6g.large.search", + "c6g.xlarge.search", + "c6g.2xlarge.search", + "c6g.4xlarge.search", + "c6g.8xlarge.search", + "c6g.12xlarge.search", + "r6gd.large.search", + "r6gd.xlarge.search", + "r6gd.2xlarge.search", + "r6gd.4xlarge.search", + "r6gd.8xlarge.search", + "r6gd.12xlarge.search", + "r6gd.16xlarge.search", + "t4g.small.search", + "t4g.medium.search" + ] + }, + "OpenSearchWarmPartitionInstanceType":{ + "type":"string", + "enum":[ + "ultrawarm1.medium.search", + "ultrawarm1.large.search", + "ultrawarm1.xlarge.search" + ] + }, + "OptionState":{ + "type":"string", + "documentation":"

The state of a requested change. One of the following:

  • Processing: The request change is still in progress.
  • Active: The request change is processed and deployed to the domain.
", + "enum":[ + "RequiresIndexDocuments", + "Processing", + "Active" + ] + }, + "OptionStatus":{ + "type":"structure", + "required":[ + "CreationDate", + "UpdateDate", + "State" + ], + "members":{ + "CreationDate":{ + "shape":"UpdateTimestamp", + "documentation":"

The timestamp of when the entity was created.

" + }, + "UpdateDate":{ + "shape":"UpdateTimestamp", + "documentation":"

The timestamp of the last time the entity was updated.

" + }, + "UpdateVersion":{ + "shape":"UIntValue", + "documentation":"

The latest version of the entity.

" + }, + "State":{ + "shape":"OptionState", + "documentation":"

Provides the OptionState for the domain.

" + }, + "PendingDeletion":{ + "shape":"Boolean", + "documentation":"

Indicates whether the domain is being deleted.

" + } + }, + "documentation":"

Provides the current status of the entity.

" + }, + "OutboundConnection":{ + "type":"structure", + "members":{ + "LocalDomainInfo":{ + "shape":"DomainInformationContainer", + "documentation":"

The DomainInformation for the local OpenSearch domain.

" + }, + "RemoteDomainInfo":{ + "shape":"DomainInformationContainer", + "documentation":"

The DomainInformation for the remote OpenSearch domain.

" + }, + "ConnectionId":{ + "shape":"ConnectionId", + "documentation":"

The connection ID for the outbound cross-cluster connection.

" + }, + "ConnectionAlias":{ + "shape":"ConnectionAlias", + "documentation":"

The connection alias for the outbound cross-cluster connection.

" + }, + "ConnectionStatus":{ + "shape":"OutboundConnectionStatus", + "documentation":"

The OutboundConnectionStatus for the outbound connection.

" + } + }, + "documentation":"

Specifies details about an outbound connection.

" + }, + "OutboundConnectionStatus":{ + "type":"structure", + "members":{ + "StatusCode":{ + "shape":"OutboundConnectionStatusCode", + "documentation":"

The state code for the outbound connection. Can be one of the following:

  • VALIDATING: The outbound connection request is being validated.
  • VALIDATION_FAILED: Validation failed for the connection request.
  • PENDING_ACCEPTANCE: Outbound connection request is validated and is not yet accepted by the remote domain owner.
  • APPROVED: Outbound connection has been approved by the remote domain owner for getting provisioned.
  • PROVISIONING: Outbound connection request is in process.
  • ACTIVE: Outbound connection is active and ready to use.
  • REJECTING: Outbound connection rejection by remote domain owner is in progress.
  • REJECTED: Outbound connection request is rejected by remote domain owner.
  • DELETING: Outbound connection deletion is in progress.
  • DELETED: Outbound connection is deleted and can no longer be used.
" + }, + "Message":{ + "shape":"ConnectionStatusMessage", + "documentation":"

Verbose information for the outbound connection status.

" + } + }, + "documentation":"

The connection status of an outbound cross-cluster connection.

" + }, + "OutboundConnectionStatusCode":{ + "type":"string", + "enum":[ + "VALIDATING", + "VALIDATION_FAILED", + "PENDING_ACCEPTANCE", + "APPROVED", + "PROVISIONING", + "ACTIVE", + "REJECTING", + "REJECTED", + "DELETING", + "DELETED" + ] + }, + "OutboundConnections":{ + "type":"list", + "member":{"shape":"OutboundConnection"} + }, + "OwnerId":{ + "type":"string", + "max":12, + "min":12, + "pattern":"[0-9]+" + }, + "PackageDescription":{ + "type":"string", + "max":1024 + }, + "PackageDetails":{ + "type":"structure", + "members":{ + "PackageID":{ + "shape":"PackageID", + "documentation":"

Internal ID of the package.

" + }, + "PackageName":{ + "shape":"PackageName", + "documentation":"

User-specified name of the package.

" + }, + "PackageType":{ + "shape":"PackageType", + "documentation":"

Currently supports only TXT-DICTIONARY.

" + }, + "PackageDescription":{ + "shape":"PackageDescription", + "documentation":"

User-specified description of the package.

" + }, + "PackageStatus":{ + "shape":"PackageStatus", + "documentation":"

Current state of the package. Values are COPYING, COPY_FAILED, AVAILABLE, DELETING, and DELETE_FAILED.

" + }, + "CreatedAt":{ + "shape":"CreatedAt", + "documentation":"

The timestamp of when the package was created.

" + }, + "LastUpdatedAt":{"shape":"LastUpdated"}, + "AvailablePackageVersion":{"shape":"PackageVersion"}, + "ErrorDetails":{ + "shape":"ErrorDetails", + "documentation":"

Additional information if the package is in an error state. Null otherwise.

" + } + }, + "documentation":"

Basic information about a package.

" + }, + "PackageDetailsList":{ + "type":"list", + "member":{"shape":"PackageDetails"} + }, + "PackageID":{"type":"string"}, + "PackageName":{ + "type":"string", + "max":28, + "min":3, + "pattern":"[a-z][a-z0-9\\-]+" + }, + "PackageSource":{ + "type":"structure", + "members":{ + "S3BucketName":{ + "shape":"S3BucketName", + "documentation":"

The name of the Amazon S3 bucket containing the package.

" + }, + "S3Key":{ + "shape":"S3Key", + "documentation":"

Key (file name) of the package.

" + } + }, + "documentation":"

The Amazon S3 location for importing the package specified as S3BucketName and S3Key

" + }, + "PackageStatus":{ + "type":"string", + "enum":[ + "COPYING", + "COPY_FAILED", + "VALIDATING", + "VALIDATION_FAILED", + "AVAILABLE", + "DELETING", + "DELETED", + "DELETE_FAILED" + ] + }, + "PackageType":{ + "type":"string", + "enum":["TXT-DICTIONARY"] + }, + "PackageVersion":{"type":"string"}, + "PackageVersionHistory":{ + "type":"structure", + "members":{ + "PackageVersion":{ + "shape":"PackageVersion", + "documentation":"

The package version.

" + }, + "CommitMessage":{ + "shape":"CommitMessage", + "documentation":"

A message associated with the package version.

" + }, + "CreatedAt":{ + "shape":"CreatedAt", + "documentation":"

The timestamp of when the package was created.

" + } + }, + "documentation":"

Details of a package version.

" + }, + "PackageVersionHistoryList":{ + "type":"list", + "member":{"shape":"PackageVersionHistory"} + }, + "Password":{ + "type":"string", + "max":128, + "min":8, + "pattern":".*", + "sensitive":true + }, + "PolicyDocument":{ + "type":"string", + "documentation":"

Access policy rules for a domain service endpoints. For more information, see Configuring access policies . The maximum size of a policy document is 100 KB.

", + "max":102400, + "min":0, + "pattern":".*" + }, + "PurchaseReservedInstanceOfferingRequest":{ + "type":"structure", + "required":[ + "ReservedInstanceOfferingId", + "ReservationName" + ], + "members":{ + "ReservedInstanceOfferingId":{ + "shape":"GUID", + "documentation":"

The ID of the reserved OpenSearch instance offering to purchase.

" + }, + "ReservationName":{ + "shape":"ReservationToken", + "documentation":"

A customer-specified identifier to track this reservation.

" + }, + "InstanceCount":{ + "shape":"InstanceCount", + "documentation":"

The number of OpenSearch instances to reserve.

" + } + }, + "documentation":"

Container for parameters to PurchaseReservedInstanceOffering

" + }, + "PurchaseReservedInstanceOfferingResponse":{ + "type":"structure", + "members":{ + "ReservedInstanceId":{ + "shape":"GUID", + "documentation":"

Details of the reserved OpenSearch instance which was purchased.

" + }, + "ReservationName":{ + "shape":"ReservationToken", + "documentation":"

The customer-specified identifier used to track this reservation.

" + } + }, + "documentation":"

Represents the output of a PurchaseReservedInstanceOffering operation.

" + }, + "RecurringCharge":{ + "type":"structure", + "members":{ + "RecurringChargeAmount":{ + "shape":"Double", + "documentation":"

The monetary amount of the recurring charge.

" + }, + "RecurringChargeFrequency":{ + "shape":"String", + "documentation":"

The frequency of the recurring charge.

" + } + }, + "documentation":"

Contains the specific price and frequency of a recurring charges for a reserved OpenSearch instance, or for a reserved OpenSearch instance offering.

" + }, + "RecurringChargeList":{ + "type":"list", + "member":{"shape":"RecurringCharge"} + }, + "ReferencePath":{"type":"string"}, + "Region":{ + "type":"string", + "max":30, + "min":5, + "pattern":"[a-z][a-z0-9\\-]+" + }, + "RejectInboundConnectionRequest":{ + "type":"structure", + "required":["ConnectionId"], + "members":{ + "ConnectionId":{ + "shape":"ConnectionId", + "documentation":"

The ID of the inbound connection to reject.

", + "location":"uri", + "locationName":"ConnectionId" + } + }, + "documentation":"

Container for the parameters to the RejectInboundConnection operation.

" + }, + "RejectInboundConnectionResponse":{ + "type":"structure", + "members":{ + "Connection":{ + "shape":"InboundConnection", + "documentation":"

The InboundConnection of the rejected inbound connection.

" + } + }, + "documentation":"

The result of a RejectInboundConnection operation. Contains details about the rejected inbound connection.

" + }, + "RemoveTagsRequest":{ + "type":"structure", + "required":[ + "ARN", + "TagKeys" + ], + "members":{ + "ARN":{ + "shape":"ARN", + "documentation":"

The ARN of the domain from which you want to delete the specified tags.

" + }, + "TagKeys":{ + "shape":"StringList", + "documentation":"

The TagKey list you want to remove from the domain.

" + } + }, + "documentation":"

Container for the parameters to the RemoveTags operation. Specify the ARN for the domain from which you want to remove the specified TagKey.

" + }, + "ReservationToken":{ + "type":"string", + "max":64, + "min":5, + "pattern":".*" + }, + "ReservedInstance":{ + "type":"structure", + "members":{ + "ReservationName":{ + "shape":"ReservationToken", + "documentation":"

The customer-specified identifier to track this reservation.

" + }, + "ReservedInstanceId":{ + "shape":"GUID", + "documentation":"

The unique identifier for the reservation.

" + }, + "BillingSubscriptionId":{"shape":"Long"}, + "ReservedInstanceOfferingId":{ + "shape":"String", + "documentation":"

The offering identifier.

" + }, + "InstanceType":{ + "shape":"OpenSearchPartitionInstanceType", + "documentation":"

The OpenSearch instance type offered by the reserved instance offering.

" + }, + "StartTime":{ + "shape":"UpdateTimestamp", + "documentation":"

The time the reservation started.

" + }, + "Duration":{ + "shape":"Integer", + "documentation":"

The duration, in seconds, for which the OpenSearch instance is reserved.

" + }, + "FixedPrice":{ + "shape":"Double", + "documentation":"

The upfront fixed charge you will paid to purchase the specific reserved OpenSearch instance offering.

" + }, + "UsagePrice":{ + "shape":"Double", + "documentation":"

The rate you are charged for each hour for the domain that is using this reserved instance.

" + }, + "CurrencyCode":{ + "shape":"String", + "documentation":"

The currency code for the reserved OpenSearch instance offering.

" + }, + "InstanceCount":{ + "shape":"Integer", + "documentation":"

The number of OpenSearch instances that have been reserved.

" + }, + "State":{ + "shape":"String", + "documentation":"

The state of the reserved OpenSearch instance.

" + }, + "PaymentOption":{ + "shape":"ReservedInstancePaymentOption", + "documentation":"

The payment option as defined in the reserved OpenSearch instance offering.

" + }, + "RecurringCharges":{ + "shape":"RecurringChargeList", + "documentation":"

The charge to your account regardless of whether you are creating any domains using the instance offering.

" + } + }, + "documentation":"

Details of a reserved OpenSearch instance.

" + }, + "ReservedInstanceList":{ + "type":"list", + "member":{"shape":"ReservedInstance"} + }, + "ReservedInstanceOffering":{ + "type":"structure", + "members":{ + "ReservedInstanceOfferingId":{ + "shape":"GUID", + "documentation":"

The OpenSearch reserved instance offering identifier.

" + }, + "InstanceType":{ + "shape":"OpenSearchPartitionInstanceType", + "documentation":"

The OpenSearch instance type offered by the reserved instance offering.

" + }, + "Duration":{ + "shape":"Integer", + "documentation":"

The duration, in seconds, for which the offering will reserve the OpenSearch instance.

" + }, + "FixedPrice":{ + "shape":"Double", + "documentation":"

The upfront fixed charge you will pay to purchase the specific reserved OpenSearch instance offering.

" + }, + "UsagePrice":{ + "shape":"Double", + "documentation":"

The rate you are charged for each hour the domain that is using the offering is running.

" + }, + "CurrencyCode":{ + "shape":"String", + "documentation":"

The currency code for the reserved OpenSearch instance offering.

" + }, + "PaymentOption":{ + "shape":"ReservedInstancePaymentOption", + "documentation":"

Payment option for the reserved OpenSearch instance offering

" + }, + "RecurringCharges":{ + "shape":"RecurringChargeList", + "documentation":"

The charge to your account regardless of whether you are creating any domains using the instance offering.

" + } + }, + "documentation":"

Details of a reserved OpenSearch instance offering.

" + }, + "ReservedInstanceOfferingList":{ + "type":"list", + "member":{"shape":"ReservedInstanceOffering"} + }, + "ReservedInstancePaymentOption":{ + "type":"string", + "enum":[ + "ALL_UPFRONT", + "PARTIAL_UPFRONT", + "NO_UPFRONT" + ] + }, + "RoleArn":{ + "type":"string", + "max":2048, + "min":20, + "pattern":"arn:(aws|aws\\-cn|aws\\-us\\-gov|aws\\-iso|aws\\-iso\\-b):iam::[0-9]+:role\\/.*" + }, + "RollbackOnDisable":{ + "type":"string", + "documentation":"

The rollback state while disabling Auto-Tune for the domain. Valid values are NO_ROLLBACK and DEFAULT_ROLLBACK.

", + "enum":[ + "NO_ROLLBACK", + "DEFAULT_ROLLBACK" + ] + }, + "S3BucketName":{ + "type":"string", + "max":63, + "min":3 + }, + "S3Key":{ + "type":"string", + "max":1024, + "min":1 + }, + "SAMLEntityId":{ + "type":"string", + "max":512, + "min":8 + }, + "SAMLIdp":{ + "type":"structure", + "required":[ + "MetadataContent", + "EntityId" + ], + "members":{ + "MetadataContent":{ + "shape":"SAMLMetadata", + "documentation":"

The metadata of the SAML application in XML format.

" + }, + "EntityId":{ + "shape":"SAMLEntityId", + "documentation":"

The unique entity ID of the application in SAML identity provider.

" + } + }, + "documentation":"

The SAML identity povider's information.

" + }, + "SAMLMetadata":{ + "type":"string", + "max":1048576, + "min":1 + }, + "SAMLOptionsInput":{ + "type":"structure", + "members":{ + "Enabled":{ + "shape":"Boolean", + "documentation":"

True if SAML is enabled.

" + }, + "Idp":{ + "shape":"SAMLIdp", + "documentation":"

The SAML Identity Provider's information.

" + }, + "MasterUserName":{ + "shape":"Username", + "documentation":"

The SAML master username, which is stored in the Amazon OpenSearch Service domain's internal database.

" + }, + "MasterBackendRole":{ + "shape":"BackendRole", + "documentation":"

The backend role that the SAML master user is mapped to.

" + }, + "SubjectKey":{ + "shape":"String", + "documentation":"

Element of the SAML assertion to use for username. Default is NameID.

" + }, + "RolesKey":{ + "shape":"String", + "documentation":"

Element of the SAML assertion to use for backend roles. Default is roles.

" + }, + "SessionTimeoutMinutes":{ + "shape":"IntegerClass", + "documentation":"

The duration, in minutes, after which a user session becomes inactive. Acceptable values are between 1 and 1440, and the default value is 60.

" + } + }, + "documentation":"

The SAML application configuration for the domain.

" + }, + "SAMLOptionsOutput":{ + "type":"structure", + "members":{ + "Enabled":{ + "shape":"Boolean", + "documentation":"

True if SAML is enabled.

" + }, + "Idp":{ + "shape":"SAMLIdp", + "documentation":"

Describes the SAML identity provider's information.

" + }, + "SubjectKey":{ + "shape":"String", + "documentation":"

The key used for matching the SAML subject attribute.

" + }, + "RolesKey":{ + "shape":"String", + "documentation":"

The key used for matching the SAML roles attribute.

" + }, + "SessionTimeoutMinutes":{ + "shape":"IntegerClass", + "documentation":"

The duration, in minutes, after which a user session becomes inactive.

" + } + }, + "documentation":"

Describes the SAML application configured for the domain.

" + }, + "ScheduledAutoTuneActionType":{ + "type":"string", + "documentation":"

The Auto-Tune action type. Valid values are JVM_HEAP_SIZE_TUNING, and JVM_YOUNG_GEN_TUNING.

", + "enum":[ + "JVM_HEAP_SIZE_TUNING", + "JVM_YOUNG_GEN_TUNING" + ] + }, + "ScheduledAutoTuneDescription":{ + "type":"string", + "documentation":"

The Auto-Tune action description.

" + }, + "ScheduledAutoTuneDetails":{ + "type":"structure", + "members":{ + "Date":{ + "shape":"AutoTuneDate", + "documentation":"

The timestamp of the Auto-Tune action scheduled for the domain.

" + }, + "ActionType":{ + "shape":"ScheduledAutoTuneActionType", + "documentation":"

The Auto-Tune action type. Valid values are JVM_HEAP_SIZE_TUNING and JVM_YOUNG_GEN_TUNING.

" + }, + "Action":{ + "shape":"ScheduledAutoTuneDescription", + "documentation":"

The Auto-Tune action description.

" + }, + "Severity":{ + "shape":"ScheduledAutoTuneSeverityType", + "documentation":"

The Auto-Tune action severity. Valid values are LOW, MEDIUM, and HIGH.

" + } + }, + "documentation":"

Specifies details about the scheduled Auto-Tune action. See Auto-Tune for Amazon OpenSearch Service for more information.

" + }, + "ScheduledAutoTuneSeverityType":{ + "type":"string", + "documentation":"

The Auto-Tune action severity. Valid values are LOW, MEDIUM, and HIGH.

", + "enum":[ + "LOW", + "MEDIUM", + "HIGH" + ] + }, + "ServiceSoftwareOptions":{ + "type":"structure", + "members":{ + "CurrentVersion":{ + "shape":"String", + "documentation":"

The current service software version present on the domain.

" + }, + "NewVersion":{ + "shape":"String", + "documentation":"

The new service software version if one is available.

" + }, + "UpdateAvailable":{ + "shape":"Boolean", + "documentation":"

True if you're able to update your service software version. False if you can't update your service software version.

" + }, + "Cancellable":{ + "shape":"Boolean", + "documentation":"

True if you're able to cancel your service software version update. False if you can't cancel your service software update.

" + }, + "UpdateStatus":{ + "shape":"DeploymentStatus", + "documentation":"

The status of your service software update. This field can take the following values: ELIGIBLE, PENDING_UPDATE, IN_PROGRESS, COMPLETED, and NOT_ELIGIBLE.

" + }, + "Description":{ + "shape":"String", + "documentation":"

The description of the UpdateStatus.

" + }, + "AutomatedUpdateDate":{ + "shape":"DeploymentCloseDateTimeStamp", + "documentation":"

The timestamp, in Epoch time, until which you can manually request a service software update. After this date, we automatically update your service software.

" + }, + "OptionalDeployment":{ + "shape":"Boolean", + "documentation":"

True if a service software is never automatically updated. False if a service software is automatically updated after AutomatedUpdateDate.

" + } + }, + "documentation":"

The current options of an domain service software options.

" + }, + "ServiceUrl":{ + "type":"string", + "documentation":"

The endpoint to which service requests are submitted. For example, search-imdb-movies-oopcnjfn6ugofer3zx5iadxxca.eu-west-1.es.amazonaws.com or doc-imdb-movies-oopcnjfn6ugofer3zx5iadxxca.eu-west-1.es.amazonaws.com.

" + }, + "SnapshotOptions":{ + "type":"structure", + "members":{ + "AutomatedSnapshotStartHour":{ + "shape":"IntegerClass", + "documentation":"

The time, in UTC format, when the service takes a daily automated snapshot of the specified domain. Default is 0 hours.

" + } + }, + "documentation":"

The time, in UTC format, when the service takes a daily automated snapshot of the specified domain. Default is 0 hours.

" + }, + "SnapshotOptionsStatus":{ + "type":"structure", + "required":[ + "Options", + "Status" + ], + "members":{ + "Options":{ + "shape":"SnapshotOptions", + "documentation":"

The daily snapshot options specified for the domain.

" + }, + "Status":{ + "shape":"OptionStatus", + "documentation":"

The status of a daily automated snapshot.

" + } + }, + "documentation":"

Status of a daily automated snapshot.

" + }, + "StartAt":{"type":"timestamp"}, + "StartServiceSoftwareUpdateRequest":{ + "type":"structure", + "required":["DomainName"], + "members":{ + "DomainName":{ + "shape":"DomainName", + "documentation":"

The name of the domain that you want to update to the latest service software.

" + } + }, + "documentation":"

Container for the parameters to the StartServiceSoftwareUpdate operation. Specifies the name of the domain to schedule a service software update for.

" + }, + "StartServiceSoftwareUpdateResponse":{ + "type":"structure", + "members":{ + "ServiceSoftwareOptions":{ + "shape":"ServiceSoftwareOptions", + "documentation":"

The current status of the OpenSearch service software update.

" + } + }, + "documentation":"

The result of a StartServiceSoftwareUpdate operation. Contains the status of the update.

" + }, + "StartTimestamp":{"type":"timestamp"}, + "StorageSubTypeName":{ + "type":"string", + "documentation":"

Sub-type of the given storage type. List of available sub-storage options: \"instance\" storageType has no storageSubType. \"ebs\" storageType has the following valid storageSubTypes:

  1. standard
  2. gp2
  3. io1
See VolumeType for more information regarding each EBS storage option.

" + }, + "StorageType":{ + "type":"structure", + "members":{ + "StorageTypeName":{"shape":"StorageTypeName"}, + "StorageSubTypeName":{"shape":"StorageSubTypeName"}, + "StorageTypeLimits":{ + "shape":"StorageTypeLimitList", + "documentation":"

Limits that are applicable for the given storage type.

" + } + }, + "documentation":"

StorageTypes represents the list of storage-related types and their attributes that are available for a given InstanceType.

" + }, + "StorageTypeLimit":{ + "type":"structure", + "members":{ + "LimitName":{ + "shape":"LimitName", + "documentation":"

Name of storage limits that are applicable for the given storage type. If StorageType is \"ebs\", the following storage options are applicable:

  1. MinimumVolumeSize
  2. Minimum amount of volume size that is applicable for the given storage type. Can be empty if not applicable.
  3. MaximumVolumeSize
  4. Maximum amount of volume size that is applicable for the given storage type. Can be empty if not applicable.
  5. MaximumIops
  6. Maximum amount of Iops that is applicable for given the storage type. Can be empty if not applicable.
  7. MinimumIops
  8. Minimum amount of Iops that is applicable for given the storage type. Can be empty if not applicable.

" + }, + "LimitValues":{ + "shape":"LimitValueList", + "documentation":"

Values for the StorageTypeLimit$LimitName .

" + } + }, + "documentation":"

Limits that are applicable for the given storage type.

" + }, + "StorageTypeLimitList":{ + "type":"list", + "member":{"shape":"StorageTypeLimit"} + }, + "StorageTypeList":{ + "type":"list", + "member":{"shape":"StorageType"} + }, + "StorageTypeName":{ + "type":"string", + "documentation":"

Type of storage. List of available storage options:

  1. instance
  2. Built-in storage available for the instance
  3. ebs
  4. Elastic block storage attached to the instance

" + }, + "String":{"type":"string"}, + "StringList":{ + "type":"list", + "member":{"shape":"String"} + }, + "TLSSecurityPolicy":{ + "type":"string", + "enum":[ + "Policy-Min-TLS-1-0-2019-07", + "Policy-Min-TLS-1-2-2019-07" + ] + }, + "Tag":{ + "type":"structure", + "required":[ + "Key", + "Value" + ], + "members":{ + "Key":{ + "shape":"TagKey", + "documentation":"

The TagKey, the name of the tag. Tag keys must be unique for the domain to which they are attached.

" + }, + "Value":{ + "shape":"TagValue", + "documentation":"

The TagValue, the value assigned to the corresponding tag key. Tag values can be null and don't have to be unique in a tag set. For example, you can have a key value pair in a tag set of project : Trinity and cost-center : Trinity

" + } + }, + "documentation":"

A key value pair for a resource tag.

" + }, + "TagKey":{ + "type":"string", + "documentation":"

A string of length from 1 to 128 characters that specifies the key for a tag. Tag keys must be unique for the domain to which they're attached.

", + "max":128, + "min":1, + "pattern":".*" + }, + "TagList":{ + "type":"list", + "member":{"shape":"Tag"}, + "documentation":"

A list of Tag.

" + }, + "TagValue":{ + "type":"string", + "documentation":"

A string of length from 0 to 256 characters that specifies the value for a tag. Tag values can be null and don't have to be unique in a tag set.

", + "max":256, + "min":0, + "pattern":".*" + }, + "TimeUnit":{ + "type":"string", + "documentation":"

The unit of a maintenance schedule duration. Valid value is HOUR. See Auto-Tune for Amazon OpenSearch Service for more information.

", + "enum":["HOURS"] + }, + "UIntValue":{ + "type":"integer", + "min":0 + }, + "UpdateDomainConfigRequest":{ + "type":"structure", + "required":["DomainName"], + "members":{ + "DomainName":{ + "shape":"DomainName", + "documentation":"

The name of the domain you're updating.

", + "location":"uri", + "locationName":"DomainName" + }, + "ClusterConfig":{ + "shape":"ClusterConfig", + "documentation":"

The type and number of instances to instantiate for the domain cluster.

" + }, + "EBSOptions":{ + "shape":"EBSOptions", + "documentation":"

Specify the type and size of the EBS volume to use.

" + }, + "SnapshotOptions":{ + "shape":"SnapshotOptions", + "documentation":"

Option to set the time, in UTC format, for the daily automated snapshot. Default value is 0 hours.

" + }, + "VPCOptions":{ + "shape":"VPCOptions", + "documentation":"

Options to specify the subnets and security groups for the VPC endpoint. For more information, see Launching your Amazon OpenSearch Service domains using a VPC .

" + }, + "CognitoOptions":{ + "shape":"CognitoOptions", + "documentation":"

Options to specify the Cognito user and identity pools for OpenSearch Dashboards authentication. For more information, see Configuring Amazon Cognito authentication for OpenSearch Dashboards.

" + }, + "AdvancedOptions":{ + "shape":"AdvancedOptions", + "documentation":"

Modifies the advanced option to allow references to indices in an HTTP request body. Must be false when configuring access to individual sub-resources. By default, the value is true. See Advanced options for more information.

" + }, + "AccessPolicies":{ + "shape":"PolicyDocument", + "documentation":"

IAM access policy as a JSON-formatted string.

" + }, + "LogPublishingOptions":{ + "shape":"LogPublishingOptions", + "documentation":"

Map of LogType and LogPublishingOption, each containing options to publish a given type of OpenSearch log.

" + }, + "EncryptionAtRestOptions":{ + "shape":"EncryptionAtRestOptions", + "documentation":"

Specifies encryption of data at rest options.

" + }, + "DomainEndpointOptions":{ + "shape":"DomainEndpointOptions", + "documentation":"

Options to specify configuration that will be applied to the domain endpoint.

" + }, + "NodeToNodeEncryptionOptions":{ + "shape":"NodeToNodeEncryptionOptions", + "documentation":"

Specifies node-to-node encryption options.

" + }, + "AdvancedSecurityOptions":{ + "shape":"AdvancedSecurityOptionsInput", + "documentation":"

Specifies advanced security options.

" + }, + "AutoTuneOptions":{ + "shape":"AutoTuneOptions", + "documentation":"

Specifies Auto-Tune options.

" + } + }, + "documentation":"

Container for the parameters to the UpdateDomain operation. Specifies the type and number of instances in the domain cluster.

" + }, + "UpdateDomainConfigResponse":{ + "type":"structure", + "required":["DomainConfig"], + "members":{ + "DomainConfig":{ + "shape":"DomainConfig", + "documentation":"

The status of the updated domain.

" + } + }, + "documentation":"

The result of an UpdateDomain request. Contains the status of the domain being updated.

" + }, + "UpdatePackageRequest":{ + "type":"structure", + "required":[ + "PackageID", + "PackageSource" + ], + "members":{ + "PackageID":{ + "shape":"PackageID", + "documentation":"

The unique identifier for the package.

" + }, + "PackageSource":{"shape":"PackageSource"}, + "PackageDescription":{ + "shape":"PackageDescription", + "documentation":"

A new description of the package.

" + }, + "CommitMessage":{ + "shape":"CommitMessage", + "documentation":"

A commit message for the new version which is shown as part of GetPackageVersionHistoryResponse.

" + } + }, + "documentation":"

Container for request parameters to the UpdatePackage operation.

" + }, + "UpdatePackageResponse":{ + "type":"structure", + "members":{ + "PackageDetails":{ + "shape":"PackageDetails", + "documentation":"

Information about the package.

" + } + }, + "documentation":"

Container for the response returned by the UpdatePackage operation.

" + }, + "UpdateTimestamp":{"type":"timestamp"}, + "UpgradeDomainRequest":{ + "type":"structure", + "required":[ + "DomainName", + "TargetVersion" + ], + "members":{ + "DomainName":{"shape":"DomainName"}, + "TargetVersion":{ + "shape":"VersionString", + "documentation":"

The version of OpenSearch you intend to upgrade the domain to.

" + }, + "PerformCheckOnly":{ + "shape":"Boolean", + "documentation":"

When true, indicates that an upgrade eligibility check needs to be performed. Does not actually perform the upgrade.

" + }, + "AdvancedOptions":{"shape":"AdvancedOptions"} + }, + "documentation":"

Container for the request parameters to UpgradeDomain operation.

" + }, + "UpgradeDomainResponse":{ + "type":"structure", + "members":{ + "UpgradeId":{"shape":"String"}, + "DomainName":{"shape":"DomainName"}, + "TargetVersion":{ + "shape":"VersionString", + "documentation":"

The version of OpenSearch that you intend to upgrade the domain to.

" + }, + "PerformCheckOnly":{ + "shape":"Boolean", + "documentation":"

When true, indicates that an upgrade eligibility check needs to be performed. Does not actually perform the upgrade.

" + }, + "AdvancedOptions":{"shape":"AdvancedOptions"} + }, + "documentation":"

Container for response returned by UpgradeDomain operation.

" + }, + "UpgradeHistory":{ + "type":"structure", + "members":{ + "UpgradeName":{ + "shape":"UpgradeName", + "documentation":"

A string that briefly describes the upgrade.

" + }, + "StartTimestamp":{ + "shape":"StartTimestamp", + "documentation":"

UTC timestamp at which the upgrade API call was made in \"yyyy-MM-ddTHH:mm:ssZ\" format.

" + }, + "UpgradeStatus":{ + "shape":"UpgradeStatus", + "documentation":"

The current status of the upgrade. The status can take one of the following values:

  • In Progress
  • Succeeded
  • Succeeded with Issues
  • Failed

" + }, + "StepsList":{ + "shape":"UpgradeStepsList", + "documentation":"

A list of UpgradeStepItem s representing information about each step performed as part of a specific upgrade or upgrade eligibility check.

" + } + }, + "documentation":"

History of the last 10 upgrades and upgrade eligibility checks.

" + }, + "UpgradeHistoryList":{ + "type":"list", + "member":{"shape":"UpgradeHistory"} + }, + "UpgradeName":{"type":"string"}, + "UpgradeStatus":{ + "type":"string", + "enum":[ + "IN_PROGRESS", + "SUCCEEDED", + "SUCCEEDED_WITH_ISSUES", + "FAILED" + ] + }, + "UpgradeStep":{ + "type":"string", + "enum":[ + "PRE_UPGRADE_CHECK", + "SNAPSHOT", + "UPGRADE" + ] + }, + "UpgradeStepItem":{ + "type":"structure", + "members":{ + "UpgradeStep":{ + "shape":"UpgradeStep", + "documentation":"

One of three steps an upgrade or upgrade eligibility check goes through:

  • PreUpgradeCheck
  • Snapshot
  • Upgrade

" + }, + "UpgradeStepStatus":{ + "shape":"UpgradeStatus", + "documentation":"

The current status of the upgrade. The status can take one of the following values:

  • In Progress
  • Succeeded
  • Succeeded with Issues
  • Failed

" + }, + "Issues":{ + "shape":"Issues", + "documentation":"

A list of strings containing detailed information about the errors encountered in a particular step.

" + }, + "ProgressPercent":{ + "shape":"Double", + "documentation":"

The floating point value representing the progress percentage of a particular step.

" + } + }, + "documentation":"

Represents a single step of the upgrade or upgrade eligibility check workflow.

" + }, + "UpgradeStepsList":{ + "type":"list", + "member":{"shape":"UpgradeStepItem"} + }, + "UserPoolId":{ + "type":"string", + "max":55, + "min":1, + "pattern":"[\\w-]+_[0-9a-zA-Z]+" + }, + "Username":{ + "type":"string", + "max":64, + "min":1, + "pattern":".*", + "sensitive":true + }, + "VPCDerivedInfo":{ + "type":"structure", + "members":{ + "VPCId":{ + "shape":"String", + "documentation":"

The VPC ID for the domain. Exists only if the domain was created with VPCOptions.

" + }, + "SubnetIds":{ + "shape":"StringList", + "documentation":"

The subnets for the VPC endpoint.

" + }, + "AvailabilityZones":{ + "shape":"StringList", + "documentation":"

The Availability Zones for the domain. Exists only if the domain was created with VPCOptions.

" + }, + "SecurityGroupIds":{ + "shape":"StringList", + "documentation":"

The security groups for the VPC endpoint.

" + } + }, + "documentation":"

Options to specify the subnets and security groups for the VPC endpoint. For more information, see Launching your Amazon OpenSearch Service domains using a VPC.

" + }, + "VPCDerivedInfoStatus":{ + "type":"structure", + "required":[ + "Options", + "Status" + ], + "members":{ + "Options":{ + "shape":"VPCDerivedInfo", + "documentation":"

The VPC options for the specified domain.

" + }, + "Status":{ + "shape":"OptionStatus", + "documentation":"

The status of the VPC options for the specified domain.

" + } + }, + "documentation":"

Status of the VPC options for the specified domain.

" + }, + "VPCOptions":{ + "type":"structure", + "members":{ + "SubnetIds":{ + "shape":"StringList", + "documentation":"

The subnets for the VPC endpoint.

" + }, + "SecurityGroupIds":{ + "shape":"StringList", + "documentation":"

The security groups for the VPC endpoint.

" + } + }, + "documentation":"

Options to specify the subnets and security groups for the VPC endpoint. For more information, see Launching your Amazon OpenSearch Service domains using a VPC.

" + }, + "ValueStringList":{ + "type":"list", + "member":{"shape":"NonEmptyString"}, + "min":1 + }, + "VersionList":{ + "type":"list", + "member":{"shape":"VersionString"}, + "documentation":"

List of supported OpenSearch versions.

" + }, + "VersionStatus":{ + "type":"structure", + "required":[ + "Options", + "Status" + ], + "members":{ + "Options":{ + "shape":"VersionString", + "documentation":"

The OpenSearch version for the specified OpenSearch domain.

" + }, + "Status":{ + "shape":"OptionStatus", + "documentation":"

The status of the OpenSearch version options for the specified OpenSearch domain.

" + } + }, + "documentation":"

The status of the OpenSearch version options for the specified OpenSearch domain.

" + }, + "VersionString":{ + "type":"string", + "max":18, + "min":14, + "pattern":"^Elasticsearch_[0-9]{1}\\.[0-9]{1,2}$|^OpenSearch_[0-9]{1,2}\\.[0-9]{1,2}$" + }, + "VolumeType":{ + "type":"string", + "documentation":"

The type of EBS volume, standard, gp2, or io1. See Configuring EBS-based Storage for more information.

", + "enum":[ + "standard", + "gp2", + "io1" + ] + }, + "ZoneAwarenessConfig":{ + "type":"structure", + "members":{ + "AvailabilityZoneCount":{ + "shape":"IntegerClass", + "documentation":"

An integer value to indicate the number of availability zones for a domain when zone awareness is enabled. This should be equal to number of subnets if VPC endpoints is enabled.

" + } + }, + "documentation":"

The zone awareness configuration for the domain cluster, such as the number of availability zones.

" + } + }, + "documentation":"Amazon OpenSearch Configuration Service

Use the Amazon OpenSearch configuration API to create, configure, and manage Amazon OpenSearch Service domains.

For sample code that uses the configuration API, see the Amazon OpenSearch Service Developer Guide. The guide also contains sample code for sending signed HTTP requests to the OpenSearch APIs.

The endpoint for configuration service requests is region-specific: es.region.amazonaws.com. For example, es.us-east-1.amazonaws.com. For a current list of supported regions and endpoints, see Regions and Endpoints.

" +} diff --git a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts index 56215192fb2f2..68156b6412958 100644 --- a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts +++ b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts @@ -1,5 +1,7 @@ /* eslint-disable no-console */ import { execSync } from 'child_process'; +import * as fs from 'fs'; +import { join } from 'path'; // import the AWSLambda package explicitly, // which is globally available in the Lambda runtime, // as otherwise linking this repository with link-all.sh @@ -77,6 +79,38 @@ function installLatestSdk(): void { latestSdkInstalled = true; } +const patchedServices: { serviceName: string; apiVersions: string[] }[] = [ + { serviceName: 'OpenSearch', apiVersions: ['2021-01-01'] }, +]; +/** + * Patches the AWS SDK by loading service models in the same manner as the actual SDK + */ +function patchSdk(awsSdk: any): any { + const apiLoader = awsSdk.apiLoader; + patchedServices.forEach(({ serviceName, apiVersions }) => { + const lowerServiceName = serviceName.toLowerCase(); + if (!awsSdk.Service.hasService(lowerServiceName)) { + apiLoader.services[lowerServiceName] = {}; + awsSdk[serviceName] = awsSdk.Service.defineService(lowerServiceName, apiVersions); + } else { + awsSdk.Service.addVersions(awsSdk[serviceName], apiVersions); + } + apiVersions.forEach(apiVersion => { + Object.defineProperty(apiLoader.services[lowerServiceName], apiVersion, { + get: function get() { + const modelFilePrefix = `aws-sdk-patch/${lowerServiceName}-${apiVersion}`; + const model = JSON.parse(fs.readFileSync(join(__dirname, `${modelFilePrefix}.service.json`), 'utf-8')); + model.paginators = JSON.parse(fs.readFileSync(join(__dirname, `${modelFilePrefix}.paginators.json`), 'utf-8')).pagination; + return model; + }, + enumerable: true, + configurable: true, + }); + }); + }); + return awsSdk; +} + /* eslint-disable @typescript-eslint/no-require-imports, import/no-extraneous-dependencies */ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent, context: AWSLambda.Context) { try { @@ -94,6 +128,11 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent } else { AWS = require('aws-sdk'); } + try { + AWS = patchSdk(AWS); + } catch (e) { + console.log(`Failed to patch AWS SDK: ${e}. Proceeding with the installed copy.`); + } console.log(JSON.stringify(event)); console.log('AWS SDK VERSION: ' + AWS.VERSION); diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index e3b5cfb83db95..c4d46c68219d6 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -268,6 +268,7 @@ "@aws-cdk/aws-networkfirewall": "0.0.0", "@aws-cdk/aws-networkmanager": "0.0.0", "@aws-cdk/aws-nimblestudio": "0.0.0", + "@aws-cdk/aws-opensearchservice": "0.0.0", "@aws-cdk/aws-opsworks": "0.0.0", "@aws-cdk/aws-opsworkscm": "0.0.0", "@aws-cdk/aws-pinpoint": "0.0.0", diff --git a/packages/decdk/package.json b/packages/decdk/package.json index f386bef25c9e5..9ccdc3dd7c754 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -175,6 +175,7 @@ "@aws-cdk/aws-networkfirewall": "0.0.0", "@aws-cdk/aws-networkmanager": "0.0.0", "@aws-cdk/aws-nimblestudio": "0.0.0", + "@aws-cdk/aws-opensearchservice": "0.0.0", "@aws-cdk/aws-opsworks": "0.0.0", "@aws-cdk/aws-opsworkscm": "0.0.0", "@aws-cdk/aws-pinpoint": "0.0.0", diff --git a/packages/monocdk/package.json b/packages/monocdk/package.json index 10c0235667470..99e49a69bf99a 100644 --- a/packages/monocdk/package.json +++ b/packages/monocdk/package.json @@ -269,6 +269,7 @@ "@aws-cdk/aws-networkfirewall": "0.0.0", "@aws-cdk/aws-networkmanager": "0.0.0", "@aws-cdk/aws-nimblestudio": "0.0.0", + "@aws-cdk/aws-opensearchservice": "0.0.0", "@aws-cdk/aws-opsworks": "0.0.0", "@aws-cdk/aws-opsworkscm": "0.0.0", "@aws-cdk/aws-pinpoint": "0.0.0", From aa572eb45b1fb504024dcbbfdb262e45450652d6 Mon Sep 17 00:00:00 2001 From: Ben Chaimberg Date: Thu, 16 Sep 2021 18:43:10 -0400 Subject: [PATCH 2/5] chore(release): 1.123.0 --- CHANGELOG.md | 7 +++++++ version.v1.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca9ef129cbefa..f98788af9e375 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.123.0](https://github.com/aws/aws-cdk/compare/v1.122.0...v1.123.0) (2021-09-16) + + +### Features + +* **opensearch:** rebrand Elasticsearch as OpenSearch ([e6c4ca5](https://github.com/aws/aws-cdk/commit/e6c4ca5e71934e890eabe41190e9c2d0bd42aefb)), closes [aws/aws-cdk#16467](https://github.com/aws/aws-cdk/issues/16467) + ## [1.122.0](https://github.com/aws/aws-cdk/compare/v1.121.0...v1.122.0) (2021-09-08) diff --git a/version.v1.json b/version.v1.json index d2fb6576ac697..77e24208bd94a 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.122.0" + "version": "1.123.0" } \ No newline at end of file From cafe8257b777c2c6f6143553b147873d640c6745 Mon Sep 17 00:00:00 2001 From: kaizen3031593 <36202692+kaizen3031593@users.noreply.github.com> Date: Fri, 17 Sep 2021 00:48:18 -0400 Subject: [PATCH 3/5] chore: migrate even more modules to jest (#16504) Migrate more modules from nodeunit to jest. Migrates `aws-codedeploy`, `aws-config`, `aws-dynamodb-global`, `aws-ssm`, and `assets`. All tests are not modified, purely migrated to jest. I added 1 test in `aws-config` to satisfy the Jest rule that 80% of branches be tested. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/assets/.gitignore | 3 +- packages/@aws-cdk/assets/.npmignore | 3 +- packages/@aws-cdk/assets/jest.config.js | 2 + packages/@aws-cdk/assets/package.json | 7 +- packages/@aws-cdk/assets/test/compat.test.ts | 12 + .../test/{test.staging.ts => staging.test.ts} | 46 +- packages/@aws-cdk/assets/test/test.compat.ts | 15 - packages/@aws-cdk/aws-codedeploy/.gitignore | 3 +- packages/@aws-cdk/aws-codedeploy/.npmignore | 3 +- .../@aws-cdk/aws-codedeploy/jest.config.js | 2 + packages/@aws-cdk/aws-codedeploy/package.json | 5 +- .../test/ecs/application.test.ts | 26 + .../test/ecs/deployment-group.test.ts | 18 + .../test/ecs/test.application.ts | 33 - .../test/ecs/test.deployment-group.ts | 23 - .../test/lambda/application.test.ts | 24 + .../lambda/custom-deployment-config.test.ts | 162 ++++ .../test/lambda/deployment-group.test.ts | 583 +++++++++++++ .../test/lambda/test.application.ts | 28 - .../lambda/test.custom-deployment-config.ts | 170 ---- .../test/lambda/test.deployment-group.ts | 600 -------------- .../test/server/deployment-config.test.ts | 45 + .../test/server/deployment-group.test.ts | 415 ++++++++++ .../test/server/test.deployment-config.ts | 54 -- .../test/server/test.deployment-group.ts | 450 ---------- packages/@aws-cdk/aws-config/.gitignore | 4 +- packages/@aws-cdk/aws-config/.npmignore | 3 +- packages/@aws-cdk/aws-config/jest.config.js | 2 + packages/@aws-cdk/aws-config/package.json | 5 +- ...managed-rules.ts => managed-rules.test.ts} | 84 +- .../test/{test.rule.ts => rule.test.ts} | 96 +-- .../@aws-cdk/aws-dynamodb-global/.gitignore | 3 +- .../@aws-cdk/aws-dynamodb-global/.npmignore | 3 +- .../aws-dynamodb-global/jest.config.js | 2 + .../@aws-cdk/aws-dynamodb-global/package.json | 7 +- .../test/dynamodb-global.test.ts | 116 +++ .../test/test.dynamodb.global.ts | 125 --- packages/@aws-cdk/aws-ssm/.gitignore | 3 +- packages/@aws-cdk/aws-ssm/.npmignore | 3 +- packages/@aws-cdk/aws-ssm/jest.config.js | 2 + packages/@aws-cdk/aws-ssm/package.json | 5 +- .../test/parameter-store-string.test.ts | 65 ++ .../@aws-cdk/aws-ssm/test/parameter.test.ts | 733 +++++++++++++++++ .../aws-ssm/test/ssm-document.test.ts | 26 + .../test/test.parameter-store-string.ts | 76 -- .../@aws-cdk/aws-ssm/test/test.parameter.ts | 778 ------------------ .../aws-ssm/test/test.ssm-document.ts | 30 - packages/@aws-cdk/aws-ssm/test/test.util.ts | 73 -- packages/@aws-cdk/aws-ssm/test/util.test.ts | 57 ++ 49 files changed, 2441 insertions(+), 2592 deletions(-) create mode 100644 packages/@aws-cdk/assets/jest.config.js create mode 100644 packages/@aws-cdk/assets/test/compat.test.ts rename packages/@aws-cdk/assets/test/{test.staging.ts => staging.test.ts} (66%) delete mode 100644 packages/@aws-cdk/assets/test/test.compat.ts create mode 100644 packages/@aws-cdk/aws-codedeploy/jest.config.js create mode 100644 packages/@aws-cdk/aws-codedeploy/test/ecs/application.test.ts create mode 100644 packages/@aws-cdk/aws-codedeploy/test/ecs/deployment-group.test.ts delete mode 100644 packages/@aws-cdk/aws-codedeploy/test/ecs/test.application.ts delete mode 100644 packages/@aws-cdk/aws-codedeploy/test/ecs/test.deployment-group.ts create mode 100644 packages/@aws-cdk/aws-codedeploy/test/lambda/application.test.ts create mode 100644 packages/@aws-cdk/aws-codedeploy/test/lambda/custom-deployment-config.test.ts create mode 100644 packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts delete mode 100644 packages/@aws-cdk/aws-codedeploy/test/lambda/test.application.ts delete mode 100644 packages/@aws-cdk/aws-codedeploy/test/lambda/test.custom-deployment-config.ts delete mode 100644 packages/@aws-cdk/aws-codedeploy/test/lambda/test.deployment-group.ts create mode 100644 packages/@aws-cdk/aws-codedeploy/test/server/deployment-config.test.ts create mode 100644 packages/@aws-cdk/aws-codedeploy/test/server/deployment-group.test.ts delete mode 100644 packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-config.ts delete mode 100644 packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-group.ts create mode 100644 packages/@aws-cdk/aws-config/jest.config.js rename packages/@aws-cdk/aws-config/test/{test.managed-rules.ts => managed-rules.test.ts} (67%) rename packages/@aws-cdk/aws-config/test/{test.rule.ts => rule.test.ts} (79%) create mode 100644 packages/@aws-cdk/aws-dynamodb-global/jest.config.js create mode 100644 packages/@aws-cdk/aws-dynamodb-global/test/dynamodb-global.test.ts delete mode 100644 packages/@aws-cdk/aws-dynamodb-global/test/test.dynamodb.global.ts create mode 100644 packages/@aws-cdk/aws-ssm/jest.config.js create mode 100644 packages/@aws-cdk/aws-ssm/test/parameter-store-string.test.ts create mode 100644 packages/@aws-cdk/aws-ssm/test/parameter.test.ts create mode 100644 packages/@aws-cdk/aws-ssm/test/ssm-document.test.ts delete mode 100644 packages/@aws-cdk/aws-ssm/test/test.parameter-store-string.ts delete mode 100644 packages/@aws-cdk/aws-ssm/test/test.parameter.ts delete mode 100644 packages/@aws-cdk/aws-ssm/test/test.ssm-document.ts delete mode 100644 packages/@aws-cdk/aws-ssm/test/test.util.ts create mode 100644 packages/@aws-cdk/aws-ssm/test/util.test.ts diff --git a/packages/@aws-cdk/assets/.gitignore b/packages/@aws-cdk/assets/.gitignore index 7e3964a75701e..cc09865158319 100644 --- a/packages/@aws-cdk/assets/.gitignore +++ b/packages/@aws-cdk/assets/.gitignore @@ -16,4 +16,5 @@ nyc.config.js *.snk !.eslintrc.js -junit.xml \ No newline at end of file +junit.xml +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/assets/.npmignore b/packages/@aws-cdk/assets/.npmignore index 9a032ae80868c..e8acf10a468a1 100644 --- a/packages/@aws-cdk/assets/.npmignore +++ b/packages/@aws-cdk/assets/.npmignore @@ -24,4 +24,5 @@ tsconfig.json **/cdk.out junit.xml test/ -!*.lit.ts \ No newline at end of file +!*.lit.ts +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/assets/jest.config.js b/packages/@aws-cdk/assets/jest.config.js new file mode 100644 index 0000000000000..f5d5c4c8ad18f --- /dev/null +++ b/packages/@aws-cdk/assets/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; \ No newline at end of file diff --git a/packages/@aws-cdk/assets/package.json b/packages/@aws-cdk/assets/package.json index f3d8de9b44039..df31bf21db5e9 100644 --- a/packages/@aws-cdk/assets/package.json +++ b/packages/@aws-cdk/assets/package.json @@ -54,7 +54,8 @@ "cdk-build": { "pre": [ "rm -rf test/fs/fixtures && cd test/fs && tar -xzvf fixtures.tar.gz" - ] + ], + "jest": true }, "keywords": [ "aws", @@ -69,12 +70,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.32", + "@types/jest": "^26.0.24", "@types/sinon": "^9.0.11", "aws-cdk": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "nodeunit": "^0.11.3", + "jest": "^26.6.3", "pkglint": "0.0.0", "sinon": "^9.2.4", "ts-mock-imports": "^1.3.7", diff --git a/packages/@aws-cdk/assets/test/compat.test.ts b/packages/@aws-cdk/assets/test/compat.test.ts new file mode 100644 index 0000000000000..25afe6dd9411b --- /dev/null +++ b/packages/@aws-cdk/assets/test/compat.test.ts @@ -0,0 +1,12 @@ +import { SymlinkFollowMode } from '@aws-cdk/core'; +import '@aws-cdk/assert-internal/jest'; +import { FollowMode } from '../lib'; +import { toSymlinkFollow } from '../lib/compat'; + +test('FollowMode compatibility', () => { + expect(toSymlinkFollow(undefined)).toEqual(undefined); + expect(toSymlinkFollow(FollowMode.ALWAYS)).toEqual(SymlinkFollowMode.ALWAYS); + expect(toSymlinkFollow(FollowMode.BLOCK_EXTERNAL)).toEqual(SymlinkFollowMode.BLOCK_EXTERNAL); + expect(toSymlinkFollow(FollowMode.EXTERNAL)).toEqual(SymlinkFollowMode.EXTERNAL); + expect(toSymlinkFollow(FollowMode.NEVER)).toEqual(SymlinkFollowMode.NEVER); +}); diff --git a/packages/@aws-cdk/assets/test/test.staging.ts b/packages/@aws-cdk/assets/test/staging.test.ts similarity index 66% rename from packages/@aws-cdk/assets/test/test.staging.ts rename to packages/@aws-cdk/assets/test/staging.test.ts index 149bb99258601..4c95f236f2d81 100644 --- a/packages/@aws-cdk/assets/test/test.staging.ts +++ b/packages/@aws-cdk/assets/test/staging.test.ts @@ -2,11 +2,11 @@ import * as fs from 'fs'; import * as path from 'path'; import { App, Stack } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import { Test } from 'nodeunit'; +import '@aws-cdk/assert-internal/jest'; import { Staging } from '../lib'; -export = { - 'base case'(test: Test) { +describe('staging', () => { + test('base case', () => { // GIVEN const stack = new Stack(); const sourcePath = path.join(__dirname, 'fs', 'fixtures', 'test1'); @@ -14,13 +14,12 @@ export = { // WHEN const staging = new Staging(stack, 's1', { sourcePath }); - test.deepEqual(staging.assetHash, '2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00'); - test.deepEqual(staging.sourcePath, sourcePath); - test.deepEqual(staging.relativeStagedPath(stack), 'asset.2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00'); - test.done(); - }, + expect(staging.assetHash).toEqual('2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00'); + expect(staging.sourcePath).toEqual(sourcePath); + expect(staging.relativeStagedPath(stack)).toEqual('asset.2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00'); + }); - 'staging can be disabled through context'(test: Test) { + test('staging can be disabled through context', () => { // GIVEN const stack = new Stack(); stack.node.setContext(cxapi.DISABLE_ASSET_STAGING_CONTEXT, true); @@ -29,13 +28,12 @@ export = { // WHEN const staging = new Staging(stack, 's1', { sourcePath }); - test.deepEqual(staging.assetHash, '2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00'); - test.deepEqual(staging.sourcePath, sourcePath); - test.deepEqual(staging.absoluteStagedPath, sourcePath); - test.done(); - }, + expect(staging.assetHash).toEqual('2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00'); + expect(staging.sourcePath).toEqual(sourcePath); + expect(staging.absoluteStagedPath).toEqual(sourcePath); + }); - 'files are copied to the output directory during synth'(test: Test) { + test('files are copied to the output directory during synth', () => { // GIVEN const app = new App(); const stack = new Stack(app, 'stack'); @@ -48,7 +46,7 @@ export = { // THEN const assembly = app.synth(); - test.deepEqual(fs.readdirSync(assembly.directory), [ + expect(fs.readdirSync(assembly.directory)).toEqual([ 'asset.2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00', 'asset.af10ac04b3b607b0f8659c8f0cee8c343025ee75baf0b146f10f0e5311d2c46b.gz', 'cdk.out', @@ -56,10 +54,9 @@ export = { 'stack.template.json', 'tree.json', ]); - test.done(); - }, + }); - 'allow specifying extra data to include in the source hash'(test: Test) { + test('allow specifying extra data to include in the source hash', () => { // GIVEN const app = new App(); const stack = new Stack(app, 'stack'); @@ -70,9 +67,8 @@ export = { const withExtra = new Staging(stack, 'withExtra', { sourcePath: directory, extraHash: 'boom' }); // THEN - test.notEqual(withoutExtra.assetHash, withExtra.assetHash); - test.deepEqual(withoutExtra.assetHash, '2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00'); - test.deepEqual(withExtra.assetHash, 'c95c915a5722bb9019e2c725d11868e5a619b55f36172f76bcbcaa8bb2d10c5f'); - test.done(); - }, -}; + expect(withoutExtra.assetHash).not.toEqual(withExtra.assetHash); + expect(withoutExtra.assetHash).toEqual('2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00'); + expect(withExtra.assetHash).toEqual('c95c915a5722bb9019e2c725d11868e5a619b55f36172f76bcbcaa8bb2d10c5f'); + }); +}); diff --git a/packages/@aws-cdk/assets/test/test.compat.ts b/packages/@aws-cdk/assets/test/test.compat.ts deleted file mode 100644 index 65b50d0b37daf..0000000000000 --- a/packages/@aws-cdk/assets/test/test.compat.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { SymlinkFollowMode } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import { FollowMode } from '../lib'; -import { toSymlinkFollow } from '../lib/compat'; - -export = { - 'FollowMode compatibility'(test: Test) { - test.equal(toSymlinkFollow(undefined), null); - test.equal(toSymlinkFollow(FollowMode.ALWAYS), SymlinkFollowMode.ALWAYS); - test.equal(toSymlinkFollow(FollowMode.BLOCK_EXTERNAL), SymlinkFollowMode.BLOCK_EXTERNAL); - test.equal(toSymlinkFollow(FollowMode.EXTERNAL), SymlinkFollowMode.EXTERNAL); - test.equal(toSymlinkFollow(FollowMode.NEVER), SymlinkFollowMode.NEVER); - test.done(); - }, -}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codedeploy/.gitignore b/packages/@aws-cdk/aws-codedeploy/.gitignore index 7fb7303ad5bf6..677cec030db6c 100644 --- a/packages/@aws-cdk/aws-codedeploy/.gitignore +++ b/packages/@aws-cdk/aws-codedeploy/.gitignore @@ -16,4 +16,5 @@ nyc.config.js !.eslintrc.js junit.xml -!test/lambda/*/*.js \ No newline at end of file +!test/lambda/*/*.js +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codedeploy/.npmignore b/packages/@aws-cdk/aws-codedeploy/.npmignore index 9a032ae80868c..e8acf10a468a1 100644 --- a/packages/@aws-cdk/aws-codedeploy/.npmignore +++ b/packages/@aws-cdk/aws-codedeploy/.npmignore @@ -24,4 +24,5 @@ tsconfig.json **/cdk.out junit.xml test/ -!*.lit.ts \ No newline at end of file +!*.lit.ts +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codedeploy/jest.config.js b/packages/@aws-cdk/aws-codedeploy/jest.config.js new file mode 100644 index 0000000000000..f5d5c4c8ad18f --- /dev/null +++ b/packages/@aws-cdk/aws-codedeploy/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codedeploy/package.json b/packages/@aws-cdk/aws-codedeploy/package.json index 89f9706dd9f3e..ac67fc09875c2 100644 --- a/packages/@aws-cdk/aws-codedeploy/package.json +++ b/packages/@aws-cdk/aws-codedeploy/package.json @@ -55,6 +55,7 @@ }, "cdk-build": { "cloudformation": "AWS::CodeDeploy", + "jest": true, "env": { "AWSLINT_BASE_CONSTRUCT": true } @@ -75,11 +76,11 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.32", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nodeunit": "^0.11.3", + "jest": "^26.6.3", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-codedeploy/test/ecs/application.test.ts b/packages/@aws-cdk/aws-codedeploy/test/ecs/application.test.ts new file mode 100644 index 0000000000000..6a6966167cd2f --- /dev/null +++ b/packages/@aws-cdk/aws-codedeploy/test/ecs/application.test.ts @@ -0,0 +1,26 @@ +import '@aws-cdk/assert-internal/jest'; +import * as cdk from '@aws-cdk/core'; +import * as codedeploy from '../../lib'; + +describe('CodeDeploy ECS Application', () => { + test('can be created', () => { + const stack = new cdk.Stack(); + new codedeploy.EcsApplication(stack, 'MyApp'); + + expect(stack).toHaveResource('AWS::CodeDeploy::Application', { + ComputePlatform: 'ECS', + }); + }); + + test('can be created with explicit name', () => { + const stack = new cdk.Stack(); + new codedeploy.EcsApplication(stack, 'MyApp', { + applicationName: 'my-name', + }); + + expect(stack).toHaveResource('AWS::CodeDeploy::Application', { + ApplicationName: 'my-name', + ComputePlatform: 'ECS', + }); + }); +}); diff --git a/packages/@aws-cdk/aws-codedeploy/test/ecs/deployment-group.test.ts b/packages/@aws-cdk/aws-codedeploy/test/ecs/deployment-group.test.ts new file mode 100644 index 0000000000000..70750e3406e44 --- /dev/null +++ b/packages/@aws-cdk/aws-codedeploy/test/ecs/deployment-group.test.ts @@ -0,0 +1,18 @@ +import * as cdk from '@aws-cdk/core'; +import * as codedeploy from '../../lib'; + +describe('CodeDeploy ECS DeploymentGroup', () => { + describe('imported with fromEcsDeploymentGroupAttributes', () => { + test('defaults the Deployment Config to AllAtOnce', () => { + const stack = new cdk.Stack(); + + const ecsApp = codedeploy.EcsApplication.fromEcsApplicationName(stack, 'EA', 'EcsApplication'); + const importedGroup = codedeploy.EcsDeploymentGroup.fromEcsDeploymentGroupAttributes(stack, 'EDG', { + application: ecsApp, + deploymentGroupName: 'EcsDeploymentGroup', + }); + + expect(importedGroup.deploymentConfig).toEqual(codedeploy.EcsDeploymentConfig.ALL_AT_ONCE); + }); + }); +}); diff --git a/packages/@aws-cdk/aws-codedeploy/test/ecs/test.application.ts b/packages/@aws-cdk/aws-codedeploy/test/ecs/test.application.ts deleted file mode 100644 index de45dc0bd58e9..0000000000000 --- a/packages/@aws-cdk/aws-codedeploy/test/ecs/test.application.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; -import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import * as codedeploy from '../../lib'; - -export = { - 'CodeDeploy ECS Application': { - 'can be created'(test: Test) { - const stack = new cdk.Stack(); - new codedeploy.EcsApplication(stack, 'MyApp'); - - expect(stack).to(haveResource('AWS::CodeDeploy::Application', { - ComputePlatform: 'ECS', - })); - - test.done(); - }, - - 'can be created with explicit name'(test: Test) { - const stack = new cdk.Stack(); - new codedeploy.EcsApplication(stack, 'MyApp', { - applicationName: 'my-name', - }); - - expect(stack).to(haveResource('AWS::CodeDeploy::Application', { - ApplicationName: 'my-name', - ComputePlatform: 'ECS', - })); - - test.done(); - }, - }, -}; diff --git a/packages/@aws-cdk/aws-codedeploy/test/ecs/test.deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/test/ecs/test.deployment-group.ts deleted file mode 100644 index 255d15f82bf01..0000000000000 --- a/packages/@aws-cdk/aws-codedeploy/test/ecs/test.deployment-group.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import * as codedeploy from '../../lib'; - -export = { - 'CodeDeploy ECS DeploymentGroup': { - 'imported with fromEcsDeploymentGroupAttributes': { - 'defaults the Deployment Config to AllAtOnce'(test: Test) { - const stack = new cdk.Stack(); - - const ecsApp = codedeploy.EcsApplication.fromEcsApplicationName(stack, 'EA', 'EcsApplication'); - const importedGroup = codedeploy.EcsDeploymentGroup.fromEcsDeploymentGroupAttributes(stack, 'EDG', { - application: ecsApp, - deploymentGroupName: 'EcsDeploymentGroup', - }); - - test.equal(importedGroup.deploymentConfig, codedeploy.EcsDeploymentConfig.ALL_AT_ONCE); - - test.done(); - }, - }, - }, -}; diff --git a/packages/@aws-cdk/aws-codedeploy/test/lambda/application.test.ts b/packages/@aws-cdk/aws-codedeploy/test/lambda/application.test.ts new file mode 100644 index 0000000000000..eaa140d0c045c --- /dev/null +++ b/packages/@aws-cdk/aws-codedeploy/test/lambda/application.test.ts @@ -0,0 +1,24 @@ +import '@aws-cdk/assert-internal/jest'; +import * as cdk from '@aws-cdk/core'; +import * as codedeploy from '../../lib'; + +describe('CodeDeploy Lambda Application', () => { + test('can be created', () => { + const stack = new cdk.Stack(); + new codedeploy.LambdaApplication(stack, 'MyApp'); + expect(stack).toHaveResource('AWS::CodeDeploy::Application', { + ComputePlatform: 'Lambda', + }); + }); + + test('can be created with explicit name', () => { + const stack = new cdk.Stack(); + new codedeploy.LambdaApplication(stack, 'MyApp', { + applicationName: 'my-name', + }); + expect(stack).toHaveResource('AWS::CodeDeploy::Application', { + ApplicationName: 'my-name', + ComputePlatform: 'Lambda', + }); + }); +}); diff --git a/packages/@aws-cdk/aws-codedeploy/test/lambda/custom-deployment-config.test.ts b/packages/@aws-cdk/aws-codedeploy/test/lambda/custom-deployment-config.test.ts new file mode 100644 index 0000000000000..9f69328613dac --- /dev/null +++ b/packages/@aws-cdk/aws-codedeploy/test/lambda/custom-deployment-config.test.ts @@ -0,0 +1,162 @@ +import { ResourcePart } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk from '@aws-cdk/core'; +import * as codedeploy from '../../lib'; + +function mockFunction(stack: cdk.Stack, id: string) { + return new lambda.Function(stack, id, { + code: lambda.Code.fromInline('mock'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, + }); +} +function mockAlias(stack: cdk.Stack) { + return new lambda.Alias(stack, 'Alias', { + aliasName: 'my-alias', + version: new lambda.Version(stack, 'Version', { + lambda: mockFunction(stack, 'Function'), + }), + }); +} + +let stack: cdk.Stack; +let application: codedeploy.LambdaApplication; +let alias: lambda.Alias; + +beforeEach(() => { + stack = new cdk.Stack(); + application = new codedeploy.LambdaApplication(stack, 'MyApp'); + alias = mockAlias(stack); +}); + + +test('custom resource created', () => { + // WHEN + const config = new codedeploy.CustomLambdaDeploymentConfig(stack, 'CustomConfig', { + type: codedeploy.CustomLambdaDeploymentConfigType.CANARY, + interval: cdk.Duration.minutes(1), + percentage: 5, + }); + new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { + application, + alias, + deploymentConfig: config, + }); + + // THEN + expect(stack).toHaveResourceLike('Custom::AWS', { + ServiceToken: { + 'Fn::GetAtt': [ + 'AWS679f53fac002430cb0da5b7982bd22872D164C4C', + 'Arn', + ], + }, + Create: '{"service":"CodeDeploy","action":"createDeploymentConfig","parameters":{"deploymentConfigName":"CustomConfig.LambdaCanary5Percent1Minutes","computePlatform":"Lambda","trafficRoutingConfig":{"type":"TimeBasedCanary","timeBasedCanary":{"canaryInterval":"1","canaryPercentage":"5"}}},"physicalResourceId":{"id":"CustomConfig.LambdaCanary5Percent1Minutes"}}', + Update: '{"service":"CodeDeploy","action":"createDeploymentConfig","parameters":{"deploymentConfigName":"CustomConfig.LambdaCanary5Percent1Minutes","computePlatform":"Lambda","trafficRoutingConfig":{"type":"TimeBasedCanary","timeBasedCanary":{"canaryInterval":"1","canaryPercentage":"5"}}},"physicalResourceId":{"id":"CustomConfig.LambdaCanary5Percent1Minutes"}}', + Delete: '{"service":"CodeDeploy","action":"deleteDeploymentConfig","parameters":{"deploymentConfigName":"CustomConfig.LambdaCanary5Percent1Minutes"}}', + }); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'codedeploy:CreateDeploymentConfig', + Effect: 'Allow', + Resource: '*', + }, + { + Action: 'codedeploy:DeleteDeploymentConfig', + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + }); +}); + +test('custom resource created with specific name', () => { + // WHEN + const config = new codedeploy.CustomLambdaDeploymentConfig(stack, 'CustomConfig', { + type: codedeploy.CustomLambdaDeploymentConfigType.CANARY, + interval: cdk.Duration.minutes(1), + percentage: 5, + deploymentConfigName: 'MyDeploymentConfig', + }); + new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { + application, + alias, + deploymentConfig: config, + }); + + // THEN + expect(stack).toHaveResourceLike('Custom::AWS', { + Create: '{"service":"CodeDeploy","action":"createDeploymentConfig","parameters":{"deploymentConfigName":"MyDeploymentConfig","computePlatform":"Lambda","trafficRoutingConfig":{"type":"TimeBasedCanary","timeBasedCanary":{"canaryInterval":"1","canaryPercentage":"5"}}},"physicalResourceId":{"id":"MyDeploymentConfig"}}', + Update: '{"service":"CodeDeploy","action":"createDeploymentConfig","parameters":{"deploymentConfigName":"MyDeploymentConfig","computePlatform":"Lambda","trafficRoutingConfig":{"type":"TimeBasedCanary","timeBasedCanary":{"canaryInterval":"1","canaryPercentage":"5"}}},"physicalResourceId":{"id":"MyDeploymentConfig"}}', + Delete: '{"service":"CodeDeploy","action":"deleteDeploymentConfig","parameters":{"deploymentConfigName":"MyDeploymentConfig"}}', + }); +}); + +test('can create linear custom config', () => { + // WHEN + const config = new codedeploy.CustomLambdaDeploymentConfig(stack, 'CustomConfig', { + type: codedeploy.CustomLambdaDeploymentConfigType.LINEAR, + interval: cdk.Duration.minutes(1), + percentage: 5, + }); + new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { + application, + alias, + deploymentConfig: config, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + DeploymentConfigName: 'CustomConfig.LambdaLinear5PercentEvery1Minutes', + }); +}); + +test('can create canary custom config', () => { + // WHEN + const config = new codedeploy.CustomLambdaDeploymentConfig(stack, 'CustomConfig', { + type: codedeploy.CustomLambdaDeploymentConfigType.CANARY, + interval: cdk.Duration.minutes(1), + percentage: 5, + }); + new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { + application, + alias, + deploymentConfig: config, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + DeploymentConfigName: 'CustomConfig.LambdaCanary5Percent1Minutes', + }); +}); + +test('dependency on the config exists to ensure ordering', () => { + // WHEN + const config = new codedeploy.CustomLambdaDeploymentConfig(stack, 'CustomConfig', { + type: codedeploy.CustomLambdaDeploymentConfigType.CANARY, + interval: cdk.Duration.minutes(1), + percentage: 5, + }); + new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { + application, + alias, + deploymentConfig: config, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + Properties: { + DeploymentConfigName: 'CustomConfig.LambdaCanary5Percent1Minutes', + }, + DependsOn: [ + 'CustomConfigDeploymentConfigCustomResourcePolicy0426B684', + 'CustomConfigDeploymentConfigE9E1F384', + ], + }, ResourcePart.CompleteDefinition); +}); diff --git a/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts b/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts new file mode 100644 index 0000000000000..1f4b18e852cff --- /dev/null +++ b/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts @@ -0,0 +1,583 @@ +import { ResourcePart } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import * as iam from '@aws-cdk/aws-iam'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk from '@aws-cdk/core'; +import * as codedeploy from '../../lib'; + +function mockFunction(stack: cdk.Stack, id: string) { + return new lambda.Function(stack, id, { + code: lambda.Code.fromInline('mock'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, + }); +} +function mockAlias(stack: cdk.Stack) { + return new lambda.Alias(stack, 'Alias', { + aliasName: 'my-alias', + version: new lambda.Version(stack, 'Version', { + lambda: mockFunction(stack, 'Function'), + }), + }); +} + +describe('CodeDeploy Lambda DeploymentGroup', () => { + test('can be created with default AllAtOnce IN_PLACE configuration', () => { + const stack = new cdk.Stack(); + const application = new codedeploy.LambdaApplication(stack, 'MyApp'); + const alias = mockAlias(stack); + new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { + application, + alias, + deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, + }); + + expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + ApplicationName: { + Ref: 'MyApp3CE31C26', + }, + ServiceRoleArn: { + 'Fn::GetAtt': [ + 'MyDGServiceRole5E94FD88', + 'Arn', + ], + }, + AutoRollbackConfiguration: { + Enabled: true, + Events: [ + 'DEPLOYMENT_FAILURE', + ], + }, + DeploymentConfigName: 'CodeDeployDefault.LambdaAllAtOnce', + DeploymentStyle: { + DeploymentOption: 'WITH_TRAFFIC_CONTROL', + DeploymentType: 'BLUE_GREEN', + }, + }); + + expect(stack).toHaveResource('AWS::Lambda::Alias', { + Type: 'AWS::Lambda::Alias', + Properties: { + FunctionName: { + Ref: 'Function76856677', + }, + FunctionVersion: { + 'Fn::GetAtt': [ + 'Version6A868472', + 'Version', + ], + }, + Name: 'my-alias', + }, + UpdatePolicy: { + CodeDeployLambdaAliasUpdate: { + ApplicationName: { + Ref: 'MyApp3CE31C26', + }, + DeploymentGroupName: { + Ref: 'MyDGC350BD3F', + }, + }, + }, + }, ResourcePart.CompleteDefinition); + + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [{ + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: { 'Fn::Join': ['', ['codedeploy.', { Ref: 'AWS::Region' }, '.', { Ref: 'AWS::URLSuffix' }]] }, + }, + }], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AWSCodeDeployRoleForLambdaLimited', + ], + ], + }, + ], + }); + }); + + + test('can be created with explicit name', () => { + const stack = new cdk.Stack(); + const application = new codedeploy.LambdaApplication(stack, 'MyApp'); + const alias = mockAlias(stack); + new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { + application, + alias, + deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, + deploymentGroupName: 'test', + }); + + expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + DeploymentGroupName: 'test', + }); + }); + + test('can be created with explicit role', () => { + const stack = new cdk.Stack(); + const application = new codedeploy.LambdaApplication(stack, 'MyApp'); + const alias = mockAlias(stack); + const serviceRole = new iam.Role(stack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('not-codedeploy.test'), + }); + + new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { + application, + alias, + deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, + role: serviceRole, + }); + + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [{ + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'not-codedeploy.test', + }, + }], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AWSCodeDeployRoleForLambdaLimited', + ], + ], + }, + ], + }); + }); + + test('can configure blue/green traffic shifting', () => { + const stack = new cdk.Stack(); + const application = new codedeploy.LambdaApplication(stack, 'MyApp'); + const alias = mockAlias(stack); + new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { + application, + alias, + deploymentConfig: codedeploy.LambdaDeploymentConfig.LINEAR_10PERCENT_EVERY_1MINUTE, + }); + + expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + ApplicationName: { + Ref: 'MyApp3CE31C26', + }, + ServiceRoleArn: { + 'Fn::GetAtt': [ + 'MyDGServiceRole5E94FD88', + 'Arn', + ], + }, + AutoRollbackConfiguration: { + Enabled: true, + Events: [ + 'DEPLOYMENT_FAILURE', + ], + }, + DeploymentConfigName: 'CodeDeployDefault.LambdaLinear10PercentEvery1Minute', + DeploymentStyle: { + DeploymentOption: 'WITH_TRAFFIC_CONTROL', + DeploymentType: 'BLUE_GREEN', + }, + }); + }); + + test('can rollback on alarm', () => { + const stack = new cdk.Stack(); + const application = new codedeploy.LambdaApplication(stack, 'MyApp'); + const alias = mockAlias(stack); + new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { + application, + alias, + deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, + alarms: [new cloudwatch.Alarm(stack, 'Failures', { + metric: alias.metricErrors(), + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, + threshold: 1, + evaluationPeriods: 1, + })], + }); + + expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + AlarmConfiguration: { + Alarms: [{ + Name: { + Ref: 'Failures8A3E1A2F', + }, + }], + Enabled: true, + }, + AutoRollbackConfiguration: { + Enabled: true, + Events: [ + 'DEPLOYMENT_FAILURE', + 'DEPLOYMENT_STOP_ON_ALARM', + ], + }, + }); + }); + + test('onPreHook throws error if pre-hook already defined', () => { + const stack = new cdk.Stack(); + const alias = mockAlias(stack); + const group = new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { + alias, + preHook: mockFunction(stack, 'PreHook'), + deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, + }); + expect(() => group.addPreHook(mockFunction(stack, 'PreHook2'))).toThrow(); + }); + + test('onPostHook throws error if post-hook already defined', () => { + const stack = new cdk.Stack(); + const alias = mockAlias(stack); + const group = new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { + alias, + postHook: mockFunction(stack, 'PostHook'), + deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, + }); + expect(() => group.addPostHook(mockFunction(stack, 'PostHook2'))).toThrow(); + }); + + test('can run pre hook lambda function before deployment', () => { + const stack = new cdk.Stack(); + const application = new codedeploy.LambdaApplication(stack, 'MyApp'); + const alias = mockAlias(stack); + new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { + application, + alias, + preHook: mockFunction(stack, 'PreHook'), + deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, + }); + + expect(stack).toHaveResourceLike('AWS::Lambda::Alias', { + UpdatePolicy: { + CodeDeployLambdaAliasUpdate: { + ApplicationName: { + Ref: 'MyApp3CE31C26', + }, + DeploymentGroupName: { + Ref: 'MyDGC350BD3F', + }, + BeforeAllowTrafficHook: { + Ref: 'PreHook8B53F672', + }, + }, + }, + }, ResourcePart.CompleteDefinition); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyName: 'MyDGServiceRoleDefaultPolicy65E8E1EA', + Roles: [{ + Ref: 'MyDGServiceRole5E94FD88', + }], + PolicyDocument: { + Statement: [{ + Action: 'lambda:InvokeFunction', + Resource: { + 'Fn::GetAtt': [ + 'PreHook8B53F672', + 'Arn', + ], + }, + Effect: 'Allow', + }], + Version: '2012-10-17', + }, + }); + }); + + test('can add pre hook lambda function after creating the deployment group', () => { + const stack = new cdk.Stack(); + const application = new codedeploy.LambdaApplication(stack, 'MyApp'); + const alias = mockAlias(stack); + const group = new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { + application, + alias, + deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, + }); + group.addPreHook(mockFunction(stack, 'PreHook')); + + expect(stack).toHaveResourceLike('AWS::Lambda::Alias', { + UpdatePolicy: { + CodeDeployLambdaAliasUpdate: { + ApplicationName: { + Ref: 'MyApp3CE31C26', + }, + DeploymentGroupName: { + Ref: 'MyDGC350BD3F', + }, + BeforeAllowTrafficHook: { + Ref: 'PreHook8B53F672', + }, + }, + }, + }, ResourcePart.CompleteDefinition); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyName: 'MyDGServiceRoleDefaultPolicy65E8E1EA', + Roles: [{ + Ref: 'MyDGServiceRole5E94FD88', + }], + PolicyDocument: { + Statement: [{ + Action: 'lambda:InvokeFunction', + Resource: { + 'Fn::GetAtt': [ + 'PreHook8B53F672', + 'Arn', + ], + }, + Effect: 'Allow', + }], + Version: '2012-10-17', + }, + }); + }); + + test('can run post hook lambda function before deployment', () => { + const stack = new cdk.Stack(); + const application = new codedeploy.LambdaApplication(stack, 'MyApp'); + const alias = mockAlias(stack); + new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { + application, + alias, + postHook: mockFunction(stack, 'PostHook'), + deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, + }); + + expect(stack).toHaveResourceLike('AWS::Lambda::Alias', { + UpdatePolicy: { + CodeDeployLambdaAliasUpdate: { + ApplicationName: { + Ref: 'MyApp3CE31C26', + }, + DeploymentGroupName: { + Ref: 'MyDGC350BD3F', + }, + AfterAllowTrafficHook: { + Ref: 'PostHookF2E49B30', + }, + }, + }, + }, ResourcePart.CompleteDefinition); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyName: 'MyDGServiceRoleDefaultPolicy65E8E1EA', + Roles: [{ + Ref: 'MyDGServiceRole5E94FD88', + }], + PolicyDocument: { + Statement: [{ + Action: 'lambda:InvokeFunction', + Resource: { + 'Fn::GetAtt': [ + 'PostHookF2E49B30', + 'Arn', + ], + }, + Effect: 'Allow', + }], + Version: '2012-10-17', + }, + }); + }); + + test('can add post hook lambda function after creating the deployment group', () => { + const stack = new cdk.Stack(); + const application = new codedeploy.LambdaApplication(stack, 'MyApp'); + const alias = mockAlias(stack); + const group = new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { + application, + alias, + deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, + }); + group.addPostHook(mockFunction(stack, 'PostHook')); + + expect(stack).toHaveResourceLike('AWS::Lambda::Alias', { + UpdatePolicy: { + CodeDeployLambdaAliasUpdate: { + ApplicationName: { + Ref: 'MyApp3CE31C26', + }, + DeploymentGroupName: { + Ref: 'MyDGC350BD3F', + }, + AfterAllowTrafficHook: { + Ref: 'PostHookF2E49B30', + }, + }, + }, + }, ResourcePart.CompleteDefinition); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyName: 'MyDGServiceRoleDefaultPolicy65E8E1EA', + Roles: [{ + Ref: 'MyDGServiceRole5E94FD88', + }], + PolicyDocument: { + Statement: [{ + Action: 'lambda:InvokeFunction', + Resource: { + 'Fn::GetAtt': [ + 'PostHookF2E49B30', + 'Arn', + ], + }, + Effect: 'Allow', + }], + Version: '2012-10-17', + }, + }); + }); + + test('can disable rollback when alarm polling fails', () => { + const stack = new cdk.Stack(); + const application = new codedeploy.LambdaApplication(stack, 'MyApp'); + const alias = mockAlias(stack); + new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { + application, + alias, + postHook: mockFunction(stack, 'PostHook'), + deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, + ignorePollAlarmsFailure: true, + alarms: [new cloudwatch.Alarm(stack, 'Failures', { + metric: alias.metricErrors(), + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, + threshold: 1, + evaluationPeriods: 1, + })], + }); + + expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + AlarmConfiguration: { + Alarms: [{ + Name: { + Ref: 'Failures8A3E1A2F', + }, + }], + Enabled: true, + IgnorePollAlarmFailure: true, + }, + }); + }); + + test('can disable rollback when deployment fails', () => { + const stack = new cdk.Stack(); + const application = new codedeploy.LambdaApplication(stack, 'MyApp'); + const alias = mockAlias(stack); + new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { + application, + alias, + postHook: mockFunction(stack, 'PostHook'), + deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, + autoRollback: { + failedDeployment: false, + }, + }); + + expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + ApplicationName: { + Ref: 'MyApp3CE31C26', + }, + ServiceRoleArn: { + 'Fn::GetAtt': [ + 'MyDGServiceRole5E94FD88', + 'Arn', + ], + }, + DeploymentConfigName: 'CodeDeployDefault.LambdaAllAtOnce', + DeploymentStyle: { + DeploymentOption: 'WITH_TRAFFIC_CONTROL', + DeploymentType: 'BLUE_GREEN', + }, + }); + }); + + test('can enable rollback when deployment stops', () => { + const stack = new cdk.Stack(); + const application = new codedeploy.LambdaApplication(stack, 'MyApp'); + const alias = mockAlias(stack); + new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { + application, + alias, + postHook: mockFunction(stack, 'PostHook'), + deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, + autoRollback: { + stoppedDeployment: true, + }, + }); + + expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + AutoRollbackConfiguration: { + Enabled: true, + Events: [ + 'DEPLOYMENT_FAILURE', + 'DEPLOYMENT_STOP_ON_REQUEST', + ], + }, + }); + }); + + test('can disable rollback when alarm in failure state', () => { + const stack = new cdk.Stack(); + const application = new codedeploy.LambdaApplication(stack, 'MyApp'); + const alias = mockAlias(stack); + new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { + application, + alias, + postHook: mockFunction(stack, 'PostHook'), + deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, + autoRollback: { + deploymentInAlarm: false, + }, + alarms: [new cloudwatch.Alarm(stack, 'Failures', { + metric: alias.metricErrors(), + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, + threshold: 1, + evaluationPeriods: 1, + })], + }); + + expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + AutoRollbackConfiguration: { + Enabled: true, + Events: [ + 'DEPLOYMENT_FAILURE', + ], + }, + }); + }); +}); + +describe('imported with fromLambdaDeploymentGroupAttributes', () => { + test('defaults the Deployment Config to Canary10Percent5Minutes', () => { + const stack = new cdk.Stack(); + + const lambdaApp = codedeploy.LambdaApplication.fromLambdaApplicationName(stack, 'LA', 'LambdaApplication'); + const importedGroup = codedeploy.LambdaDeploymentGroup.fromLambdaDeploymentGroupAttributes(stack, 'LDG', { + application: lambdaApp, + deploymentGroupName: 'LambdaDeploymentGroup', + }); + + expect(importedGroup.deploymentConfig).toEqual(codedeploy.LambdaDeploymentConfig.CANARY_10PERCENT_5MINUTES); + }); +}); diff --git a/packages/@aws-cdk/aws-codedeploy/test/lambda/test.application.ts b/packages/@aws-cdk/aws-codedeploy/test/lambda/test.application.ts deleted file mode 100644 index f3f79115d0165..0000000000000 --- a/packages/@aws-cdk/aws-codedeploy/test/lambda/test.application.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; -import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import * as codedeploy from '../../lib'; - -export = { - 'CodeDeploy Lambda Application': { - 'can be created'(test: Test) { - const stack = new cdk.Stack(); - new codedeploy.LambdaApplication(stack, 'MyApp'); - expect(stack).to(haveResource('AWS::CodeDeploy::Application', { - ComputePlatform: 'Lambda', - })); - test.done(); - }, - 'can be created with explicit name'(test: Test) { - const stack = new cdk.Stack(); - new codedeploy.LambdaApplication(stack, 'MyApp', { - applicationName: 'my-name', - }); - expect(stack).to(haveResource('AWS::CodeDeploy::Application', { - ApplicationName: 'my-name', - ComputePlatform: 'Lambda', - })); - test.done(); - }, - }, -}; diff --git a/packages/@aws-cdk/aws-codedeploy/test/lambda/test.custom-deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/test/lambda/test.custom-deployment-config.ts deleted file mode 100644 index 9ba58b043aebd..0000000000000 --- a/packages/@aws-cdk/aws-codedeploy/test/lambda/test.custom-deployment-config.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { expect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert-internal'; -import * as lambda from '@aws-cdk/aws-lambda'; -import * as cdk from '@aws-cdk/core'; -import { ICallbackFunction, Test } from 'nodeunit'; -import * as codedeploy from '../../lib'; - -function mockFunction(stack: cdk.Stack, id: string) { - return new lambda.Function(stack, id, { - code: lambda.Code.fromInline('mock'), - handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, - }); -} -function mockAlias(stack: cdk.Stack) { - return new lambda.Alias(stack, 'Alias', { - aliasName: 'my-alias', - version: new lambda.Version(stack, 'Version', { - lambda: mockFunction(stack, 'Function'), - }), - }); -} - -let stack: cdk.Stack; -let application: codedeploy.LambdaApplication; -let alias: lambda.Alias; - -export = { - 'setUp'(cb: ICallbackFunction) { - stack = new cdk.Stack(); - application = new codedeploy.LambdaApplication(stack, 'MyApp'); - alias = mockAlias(stack); - cb(); - }, - 'custom resource created'(test: Test) { - - // WHEN - const config = new codedeploy.CustomLambdaDeploymentConfig(stack, 'CustomConfig', { - type: codedeploy.CustomLambdaDeploymentConfigType.CANARY, - interval: cdk.Duration.minutes(1), - percentage: 5, - }); - new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { - application, - alias, - deploymentConfig: config, - }); - - // THEN - expect(stack).to(haveResourceLike('Custom::AWS', { - ServiceToken: { - 'Fn::GetAtt': [ - 'AWS679f53fac002430cb0da5b7982bd22872D164C4C', - 'Arn', - ], - }, - Create: '{"service":"CodeDeploy","action":"createDeploymentConfig","parameters":{"deploymentConfigName":"CustomConfig.LambdaCanary5Percent1Minutes","computePlatform":"Lambda","trafficRoutingConfig":{"type":"TimeBasedCanary","timeBasedCanary":{"canaryInterval":"1","canaryPercentage":"5"}}},"physicalResourceId":{"id":"CustomConfig.LambdaCanary5Percent1Minutes"}}', - Update: '{"service":"CodeDeploy","action":"createDeploymentConfig","parameters":{"deploymentConfigName":"CustomConfig.LambdaCanary5Percent1Minutes","computePlatform":"Lambda","trafficRoutingConfig":{"type":"TimeBasedCanary","timeBasedCanary":{"canaryInterval":"1","canaryPercentage":"5"}}},"physicalResourceId":{"id":"CustomConfig.LambdaCanary5Percent1Minutes"}}', - Delete: '{"service":"CodeDeploy","action":"deleteDeploymentConfig","parameters":{"deploymentConfigName":"CustomConfig.LambdaCanary5Percent1Minutes"}}', - })); - - expect(stack).to(haveResource('AWS::IAM::Policy', { - PolicyDocument: { - Statement: [ - { - Action: 'codedeploy:CreateDeploymentConfig', - Effect: 'Allow', - Resource: '*', - }, - { - Action: 'codedeploy:DeleteDeploymentConfig', - Effect: 'Allow', - Resource: '*', - }, - ], - Version: '2012-10-17', - }, - })); - test.done(); - }, - 'custom resource created with specific name'(test: Test) { - - // WHEN - const config = new codedeploy.CustomLambdaDeploymentConfig(stack, 'CustomConfig', { - type: codedeploy.CustomLambdaDeploymentConfigType.CANARY, - interval: cdk.Duration.minutes(1), - percentage: 5, - deploymentConfigName: 'MyDeploymentConfig', - }); - new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { - application, - alias, - deploymentConfig: config, - }); - - // THEN - expect(stack).to(haveResourceLike('Custom::AWS', { - Create: '{"service":"CodeDeploy","action":"createDeploymentConfig","parameters":{"deploymentConfigName":"MyDeploymentConfig","computePlatform":"Lambda","trafficRoutingConfig":{"type":"TimeBasedCanary","timeBasedCanary":{"canaryInterval":"1","canaryPercentage":"5"}}},"physicalResourceId":{"id":"MyDeploymentConfig"}}', - Update: '{"service":"CodeDeploy","action":"createDeploymentConfig","parameters":{"deploymentConfigName":"MyDeploymentConfig","computePlatform":"Lambda","trafficRoutingConfig":{"type":"TimeBasedCanary","timeBasedCanary":{"canaryInterval":"1","canaryPercentage":"5"}}},"physicalResourceId":{"id":"MyDeploymentConfig"}}', - Delete: '{"service":"CodeDeploy","action":"deleteDeploymentConfig","parameters":{"deploymentConfigName":"MyDeploymentConfig"}}', - })); - test.done(); - }, - 'can create linear custom config'(test: Test) { - - // WHEN - const config = new codedeploy.CustomLambdaDeploymentConfig(stack, 'CustomConfig', { - type: codedeploy.CustomLambdaDeploymentConfigType.LINEAR, - interval: cdk.Duration.minutes(1), - percentage: 5, - }); - new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { - application, - alias, - deploymentConfig: config, - }); - - // THEN - expect(stack).to(haveResourceLike('AWS::CodeDeploy::DeploymentGroup', { - DeploymentConfigName: 'CustomConfig.LambdaLinear5PercentEvery1Minutes', - })); - - test.done(); - }, - 'can create canary custom config'(test: Test) { - - // WHEN - const config = new codedeploy.CustomLambdaDeploymentConfig(stack, 'CustomConfig', { - type: codedeploy.CustomLambdaDeploymentConfigType.CANARY, - interval: cdk.Duration.minutes(1), - percentage: 5, - }); - new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { - application, - alias, - deploymentConfig: config, - }); - - // THEN - expect(stack).to(haveResourceLike('AWS::CodeDeploy::DeploymentGroup', { - DeploymentConfigName: 'CustomConfig.LambdaCanary5Percent1Minutes', - })); - test.done(); - }, - 'dependency on the config exists to ensure ordering'(test: Test) { - - // WHEN - const config = new codedeploy.CustomLambdaDeploymentConfig(stack, 'CustomConfig', { - type: codedeploy.CustomLambdaDeploymentConfigType.CANARY, - interval: cdk.Duration.minutes(1), - percentage: 5, - }); - new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { - application, - alias, - deploymentConfig: config, - }); - - // THEN - expect(stack).to(haveResourceLike('AWS::CodeDeploy::DeploymentGroup', { - Properties: { - DeploymentConfigName: 'CustomConfig.LambdaCanary5Percent1Minutes', - }, - DependsOn: [ - 'CustomConfigDeploymentConfigCustomResourcePolicy0426B684', - 'CustomConfigDeploymentConfigE9E1F384', - ], - }, ResourcePart.CompleteDefinition)); - test.done(); - }, -}; diff --git a/packages/@aws-cdk/aws-codedeploy/test/lambda/test.deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/test/lambda/test.deployment-group.ts deleted file mode 100644 index 6274fd6ca3457..0000000000000 --- a/packages/@aws-cdk/aws-codedeploy/test/lambda/test.deployment-group.ts +++ /dev/null @@ -1,600 +0,0 @@ -import { expect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert-internal'; -import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; -import * as iam from '@aws-cdk/aws-iam'; -import * as lambda from '@aws-cdk/aws-lambda'; -import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import * as codedeploy from '../../lib'; - -function mockFunction(stack: cdk.Stack, id: string) { - return new lambda.Function(stack, id, { - code: lambda.Code.fromInline('mock'), - handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, - }); -} -function mockAlias(stack: cdk.Stack) { - return new lambda.Alias(stack, 'Alias', { - aliasName: 'my-alias', - version: new lambda.Version(stack, 'Version', { - lambda: mockFunction(stack, 'Function'), - }), - }); -} - -export = { - 'CodeDeploy Lambda DeploymentGroup': { - 'can be created with default AllAtOnce IN_PLACE configuration'(test: Test) { - const stack = new cdk.Stack(); - const application = new codedeploy.LambdaApplication(stack, 'MyApp'); - const alias = mockAlias(stack); - new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { - application, - alias, - deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, - }); - - expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentGroup', { - ApplicationName: { - Ref: 'MyApp3CE31C26', - }, - ServiceRoleArn: { - 'Fn::GetAtt': [ - 'MyDGServiceRole5E94FD88', - 'Arn', - ], - }, - AutoRollbackConfiguration: { - Enabled: true, - Events: [ - 'DEPLOYMENT_FAILURE', - ], - }, - DeploymentConfigName: 'CodeDeployDefault.LambdaAllAtOnce', - DeploymentStyle: { - DeploymentOption: 'WITH_TRAFFIC_CONTROL', - DeploymentType: 'BLUE_GREEN', - }, - })); - - expect(stack).to(haveResource('AWS::Lambda::Alias', { - Type: 'AWS::Lambda::Alias', - Properties: { - FunctionName: { - Ref: 'Function76856677', - }, - FunctionVersion: { - 'Fn::GetAtt': [ - 'Version6A868472', - 'Version', - ], - }, - Name: 'my-alias', - }, - UpdatePolicy: { - CodeDeployLambdaAliasUpdate: { - ApplicationName: { - Ref: 'MyApp3CE31C26', - }, - DeploymentGroupName: { - Ref: 'MyDGC350BD3F', - }, - }, - }, - }, ResourcePart.CompleteDefinition)); - - expect(stack).to(haveResource('AWS::IAM::Role', { - AssumeRolePolicyDocument: { - Statement: [{ - Action: 'sts:AssumeRole', - Effect: 'Allow', - Principal: { - Service: { 'Fn::Join': ['', ['codedeploy.', { Ref: 'AWS::Region' }, '.', { Ref: 'AWS::URLSuffix' }]] }, - }, - }], - Version: '2012-10-17', - }, - ManagedPolicyArns: [ - { - 'Fn::Join': [ - '', - [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::aws:policy/service-role/AWSCodeDeployRoleForLambdaLimited', - ], - ], - }, - ], - })); - - test.done(); - }, - 'can be created with explicit name'(test: Test) { - const stack = new cdk.Stack(); - const application = new codedeploy.LambdaApplication(stack, 'MyApp'); - const alias = mockAlias(stack); - new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { - application, - alias, - deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, - deploymentGroupName: 'test', - }); - - expect(stack).to(haveResourceLike('AWS::CodeDeploy::DeploymentGroup', { - DeploymentGroupName: 'test', - })); - - test.done(); - }, - 'can be created with explicit role'(test: Test) { - const stack = new cdk.Stack(); - const application = new codedeploy.LambdaApplication(stack, 'MyApp'); - const alias = mockAlias(stack); - const serviceRole = new iam.Role(stack, 'MyRole', { - assumedBy: new iam.ServicePrincipal('not-codedeploy.test'), - }); - - new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { - application, - alias, - deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, - role: serviceRole, - }); - - expect(stack).to(haveResource('AWS::IAM::Role', { - AssumeRolePolicyDocument: { - Statement: [{ - Action: 'sts:AssumeRole', - Effect: 'Allow', - Principal: { - Service: 'not-codedeploy.test', - }, - }], - Version: '2012-10-17', - }, - ManagedPolicyArns: [ - { - 'Fn::Join': [ - '', - [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::aws:policy/service-role/AWSCodeDeployRoleForLambdaLimited', - ], - ], - }, - ], - })); - - test.done(); - }, - 'can configure blue/green traffic shifting'(test: Test) { - const stack = new cdk.Stack(); - const application = new codedeploy.LambdaApplication(stack, 'MyApp'); - const alias = mockAlias(stack); - new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { - application, - alias, - deploymentConfig: codedeploy.LambdaDeploymentConfig.LINEAR_10PERCENT_EVERY_1MINUTE, - }); - - expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentGroup', { - ApplicationName: { - Ref: 'MyApp3CE31C26', - }, - ServiceRoleArn: { - 'Fn::GetAtt': [ - 'MyDGServiceRole5E94FD88', - 'Arn', - ], - }, - AutoRollbackConfiguration: { - Enabled: true, - Events: [ - 'DEPLOYMENT_FAILURE', - ], - }, - DeploymentConfigName: 'CodeDeployDefault.LambdaLinear10PercentEvery1Minute', - DeploymentStyle: { - DeploymentOption: 'WITH_TRAFFIC_CONTROL', - DeploymentType: 'BLUE_GREEN', - }, - })); - - test.done(); - }, - 'can rollback on alarm'(test: Test) { - const stack = new cdk.Stack(); - const application = new codedeploy.LambdaApplication(stack, 'MyApp'); - const alias = mockAlias(stack); - new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { - application, - alias, - deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, - alarms: [new cloudwatch.Alarm(stack, 'Failures', { - metric: alias.metricErrors(), - comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, - threshold: 1, - evaluationPeriods: 1, - })], - }); - - expect(stack).to(haveResourceLike('AWS::CodeDeploy::DeploymentGroup', { - AlarmConfiguration: { - Alarms: [{ - Name: { - Ref: 'Failures8A3E1A2F', - }, - }], - Enabled: true, - }, - AutoRollbackConfiguration: { - Enabled: true, - Events: [ - 'DEPLOYMENT_FAILURE', - 'DEPLOYMENT_STOP_ON_ALARM', - ], - }, - })); - - test.done(); - }, - 'onPreHook throws error if pre-hook already defined'(test: Test) { - const stack = new cdk.Stack(); - const alias = mockAlias(stack); - const group = new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { - alias, - preHook: mockFunction(stack, 'PreHook'), - deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, - }); - test.throws(() => group.addPreHook(mockFunction(stack, 'PreHook2'))); - test.done(); - }, - 'onPostHook throws error if post-hook already defined'(test: Test) { - const stack = new cdk.Stack(); - const alias = mockAlias(stack); - const group = new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { - alias, - postHook: mockFunction(stack, 'PostHook'), - deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, - }); - test.throws(() => group.addPostHook(mockFunction(stack, 'PostHook2'))); - test.done(); - }, - 'can run pre hook lambda function before deployment'(test: Test) { - const stack = new cdk.Stack(); - const application = new codedeploy.LambdaApplication(stack, 'MyApp'); - const alias = mockAlias(stack); - new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { - application, - alias, - preHook: mockFunction(stack, 'PreHook'), - deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, - }); - - expect(stack).to(haveResourceLike('AWS::Lambda::Alias', { - UpdatePolicy: { - CodeDeployLambdaAliasUpdate: { - ApplicationName: { - Ref: 'MyApp3CE31C26', - }, - DeploymentGroupName: { - Ref: 'MyDGC350BD3F', - }, - BeforeAllowTrafficHook: { - Ref: 'PreHook8B53F672', - }, - }, - }, - }, ResourcePart.CompleteDefinition)); - - expect(stack).to(haveResource('AWS::IAM::Policy', { - PolicyName: 'MyDGServiceRoleDefaultPolicy65E8E1EA', - Roles: [{ - Ref: 'MyDGServiceRole5E94FD88', - }], - PolicyDocument: { - Statement: [{ - Action: 'lambda:InvokeFunction', - Resource: { - 'Fn::GetAtt': [ - 'PreHook8B53F672', - 'Arn', - ], - }, - Effect: 'Allow', - }], - Version: '2012-10-17', - }, - })); - - test.done(); - }, - 'can add pre hook lambda function after creating the deployment group'(test: Test) { - const stack = new cdk.Stack(); - const application = new codedeploy.LambdaApplication(stack, 'MyApp'); - const alias = mockAlias(stack); - const group = new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { - application, - alias, - deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, - }); - group.addPreHook(mockFunction(stack, 'PreHook')); - - expect(stack).to(haveResourceLike('AWS::Lambda::Alias', { - UpdatePolicy: { - CodeDeployLambdaAliasUpdate: { - ApplicationName: { - Ref: 'MyApp3CE31C26', - }, - DeploymentGroupName: { - Ref: 'MyDGC350BD3F', - }, - BeforeAllowTrafficHook: { - Ref: 'PreHook8B53F672', - }, - }, - }, - }, ResourcePart.CompleteDefinition)); - - expect(stack).to(haveResource('AWS::IAM::Policy', { - PolicyName: 'MyDGServiceRoleDefaultPolicy65E8E1EA', - Roles: [{ - Ref: 'MyDGServiceRole5E94FD88', - }], - PolicyDocument: { - Statement: [{ - Action: 'lambda:InvokeFunction', - Resource: { - 'Fn::GetAtt': [ - 'PreHook8B53F672', - 'Arn', - ], - }, - Effect: 'Allow', - }], - Version: '2012-10-17', - }, - })); - - test.done(); - }, - 'can run post hook lambda function before deployment'(test: Test) { - const stack = new cdk.Stack(); - const application = new codedeploy.LambdaApplication(stack, 'MyApp'); - const alias = mockAlias(stack); - new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { - application, - alias, - postHook: mockFunction(stack, 'PostHook'), - deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, - }); - - expect(stack).to(haveResourceLike('AWS::Lambda::Alias', { - UpdatePolicy: { - CodeDeployLambdaAliasUpdate: { - ApplicationName: { - Ref: 'MyApp3CE31C26', - }, - DeploymentGroupName: { - Ref: 'MyDGC350BD3F', - }, - AfterAllowTrafficHook: { - Ref: 'PostHookF2E49B30', - }, - }, - }, - }, ResourcePart.CompleteDefinition)); - - expect(stack).to(haveResource('AWS::IAM::Policy', { - PolicyName: 'MyDGServiceRoleDefaultPolicy65E8E1EA', - Roles: [{ - Ref: 'MyDGServiceRole5E94FD88', - }], - PolicyDocument: { - Statement: [{ - Action: 'lambda:InvokeFunction', - Resource: { - 'Fn::GetAtt': [ - 'PostHookF2E49B30', - 'Arn', - ], - }, - Effect: 'Allow', - }], - Version: '2012-10-17', - }, - })); - - test.done(); - }, - 'can add post hook lambda function after creating the deployment group'(test: Test) { - const stack = new cdk.Stack(); - const application = new codedeploy.LambdaApplication(stack, 'MyApp'); - const alias = mockAlias(stack); - const group = new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { - application, - alias, - deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, - }); - group.addPostHook(mockFunction(stack, 'PostHook')); - - expect(stack).to(haveResourceLike('AWS::Lambda::Alias', { - UpdatePolicy: { - CodeDeployLambdaAliasUpdate: { - ApplicationName: { - Ref: 'MyApp3CE31C26', - }, - DeploymentGroupName: { - Ref: 'MyDGC350BD3F', - }, - AfterAllowTrafficHook: { - Ref: 'PostHookF2E49B30', - }, - }, - }, - }, ResourcePart.CompleteDefinition)); - - expect(stack).to(haveResource('AWS::IAM::Policy', { - PolicyName: 'MyDGServiceRoleDefaultPolicy65E8E1EA', - Roles: [{ - Ref: 'MyDGServiceRole5E94FD88', - }], - PolicyDocument: { - Statement: [{ - Action: 'lambda:InvokeFunction', - Resource: { - 'Fn::GetAtt': [ - 'PostHookF2E49B30', - 'Arn', - ], - }, - Effect: 'Allow', - }], - Version: '2012-10-17', - }, - })); - - test.done(); - }, - 'can disable rollback when alarm polling fails'(test: Test) { - const stack = new cdk.Stack(); - const application = new codedeploy.LambdaApplication(stack, 'MyApp'); - const alias = mockAlias(stack); - new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { - application, - alias, - postHook: mockFunction(stack, 'PostHook'), - deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, - ignorePollAlarmsFailure: true, - alarms: [new cloudwatch.Alarm(stack, 'Failures', { - metric: alias.metricErrors(), - comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, - threshold: 1, - evaluationPeriods: 1, - })], - }); - - expect(stack).to(haveResourceLike('AWS::CodeDeploy::DeploymentGroup', { - AlarmConfiguration: { - Alarms: [{ - Name: { - Ref: 'Failures8A3E1A2F', - }, - }], - Enabled: true, - IgnorePollAlarmFailure: true, - }, - })); - - test.done(); - }, - 'can disable rollback when deployment fails'(test: Test) { - const stack = new cdk.Stack(); - const application = new codedeploy.LambdaApplication(stack, 'MyApp'); - const alias = mockAlias(stack); - new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { - application, - alias, - postHook: mockFunction(stack, 'PostHook'), - deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, - autoRollback: { - failedDeployment: false, - }, - }); - - expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentGroup', { - ApplicationName: { - Ref: 'MyApp3CE31C26', - }, - ServiceRoleArn: { - 'Fn::GetAtt': [ - 'MyDGServiceRole5E94FD88', - 'Arn', - ], - }, - DeploymentConfigName: 'CodeDeployDefault.LambdaAllAtOnce', - DeploymentStyle: { - DeploymentOption: 'WITH_TRAFFIC_CONTROL', - DeploymentType: 'BLUE_GREEN', - }, - })); - - test.done(); - }, - 'can enable rollback when deployment stops'(test: Test) { - const stack = new cdk.Stack(); - const application = new codedeploy.LambdaApplication(stack, 'MyApp'); - const alias = mockAlias(stack); - new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { - application, - alias, - postHook: mockFunction(stack, 'PostHook'), - deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, - autoRollback: { - stoppedDeployment: true, - }, - }); - - expect(stack).to(haveResourceLike('AWS::CodeDeploy::DeploymentGroup', { - AutoRollbackConfiguration: { - Enabled: true, - Events: [ - 'DEPLOYMENT_FAILURE', - 'DEPLOYMENT_STOP_ON_REQUEST', - ], - }, - })); - - test.done(); - }, - 'can disable rollback when alarm in failure state'(test: Test) { - const stack = new cdk.Stack(); - const application = new codedeploy.LambdaApplication(stack, 'MyApp'); - const alias = mockAlias(stack); - new codedeploy.LambdaDeploymentGroup(stack, 'MyDG', { - application, - alias, - postHook: mockFunction(stack, 'PostHook'), - deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, - autoRollback: { - deploymentInAlarm: false, - }, - alarms: [new cloudwatch.Alarm(stack, 'Failures', { - metric: alias.metricErrors(), - comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, - threshold: 1, - evaluationPeriods: 1, - })], - }); - - expect(stack).to(haveResourceLike('AWS::CodeDeploy::DeploymentGroup', { - AutoRollbackConfiguration: { - Enabled: true, - Events: [ - 'DEPLOYMENT_FAILURE', - ], - }, - })); - - test.done(); - }, - - 'imported with fromLambdaDeploymentGroupAttributes': { - 'defaults the Deployment Config to Canary10Percent5Minutes'(test: Test) { - const stack = new cdk.Stack(); - - const lambdaApp = codedeploy.LambdaApplication.fromLambdaApplicationName(stack, 'LA', 'LambdaApplication'); - const importedGroup = codedeploy.LambdaDeploymentGroup.fromLambdaDeploymentGroupAttributes(stack, 'LDG', { - application: lambdaApp, - deploymentGroupName: 'LambdaDeploymentGroup', - }); - - test.equal(importedGroup.deploymentConfig, codedeploy.LambdaDeploymentConfig.CANARY_10PERCENT_5MINUTES); - - test.done(); - }, - }, - }, -}; diff --git a/packages/@aws-cdk/aws-codedeploy/test/server/deployment-config.test.ts b/packages/@aws-cdk/aws-codedeploy/test/server/deployment-config.test.ts new file mode 100644 index 0000000000000..2838bacfdc7fc --- /dev/null +++ b/packages/@aws-cdk/aws-codedeploy/test/server/deployment-config.test.ts @@ -0,0 +1,45 @@ +import '@aws-cdk/assert-internal/jest'; +import * as cdk from '@aws-cdk/core'; +import * as codedeploy from '../../lib'; + +/* eslint-disable quote-props */ + +describe('CodeDeploy DeploymentConfig', () => { + test('can be created by specifying only minHealthyHostCount', () => { + const stack = new cdk.Stack(); + + new codedeploy.ServerDeploymentConfig(stack, 'DeploymentConfig', { + minimumHealthyHosts: codedeploy.MinimumHealthyHosts.count(1), + }); + + expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentConfig', { + 'MinimumHealthyHosts': { + 'Type': 'HOST_COUNT', + 'Value': 1, + }, + }); + }); + + test('can be created by specifying only minHealthyHostPercentage', () => { + const stack = new cdk.Stack(); + + new codedeploy.ServerDeploymentConfig(stack, 'DeploymentConfig', { + minimumHealthyHosts: codedeploy.MinimumHealthyHosts.percentage(75), + }); + + expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentConfig', { + 'MinimumHealthyHosts': { + 'Type': 'FLEET_PERCENT', + 'Value': 75, + }, + }); + }); + + test('can be imported', () => { + const stack = new cdk.Stack(); + + const deploymentConfig = codedeploy.ServerDeploymentConfig.fromServerDeploymentConfigName(stack, 'MyDC', 'MyDC'); + + expect(deploymentConfig).not.toEqual(undefined); + }); +}); diff --git a/packages/@aws-cdk/aws-codedeploy/test/server/deployment-group.test.ts b/packages/@aws-cdk/aws-codedeploy/test/server/deployment-group.test.ts new file mode 100644 index 0000000000000..5ffb54ec655e2 --- /dev/null +++ b/packages/@aws-cdk/aws-codedeploy/test/server/deployment-group.test.ts @@ -0,0 +1,415 @@ +import { SynthUtils } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import * as autoscaling from '@aws-cdk/aws-autoscaling'; +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as lbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; +import * as cdk from '@aws-cdk/core'; +import * as codedeploy from '../../lib'; + +/* eslint-disable quote-props */ + +describe('CodeDeploy Server Deployment Group', () => { + test('can be created by explicitly passing an Application', () => { + const stack = new cdk.Stack(); + + const application = new codedeploy.ServerApplication(stack, 'MyApp'); + new codedeploy.ServerDeploymentGroup(stack, 'MyDG', { + application, + }); + + expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + 'ApplicationName': { + 'Ref': 'MyApp3CE31C26', + }, + }); + }); + + test('creating an application with physical name if needed', () => { + const stack = new cdk.Stack(undefined, undefined, { env: { account: '12345', region: 'us-test-1' } }); + const stack2 = new cdk.Stack(undefined, undefined, { env: { account: '12346', region: 'us-test-2' } }); + const serverDeploymentGroup = new codedeploy.ServerDeploymentGroup(stack, 'MyDG', { + deploymentGroupName: cdk.PhysicalName.GENERATE_IF_NEEDED, + }); + + new cdk.CfnOutput(stack2, 'Output', { + value: serverDeploymentGroup.application.applicationName, + }); + + expect(stack2).toHaveOutput({ + outputName: 'Output', + outputValue: 'defaultmydgapplication78dba0bb0c7580b32033', + }); + }); + + test('can be imported', () => { + const stack = new cdk.Stack(); + + const application = codedeploy.ServerApplication.fromServerApplicationName(stack, 'MyApp', 'MyApp'); + const deploymentGroup = codedeploy.ServerDeploymentGroup.fromServerDeploymentGroupAttributes(stack, 'MyDG', { + application, + deploymentGroupName: 'MyDG', + }); + + expect(deploymentGroup).not.toEqual(undefined); + }); + + test('uses good linux install agent script', () => { + const stack = new cdk.Stack(); + + const asg = new autoscaling.AutoScalingGroup(stack, 'ASG', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.STANDARD3, ec2.InstanceSize.SMALL), + machineImage: new ec2.AmazonLinuxImage(), + vpc: new ec2.Vpc(stack, 'VPC'), + }); + + new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { + autoScalingGroups: [asg], + installAgent: true, + }); + + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + 'UserData': { + 'Fn::Base64': { + 'Fn::Join': [ + '', + [ + '#!/bin/bash\nset +e\nPKG_CMD=`which yum 2>/dev/null`\nset -e\nif [ -z "$PKG_CMD" ]; then\nPKG_CMD=apt-get\nelse\nPKG_CMD=yum\nfi\n$PKG_CMD update -y\nset +e\n$PKG_CMD install -y ruby2.0\nRUBY2_INSTALL=$?\nset -e\nif [ $RUBY2_INSTALL -ne 0 ]; then\n$PKG_CMD install -y ruby\nfi\nAWS_CLI_PACKAGE_NAME=awscli\nif [ "$PKG_CMD" = "yum" ]; then\nAWS_CLI_PACKAGE_NAME=aws-cli\nfi\n$PKG_CMD install -y $AWS_CLI_PACKAGE_NAME\nTMP_DIR=`mktemp -d`\ncd $TMP_DIR\naws s3 cp s3://aws-codedeploy-', + { + 'Ref': 'AWS::Region', + }, + '/latest/install . --region ', + { + 'Ref': 'AWS::Region', + }, + '\nchmod +x ./install\n./install auto\nrm -fr $TMP_DIR', + ], + ], + }, + }, + }); + }); + + test('uses good windows install agent script', () => { + const stack = new cdk.Stack(); + + const asg = new autoscaling.AutoScalingGroup(stack, 'ASG', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.STANDARD3, ec2.InstanceSize.SMALL), + machineImage: new ec2.WindowsImage(ec2.WindowsVersion.WINDOWS_SERVER_2019_ENGLISH_FULL_BASE, {}), + vpc: new ec2.Vpc(stack, 'VPC'), + }); + + new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { + autoScalingGroups: [asg], + installAgent: true, + }); + + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + 'UserData': { + 'Fn::Base64': { + 'Fn::Join': [ + '', + [ + 'Set-Variable -Name TEMPDIR -Value (New-TemporaryFile).DirectoryName\naws s3 cp s3://aws-codedeploy-', + { + 'Ref': 'AWS::Region', + }, + '/latest/codedeploy-agent.msi $TEMPDIR\\codedeploy-agent.msi\ncd $TEMPDIR\n.\\codedeploy-agent.msi /quiet /l c:\\temp\\host-agent-install-log.txt', + ], + ], + }, + }, + }); + }); + + test('created with ASGs contains the ASG names', () => { + const stack = new cdk.Stack(); + + const asg = new autoscaling.AutoScalingGroup(stack, 'ASG', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.STANDARD3, ec2.InstanceSize.SMALL), + machineImage: new ec2.AmazonLinuxImage(), + vpc: new ec2.Vpc(stack, 'VPC'), + }); + + new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { + autoScalingGroups: [asg], + }); + + expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + 'AutoScalingGroups': [ + { + 'Ref': 'ASG46ED3070', + }, + ], + }); + }); + + test('created without ASGs but adding them later contains the ASG names', () => { + const stack = new cdk.Stack(); + + const asg = new autoscaling.AutoScalingGroup(stack, 'ASG', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.STANDARD3, ec2.InstanceSize.SMALL), + machineImage: new ec2.AmazonLinuxImage(), + vpc: new ec2.Vpc(stack, 'VPC'), + }); + + const deploymentGroup = new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup'); + deploymentGroup.addAutoScalingGroup(asg); + + expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + 'AutoScalingGroups': [ + { + 'Ref': 'ASG46ED3070', + }, + ], + }); + }); + + test('can be created with an ALB Target Group as the load balancer', () => { + const stack = new cdk.Stack(); + + const alb = new lbv2.ApplicationLoadBalancer(stack, 'ALB', { + vpc: new ec2.Vpc(stack, 'VPC'), + }); + const listener = alb.addListener('Listener', { protocol: lbv2.ApplicationProtocol.HTTP }); + const targetGroup = listener.addTargets('Fleet', { protocol: lbv2.ApplicationProtocol.HTTP }); + + new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { + loadBalancer: codedeploy.LoadBalancer.application(targetGroup), + }); + + expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + 'LoadBalancerInfo': { + 'TargetGroupInfoList': [ + { + 'Name': { + 'Fn::GetAtt': [ + 'ALBListenerFleetGroup008CEEE4', + 'TargetGroupName', + ], + }, + }, + ], + }, + 'DeploymentStyle': { + 'DeploymentOption': 'WITH_TRAFFIC_CONTROL', + }, + }); + }); + + test('can be created with an NLB Target Group as the load balancer', () => { + const stack = new cdk.Stack(); + + const nlb = new lbv2.NetworkLoadBalancer(stack, 'NLB', { + vpc: new ec2.Vpc(stack, 'VPC'), + }); + const listener = nlb.addListener('Listener', { port: 80 }); + const targetGroup = listener.addTargets('Fleet', { port: 80 }); + + new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { + loadBalancer: codedeploy.LoadBalancer.network(targetGroup), + }); + + expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + 'LoadBalancerInfo': { + 'TargetGroupInfoList': [ + { + 'Name': { + 'Fn::GetAtt': [ + 'NLBListenerFleetGroupB882EC86', + 'TargetGroupName', + ], + }, + }, + ], + }, + 'DeploymentStyle': { + 'DeploymentOption': 'WITH_TRAFFIC_CONTROL', + }, + }); + }); + + test('can be created with a single EC2 instance tag set with a single or no value', () => { + const stack = new cdk.Stack(); + + new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { + ec2InstanceTags: new codedeploy.InstanceTagSet( + { + 'some-key': ['some-value'], + 'other-key': [], + }, + ), + }); + + expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + 'Ec2TagSet': { + 'Ec2TagSetList': [ + { + 'Ec2TagGroup': [ + { + 'Key': 'some-key', + 'Value': 'some-value', + 'Type': 'KEY_AND_VALUE', + }, + { + 'Key': 'other-key', + 'Type': 'KEY_ONLY', + }, + ], + }, + ], + }, + }); + }); + + test('can be created with two on-premise instance tag sets with multiple values or without a key', () => { + const stack = new cdk.Stack(); + + new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { + onPremiseInstanceTags: new codedeploy.InstanceTagSet( + { + 'some-key': ['some-value', 'another-value'], + }, + { + '': ['keyless-value'], + }, + ), + }); + + expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + 'OnPremisesTagSet': { + 'OnPremisesTagSetList': [ + { + 'OnPremisesTagGroup': [ + { + 'Key': 'some-key', + 'Value': 'some-value', + 'Type': 'KEY_AND_VALUE', + }, + { + 'Key': 'some-key', + 'Value': 'another-value', + 'Type': 'KEY_AND_VALUE', + }, + ], + }, + { + 'OnPremisesTagGroup': [ + { + 'Value': 'keyless-value', + 'Type': 'VALUE_ONLY', + }, + ], + }, + ], + }, + }); + }); + + test('cannot be created with an instance tag set containing a keyless, valueless filter', () => { + const stack = new cdk.Stack(); + + expect(() => { + new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { + onPremiseInstanceTags: new codedeploy.InstanceTagSet({ + '': [], + }), + }); + }).toThrow(); + }); + + test('cannot be created with an instance tag set containing 4 instance tag groups', () => { + const stack = new cdk.Stack(); + + expect(() => { + new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { + onPremiseInstanceTags: new codedeploy.InstanceTagSet({}, {}, {}, {}), + }); + }).toThrow(/3/); + }); + + test('can have alarms added to it after being created', () => { + const stack = new cdk.Stack(); + + const alarm = new cloudwatch.Alarm(stack, 'Alarm1', { + metric: new cloudwatch.Metric({ + metricName: 'Errors', + namespace: 'my.namespace', + }), + threshold: 1, + evaluationPeriods: 1, + }); + + const deploymentGroup = new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup'); + deploymentGroup.addAlarm(alarm); + + expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + 'AlarmConfiguration': { + 'Alarms': [ + { + 'Name': { + 'Ref': 'Alarm1F9009D71', + }, + }, + ], + 'Enabled': true, + }, + }); + }); + + test('only automatically rolls back failed deployments by default', () => { + const stack = new cdk.Stack(); + + new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup'); + + expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + 'AutoRollbackConfiguration': { + 'Enabled': true, + 'Events': [ + 'DEPLOYMENT_FAILURE', + ], + }, + }); + }); + + test('rolls back alarmed deployments if at least one alarm has been added', () => { + const stack = new cdk.Stack(); + + const alarm = new cloudwatch.Alarm(stack, 'Alarm1', { + metric: new cloudwatch.Metric({ + metricName: 'Errors', + namespace: 'my.namespace', + }), + threshold: 1, + evaluationPeriods: 1, + }); + + const deploymentGroup = new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { + autoRollback: { + failedDeployment: false, + }, + }); + deploymentGroup.addAlarm(alarm); + + expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + 'AutoRollbackConfiguration': { + 'Enabled': true, + 'Events': [ + 'DEPLOYMENT_STOP_ON_ALARM', + ], + }, + }); + }); + + test('setting to roll back on alarms without providing any results in an exception', () => { + const stack = new cdk.Stack(); + + new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { + autoRollback: { + deploymentInAlarm: true, + }, + }); + + expect(() => SynthUtils.toCloudFormation(stack)).toThrow(/deploymentInAlarm/); + }); +}); diff --git a/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-config.ts deleted file mode 100644 index 0108122030276..0000000000000 --- a/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-config.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; -import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import * as codedeploy from '../../lib'; - -/* eslint-disable quote-props */ - -export = { - 'CodeDeploy DeploymentConfig': { - 'can be created by specifying only minHealthyHostCount'(test: Test) { - const stack = new cdk.Stack(); - - new codedeploy.ServerDeploymentConfig(stack, 'DeploymentConfig', { - minimumHealthyHosts: codedeploy.MinimumHealthyHosts.count(1), - }); - - expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentConfig', { - 'MinimumHealthyHosts': { - 'Type': 'HOST_COUNT', - 'Value': 1, - }, - })); - - test.done(); - }, - - 'can be created by specifying only minHealthyHostPercentage'(test: Test) { - const stack = new cdk.Stack(); - - new codedeploy.ServerDeploymentConfig(stack, 'DeploymentConfig', { - minimumHealthyHosts: codedeploy.MinimumHealthyHosts.percentage(75), - }); - - expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentConfig', { - 'MinimumHealthyHosts': { - 'Type': 'FLEET_PERCENT', - 'Value': 75, - }, - })); - - test.done(); - }, - - 'can be imported'(test: Test) { - const stack = new cdk.Stack(); - - const deploymentConfig = codedeploy.ServerDeploymentConfig.fromServerDeploymentConfigName(stack, 'MyDC', 'MyDC'); - - test.notEqual(deploymentConfig, undefined); - - test.done(); - }, - }, -}; diff --git a/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-group.ts deleted file mode 100644 index 4bd4da6192380..0000000000000 --- a/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-group.ts +++ /dev/null @@ -1,450 +0,0 @@ -import { expect, haveOutput, haveResource, SynthUtils } from '@aws-cdk/assert-internal'; -import * as autoscaling from '@aws-cdk/aws-autoscaling'; -import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; -import * as ec2 from '@aws-cdk/aws-ec2'; -import * as lbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; -import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import * as codedeploy from '../../lib'; - -/* eslint-disable quote-props */ - -export = { - 'CodeDeploy Server Deployment Group': { - 'can be created by explicitly passing an Application'(test: Test) { - const stack = new cdk.Stack(); - - const application = new codedeploy.ServerApplication(stack, 'MyApp'); - new codedeploy.ServerDeploymentGroup(stack, 'MyDG', { - application, - }); - - expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentGroup', { - 'ApplicationName': { - 'Ref': 'MyApp3CE31C26', - }, - })); - - test.done(); - }, - - 'creating an application with physical name if needed'(test: Test) { - const stack = new cdk.Stack(undefined, undefined, { env: { account: '12345', region: 'us-test-1' } }); - const stack2 = new cdk.Stack(undefined, undefined, { env: { account: '12346', region: 'us-test-2' } }); - const serverDeploymentGroup = new codedeploy.ServerDeploymentGroup(stack, 'MyDG', { - deploymentGroupName: cdk.PhysicalName.GENERATE_IF_NEEDED, - }); - - new cdk.CfnOutput(stack2, 'Output', { - value: serverDeploymentGroup.application.applicationName, - }); - - expect(stack2).to(haveOutput({ - outputName: 'Output', - outputValue: 'defaultmydgapplication78dba0bb0c7580b32033', - })); - - test.done(); - }, - - 'can be imported'(test: Test) { - const stack = new cdk.Stack(); - - const application = codedeploy.ServerApplication.fromServerApplicationName(stack, 'MyApp', 'MyApp'); - const deploymentGroup = codedeploy.ServerDeploymentGroup.fromServerDeploymentGroupAttributes(stack, 'MyDG', { - application, - deploymentGroupName: 'MyDG', - }); - - test.notEqual(deploymentGroup, undefined); - - test.done(); - }, - - 'uses good linux install agent script'(test: Test) { - const stack = new cdk.Stack(); - - const asg = new autoscaling.AutoScalingGroup(stack, 'ASG', { - instanceType: ec2.InstanceType.of(ec2.InstanceClass.STANDARD3, ec2.InstanceSize.SMALL), - machineImage: new ec2.AmazonLinuxImage(), - vpc: new ec2.Vpc(stack, 'VPC'), - }); - - new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { - autoScalingGroups: [asg], - installAgent: true, - }); - - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { - 'UserData': { - 'Fn::Base64': { - 'Fn::Join': [ - '', - [ - '#!/bin/bash\nset +e\nPKG_CMD=`which yum 2>/dev/null`\nset -e\nif [ -z "$PKG_CMD" ]; then\nPKG_CMD=apt-get\nelse\nPKG_CMD=yum\nfi\n$PKG_CMD update -y\nset +e\n$PKG_CMD install -y ruby2.0\nRUBY2_INSTALL=$?\nset -e\nif [ $RUBY2_INSTALL -ne 0 ]; then\n$PKG_CMD install -y ruby\nfi\nAWS_CLI_PACKAGE_NAME=awscli\nif [ "$PKG_CMD" = "yum" ]; then\nAWS_CLI_PACKAGE_NAME=aws-cli\nfi\n$PKG_CMD install -y $AWS_CLI_PACKAGE_NAME\nTMP_DIR=`mktemp -d`\ncd $TMP_DIR\naws s3 cp s3://aws-codedeploy-', - { - 'Ref': 'AWS::Region', - }, - '/latest/install . --region ', - { - 'Ref': 'AWS::Region', - }, - '\nchmod +x ./install\n./install auto\nrm -fr $TMP_DIR', - ], - ], - }, - }, - })); - - test.done(); - }, - - 'uses good windows install agent script'(test: Test) { - const stack = new cdk.Stack(); - - const asg = new autoscaling.AutoScalingGroup(stack, 'ASG', { - instanceType: ec2.InstanceType.of(ec2.InstanceClass.STANDARD3, ec2.InstanceSize.SMALL), - machineImage: new ec2.WindowsImage(ec2.WindowsVersion.WINDOWS_SERVER_2019_ENGLISH_FULL_BASE, {}), - vpc: new ec2.Vpc(stack, 'VPC'), - }); - - new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { - autoScalingGroups: [asg], - installAgent: true, - }); - - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { - 'UserData': { - 'Fn::Base64': { - 'Fn::Join': [ - '', - [ - 'Set-Variable -Name TEMPDIR -Value (New-TemporaryFile).DirectoryName\naws s3 cp s3://aws-codedeploy-', - { - 'Ref': 'AWS::Region', - }, - '/latest/codedeploy-agent.msi $TEMPDIR\\codedeploy-agent.msi\ncd $TEMPDIR\n.\\codedeploy-agent.msi /quiet /l c:\\temp\\host-agent-install-log.txt', - ], - ], - }, - }, - })); - - test.done(); - }, - - 'created with ASGs contains the ASG names'(test: Test) { - const stack = new cdk.Stack(); - - const asg = new autoscaling.AutoScalingGroup(stack, 'ASG', { - instanceType: ec2.InstanceType.of(ec2.InstanceClass.STANDARD3, ec2.InstanceSize.SMALL), - machineImage: new ec2.AmazonLinuxImage(), - vpc: new ec2.Vpc(stack, 'VPC'), - }); - - new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { - autoScalingGroups: [asg], - }); - - expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentGroup', { - 'AutoScalingGroups': [ - { - 'Ref': 'ASG46ED3070', - }, - ], - })); - - test.done(); - }, - - 'created without ASGs but adding them later contains the ASG names'(test: Test) { - const stack = new cdk.Stack(); - - const asg = new autoscaling.AutoScalingGroup(stack, 'ASG', { - instanceType: ec2.InstanceType.of(ec2.InstanceClass.STANDARD3, ec2.InstanceSize.SMALL), - machineImage: new ec2.AmazonLinuxImage(), - vpc: new ec2.Vpc(stack, 'VPC'), - }); - - const deploymentGroup = new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup'); - deploymentGroup.addAutoScalingGroup(asg); - - expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentGroup', { - 'AutoScalingGroups': [ - { - 'Ref': 'ASG46ED3070', - }, - ], - })); - - test.done(); - }, - - 'can be created with an ALB Target Group as the load balancer'(test: Test) { - const stack = new cdk.Stack(); - - const alb = new lbv2.ApplicationLoadBalancer(stack, 'ALB', { - vpc: new ec2.Vpc(stack, 'VPC'), - }); - const listener = alb.addListener('Listener', { protocol: lbv2.ApplicationProtocol.HTTP }); - const targetGroup = listener.addTargets('Fleet', { protocol: lbv2.ApplicationProtocol.HTTP }); - - new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { - loadBalancer: codedeploy.LoadBalancer.application(targetGroup), - }); - - expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentGroup', { - 'LoadBalancerInfo': { - 'TargetGroupInfoList': [ - { - 'Name': { - 'Fn::GetAtt': [ - 'ALBListenerFleetGroup008CEEE4', - 'TargetGroupName', - ], - }, - }, - ], - }, - 'DeploymentStyle': { - 'DeploymentOption': 'WITH_TRAFFIC_CONTROL', - }, - })); - - test.done(); - }, - - 'can be created with an NLB Target Group as the load balancer'(test: Test) { - const stack = new cdk.Stack(); - - const nlb = new lbv2.NetworkLoadBalancer(stack, 'NLB', { - vpc: new ec2.Vpc(stack, 'VPC'), - }); - const listener = nlb.addListener('Listener', { port: 80 }); - const targetGroup = listener.addTargets('Fleet', { port: 80 }); - - new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { - loadBalancer: codedeploy.LoadBalancer.network(targetGroup), - }); - - expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentGroup', { - 'LoadBalancerInfo': { - 'TargetGroupInfoList': [ - { - 'Name': { - 'Fn::GetAtt': [ - 'NLBListenerFleetGroupB882EC86', - 'TargetGroupName', - ], - }, - }, - ], - }, - 'DeploymentStyle': { - 'DeploymentOption': 'WITH_TRAFFIC_CONTROL', - }, - })); - - test.done(); - }, - - 'can be created with a single EC2 instance tag set with a single or no value'(test: Test) { - const stack = new cdk.Stack(); - - new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { - ec2InstanceTags: new codedeploy.InstanceTagSet( - { - 'some-key': ['some-value'], - 'other-key': [], - }, - ), - }); - - expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentGroup', { - 'Ec2TagSet': { - 'Ec2TagSetList': [ - { - 'Ec2TagGroup': [ - { - 'Key': 'some-key', - 'Value': 'some-value', - 'Type': 'KEY_AND_VALUE', - }, - { - 'Key': 'other-key', - 'Type': 'KEY_ONLY', - }, - ], - }, - ], - }, - })); - - test.done(); - }, - - 'can be created with two on-premise instance tag sets with multiple values or without a key'(test: Test) { - const stack = new cdk.Stack(); - - new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { - onPremiseInstanceTags: new codedeploy.InstanceTagSet( - { - 'some-key': ['some-value', 'another-value'], - }, - { - '': ['keyless-value'], - }, - ), - }); - - expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentGroup', { - 'OnPremisesTagSet': { - 'OnPremisesTagSetList': [ - { - 'OnPremisesTagGroup': [ - { - 'Key': 'some-key', - 'Value': 'some-value', - 'Type': 'KEY_AND_VALUE', - }, - { - 'Key': 'some-key', - 'Value': 'another-value', - 'Type': 'KEY_AND_VALUE', - }, - ], - }, - { - 'OnPremisesTagGroup': [ - { - 'Value': 'keyless-value', - 'Type': 'VALUE_ONLY', - }, - ], - }, - ], - }, - })); - - test.done(); - }, - - 'cannot be created with an instance tag set containing a keyless, valueless filter'(test: Test) { - const stack = new cdk.Stack(); - - test.throws(() => { - new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { - onPremiseInstanceTags: new codedeploy.InstanceTagSet({ - '': [], - }), - }); - }); - - test.done(); - }, - - 'cannot be created with an instance tag set containing 4 instance tag groups'(test: Test) { - const stack = new cdk.Stack(); - - test.throws(() => { - new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { - onPremiseInstanceTags: new codedeploy.InstanceTagSet({}, {}, {}, {}), - }); - }, /3/); - - test.done(); - }, - - 'can have alarms added to it after being created'(test: Test) { - const stack = new cdk.Stack(); - - const alarm = new cloudwatch.Alarm(stack, 'Alarm1', { - metric: new cloudwatch.Metric({ - metricName: 'Errors', - namespace: 'my.namespace', - }), - threshold: 1, - evaluationPeriods: 1, - }); - - const deploymentGroup = new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup'); - deploymentGroup.addAlarm(alarm); - - expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentGroup', { - 'AlarmConfiguration': { - 'Alarms': [ - { - 'Name': { - 'Ref': 'Alarm1F9009D71', - }, - }, - ], - 'Enabled': true, - }, - })); - - test.done(); - }, - - 'only automatically rolls back failed deployments by default'(test: Test) { - const stack = new cdk.Stack(); - - new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup'); - - expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentGroup', { - 'AutoRollbackConfiguration': { - 'Enabled': true, - 'Events': [ - 'DEPLOYMENT_FAILURE', - ], - }, - })); - - test.done(); - }, - - 'rolls back alarmed deployments if at least one alarm has been added'(test: Test) { - const stack = new cdk.Stack(); - - const alarm = new cloudwatch.Alarm(stack, 'Alarm1', { - metric: new cloudwatch.Metric({ - metricName: 'Errors', - namespace: 'my.namespace', - }), - threshold: 1, - evaluationPeriods: 1, - }); - - const deploymentGroup = new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { - autoRollback: { - failedDeployment: false, - }, - }); - deploymentGroup.addAlarm(alarm); - - expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentGroup', { - 'AutoRollbackConfiguration': { - 'Enabled': true, - 'Events': [ - 'DEPLOYMENT_STOP_ON_ALARM', - ], - }, - })); - - test.done(); - }, - - 'setting to roll back on alarms without providing any results in an exception'(test: Test) { - const stack = new cdk.Stack(); - - new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { - autoRollback: { - deploymentInAlarm: true, - }, - }); - - test.throws(() => SynthUtils.toCloudFormation(stack), /deploymentInAlarm/); - test.done(); - }, - }, -}; diff --git a/packages/@aws-cdk/aws-config/.gitignore b/packages/@aws-cdk/aws-config/.gitignore index 018c65919d67c..457563a185b1e 100644 --- a/packages/@aws-cdk/aws-config/.gitignore +++ b/packages/@aws-cdk/aws-config/.gitignore @@ -15,4 +15,6 @@ nyc.config.js *.snk !.eslintrc.js -junit.xml \ No newline at end of file +junit.xml + +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-config/.npmignore b/packages/@aws-cdk/aws-config/.npmignore index 9a032ae80868c..e8acf10a468a1 100644 --- a/packages/@aws-cdk/aws-config/.npmignore +++ b/packages/@aws-cdk/aws-config/.npmignore @@ -24,4 +24,5 @@ tsconfig.json **/cdk.out junit.xml test/ -!*.lit.ts \ No newline at end of file +!*.lit.ts +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-config/jest.config.js b/packages/@aws-cdk/aws-config/jest.config.js new file mode 100644 index 0000000000000..f5d5c4c8ad18f --- /dev/null +++ b/packages/@aws-cdk/aws-config/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-config/package.json b/packages/@aws-cdk/aws-config/package.json index c03852d4321e1..ee4205b4d9241 100644 --- a/packages/@aws-cdk/aws-config/package.json +++ b/packages/@aws-cdk/aws-config/package.json @@ -55,6 +55,7 @@ }, "cdk-build": { "cloudformation": "AWS::Config", + "jest": true, "env": { "AWSLINT_BASE_CONSTRUCT": true } @@ -73,11 +74,11 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/aws-events-targets": "0.0.0", - "@types/nodeunit": "^0.0.32", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nodeunit": "^0.11.3", + "jest": "^26.6.3", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-config/test/test.managed-rules.ts b/packages/@aws-cdk/aws-config/test/managed-rules.test.ts similarity index 67% rename from packages/@aws-cdk/aws-config/test/test.managed-rules.ts rename to packages/@aws-cdk/aws-config/test/managed-rules.test.ts index 2c6c9c764fcdd..98dd3fbd34262 100644 --- a/packages/@aws-cdk/aws-config/test/test.managed-rules.ts +++ b/packages/@aws-cdk/aws-config/test/managed-rules.test.ts @@ -1,11 +1,10 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as sns from '@aws-cdk/aws-sns'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as config from '../lib'; -export = { - 'access keys rotated'(test: Test) { +describe('access keys', () => { + test('rotated', () => { // GIVEN const stack = new cdk.Stack(); @@ -13,17 +12,38 @@ export = { new config.AccessKeysRotated(stack, 'AccessKeys'); // THEN - expect(stack).to(haveResource('AWS::Config::ConfigRule', { + expect(stack).toHaveResource('AWS::Config::ConfigRule', { Source: { Owner: 'AWS', SourceIdentifier: 'ACCESS_KEYS_ROTATED', }, - })); + }); + }); + + test('rotated with max age', () => { + // GIVEN + const stack = new cdk.Stack(); - test.done(); - }, + // WHEN + new config.AccessKeysRotated(stack, 'AccessKeys', { + maxAge: cdk.Duration.days(1), + }); - 'cloudformation stack drift detection check'(test: Test) { + // THEN + expect(stack).toHaveResource('AWS::Config::ConfigRule', { + Source: { + Owner: 'AWS', + SourceIdentifier: 'ACCESS_KEYS_ROTATED', + }, + InputParameters: { + maxAccessKeyAge: 1, + }, + }); + }); +}); + +describe('cloudformation stack', () => { + test('drift detection check', () => { // GIVEN const stack = new cdk.Stack(); @@ -31,7 +51,7 @@ export = { new config.CloudFormationStackDriftDetectionCheck(stack, 'Drift'); // THEN - expect(stack).to(haveResource('AWS::Config::ConfigRule', { + expect(stack).toHaveResource('AWS::Config::ConfigRule', { Source: { Owner: 'AWS', SourceIdentifier: 'CLOUDFORMATION_STACK_DRIFT_DETECTION_CHECK', @@ -49,9 +69,9 @@ export = { 'AWS::CloudFormation::Stack', ], }, - })); + }); - expect(stack).to(haveResource('AWS::IAM::Role', { + expect(stack).toHaveResource('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -78,12 +98,10 @@ export = { ], }, ], - })); - - test.done(); - }, + }); + }); - 'cloudformation stack notification check'(test: Test) { + test('notification check', () => { // GIVEN const stack = new cdk.Stack(); const topic1 = new sns.Topic(stack, 'AllowedTopic1'); @@ -95,7 +113,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Config::ConfigRule', { + expect(stack).toHaveResource('AWS::Config::ConfigRule', { Source: { Owner: 'AWS', SourceIdentifier: 'CLOUDFORMATION_STACK_NOTIFICATION_CHECK', @@ -113,25 +131,23 @@ export = { 'AWS::CloudFormation::Stack', ], }, - })); - - test.done(); - }, + }); + }); - 'cloudformation stack notification check throws with more than 5 topics'(test: Test) { + test('notification check throws with more than 5 topics', () => { // GIVEN const stack = new cdk.Stack(); const topic = new sns.Topic(stack, 'AllowedTopic1'); // THEN - test.throws(() => new config.CloudFormationStackNotificationCheck(stack, 'Notification', { + expect(() => new config.CloudFormationStackNotificationCheck(stack, 'Notification', { topics: [topic, topic, topic, topic, topic, topic], - }), /5 topics/); - - test.done(); - }, + })).toThrow(/5 topics/); + }); +}); - 'ec2 instance profile attached check'(test: Test) { +describe('ec2 instance', () => { + test('profile attached check', () => { // GIVEN const stack = new cdk.Stack(); @@ -141,13 +157,11 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Config::ConfigRule', { + expect(stack).toHaveResource('AWS::Config::ConfigRule', { Source: { Owner: 'AWS', SourceIdentifier: config.ManagedRuleIdentifiers.EC2_INSTANCE_PROFILE_ATTACHED, }, - })); - - test.done(); - }, -}; + }); + }); +}); diff --git a/packages/@aws-cdk/aws-config/test/test.rule.ts b/packages/@aws-cdk/aws-config/test/rule.test.ts similarity index 79% rename from packages/@aws-cdk/aws-config/test/test.rule.ts rename to packages/@aws-cdk/aws-config/test/rule.test.ts index ef2398ffae689..77599b8d95308 100644 --- a/packages/@aws-cdk/aws-config/test/test.rule.ts +++ b/packages/@aws-cdk/aws-config/test/rule.test.ts @@ -1,12 +1,12 @@ -import { expect, haveResource, ResourcePart } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { ResourcePart } from '@aws-cdk/assert-internal'; import * as targets from '@aws-cdk/aws-events-targets'; import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as config from '../lib'; -export = { - 'create a managed rule'(test: Test) { +describe('rule', () => { + test('create a managed rule', () => { // GIVEN const stack = new cdk.Stack(); @@ -22,7 +22,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Config::ConfigRule', { + expect(stack).toHaveResource('AWS::Config::ConfigRule', { Source: { Owner: 'AWS', SourceIdentifier: 'AWS_SUPER_COOL', @@ -33,12 +33,10 @@ export = { key: 'value', }, MaximumExecutionFrequency: 'Three_Hours', - })); - - test.done(); - }, + }); + }); - 'create a custom rule'(test: Test) { + test('create a custom rule', () => { // GIVEN const stack = new cdk.Stack(); const fn = new lambda.Function(stack, 'Function', { @@ -61,7 +59,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Config::ConfigRule', { + expect(stack).toHaveResource('AWS::Config::ConfigRule', { Properties: { Source: { Owner: 'CUSTOM_LAMBDA', @@ -99,13 +97,13 @@ export = { 'Function76856677', 'FunctionServiceRole675BB04A', ], - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - expect(stack).to(haveResource('AWS::Lambda::Permission', { + expect(stack).toHaveResource('AWS::Lambda::Permission', { Principal: 'config.amazonaws.com', - })); + }); - expect(stack).to(haveResource('AWS::IAM::Role', { + expect(stack).toHaveResource('AWS::IAM::Role', { ManagedPolicyArns: [ { 'Fn::Join': [ @@ -132,12 +130,10 @@ export = { ], }, ], - })); - - test.done(); - }, + }); + }); - 'scope to resource'(test: Test) { + test('scope to resource', () => { // GIVEN const stack = new cdk.Stack(); @@ -148,19 +144,17 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Config::ConfigRule', { + expect(stack).toHaveResource('AWS::Config::ConfigRule', { Scope: { ComplianceResourceId: 'i-1234', ComplianceResourceTypes: [ 'AWS::EC2::Instance', ], }, - })); - - test.done(); - }, + }); + }); - 'scope to resources'(test: Test) { + test('scope to resources', () => { // GIVEN const stack = new cdk.Stack(); @@ -171,19 +165,17 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Config::ConfigRule', { + expect(stack).toHaveResource('AWS::Config::ConfigRule', { Scope: { ComplianceResourceTypes: [ 'AWS::S3::Bucket', 'AWS::CloudFormation::Stack', ], }, - })); - - test.done(); - }, + }); + }), - 'scope to tag'(test: Test) { + test('scope to tag', () => { // GIVEN const stack = new cdk.Stack(); @@ -194,17 +186,15 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Config::ConfigRule', { + expect(stack).toHaveResource('AWS::Config::ConfigRule', { Scope: { TagKey: 'key', TagValue: 'value', }, - })); - - test.done(); - }, + }); + }), - 'allows scoping a custom rule without configurationChanges enabled'(test: Test) { + test('allows scoping a custom rule without configurationChanges enabled', () => { // GIVEN const stack = new cdk.Stack(); const fn = new lambda.Function(stack, 'Function', { @@ -214,16 +204,14 @@ export = { }); // THEN - test.doesNotThrow(() => new config.CustomRule(stack, 'Rule', { + expect(() => new config.CustomRule(stack, 'Rule', { lambdaFunction: fn, periodic: true, ruleScope: config.RuleScope.fromResources([config.ResourceType.of('resource')]), - })); - - test.done(); - }, + })).not.toThrow(); + }), - 'throws when both configurationChanges and periodic are falsy'(test: Test) { + test('throws when both configurationChanges and periodic are falsy', () => { // GIVEN const stack = new cdk.Stack(); const fn = new lambda.Function(stack, 'Function', { @@ -233,14 +221,12 @@ export = { }); // THEN - test.throws(() => new config.CustomRule(stack, 'Rule', { + expect(() => new config.CustomRule(stack, 'Rule', { lambdaFunction: fn, - }), /`configurationChanges`.*`periodic`/); + })).toThrow(/`configurationChanges`.*`periodic`/); + }), - test.done(); - }, - - 'on compliance change event'(test: Test) { + test('on compliance change event', () => { // GIVEN const stack = new cdk.Stack(); const rule = new config.ManagedRule(stack, 'Rule', { @@ -258,7 +244,7 @@ export = { target: new targets.LambdaFunction(fn), }); - expect(stack).to(haveResource('AWS::Events::Rule', { + expect(stack).toHaveResource('AWS::Events::Rule', { EventPattern: { 'source': [ 'aws.config', @@ -274,8 +260,6 @@ export = { 'Config Rules Compliance Change', ], }, - })); - - test.done(); - }, -}; + }); + }); +}); diff --git a/packages/@aws-cdk/aws-dynamodb-global/.gitignore b/packages/@aws-cdk/aws-dynamodb-global/.gitignore index ad9e57500b0bc..0fae327d5d4db 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/.gitignore +++ b/packages/@aws-cdk/aws-dynamodb-global/.gitignore @@ -18,4 +18,5 @@ nyc.config.js !test/test.lambda.handler.js !.eslintrc.js -junit.xml \ No newline at end of file +junit.xml +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-dynamodb-global/.npmignore b/packages/@aws-cdk/aws-dynamodb-global/.npmignore index 3fd2a6ce320a0..03d06dbd442ee 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/.npmignore +++ b/packages/@aws-cdk/aws-dynamodb-global/.npmignore @@ -25,4 +25,5 @@ tsconfig.json **/cdk.out junit.xml test/ -!*.lit.ts \ No newline at end of file +!*.lit.ts +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-dynamodb-global/jest.config.js b/packages/@aws-cdk/aws-dynamodb-global/jest.config.js new file mode 100644 index 0000000000000..f5d5c4c8ad18f --- /dev/null +++ b/packages/@aws-cdk/aws-dynamodb-global/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-dynamodb-global/package.json b/packages/@aws-cdk/aws-dynamodb-global/package.json index 77e92e5d5aa17..4f55047bb9b95 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/package.json +++ b/packages/@aws-cdk/aws-dynamodb-global/package.json @@ -41,6 +41,9 @@ }, "projectReferences": true }, + "cdk-build": { + "jest": true + }, "keywords": [ "aws", "cdk", @@ -56,10 +59,10 @@ "constructs": "^3.3.69" }, "devDependencies": { - "@types/nodeunit": "^0.0.32", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "nodeunit": "^0.11.3", + "jest": "^26.6.3", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-dynamodb-global/test/dynamodb-global.test.ts b/packages/@aws-cdk/aws-dynamodb-global/test/dynamodb-global.test.ts new file mode 100644 index 0000000000000..ed84a3f8a6145 --- /dev/null +++ b/packages/@aws-cdk/aws-dynamodb-global/test/dynamodb-global.test.ts @@ -0,0 +1,116 @@ +import '@aws-cdk/assert-internal/jest'; +import { Attribute, AttributeType, StreamViewType, Table } from '@aws-cdk/aws-dynamodb'; +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { GlobalTable, GlobalTableProps } from '../lib'; + +/* eslint-disable quote-props */ + +// CDK parameters +const CONSTRUCT_NAME = 'aws-cdk-dynamodb-global'; + +// DynamoDB table parameters +const TABLE_NAME = 'GlobalTable'; +const TABLE_PARTITION_KEY: Attribute = { name: 'hashKey', type: AttributeType.STRING }; + +const STACK_PROPS: GlobalTableProps = { + partitionKey: TABLE_PARTITION_KEY, + tableName: TABLE_NAME, + regions: ['us-east-1', 'us-east-2', 'us-west-2'], +}; + +describe('Default Global DynamoDB stack', () => { + test('global dynamo', () => { + const stack = new Stack(); + new GlobalTable(stack, CONSTRUCT_NAME, STACK_PROPS); + const topStack = stack.node.findChild(CONSTRUCT_NAME) as Stack; + for ( const reg of STACK_PROPS.regions ) { + const tableStack = topStack.node.findChild(CONSTRUCT_NAME + '-' + reg) as Stack; + expect(tableStack).toHaveResource('AWS::DynamoDB::Table', { + 'KeySchema': [ + { + 'AttributeName': 'hashKey', + 'KeyType': 'HASH', + }, + ], + 'AttributeDefinitions': [ + { + 'AttributeName': 'hashKey', + 'AttributeType': 'S', + }, + ], + 'StreamSpecification': { + 'StreamViewType': 'NEW_AND_OLD_IMAGES', + }, + 'TableName': 'GlobalTable', + }); + } + const customResourceStack = stack.node.findChild(CONSTRUCT_NAME + '-CustomResource') as Stack; + expect(customResourceStack).toHaveResource('AWS::Lambda::Function', { + Description: 'Lambda to make DynamoDB a global table', + Handler: 'index.handler', + Timeout: 300, + }); + expect(customResourceStack).toHaveResource('AWS::CloudFormation::CustomResource', { + Regions: STACK_PROPS.regions, + ResourceType: 'Custom::DynamoGlobalTableCoordinator', + TableName: TABLE_NAME, + }); + }); +}); + +test('GlobalTable generated stacks inherit their account from the parent stack', () => { + const app = new App({ context: { '@aws-cdk/core:stackRelativeExports': true } }); + const stack = new Stack(app, 'GlobalTableStack', { env: { account: '123456789012', region: 'us-east-1' } }); + + const globalTable = new GlobalTable(stack, CONSTRUCT_NAME, { + tableName: TABLE_NAME, + partitionKey: TABLE_PARTITION_KEY, + regions: ['us-east-1', 'us-west-2'], + stream: StreamViewType.NEW_AND_OLD_IMAGES, + }); + + new CfnOutput(stack, 'DynamoDbOutput', { + // this works, because both `stack` and `regionTables[0]` stack are in the same account & region + value: globalTable.regionalTables[0].tableStreamArn!, + }); + + expect(stack).toMatchTemplate({ + 'Outputs': { + 'DynamoDbOutput': { + 'Value': { + 'Fn::ImportValue': 'GlobalTableStackawscdkdynamodbglobalawscdkdynamodbglobaluseast19C1C8A14:ExportsOutputFnGetAttawscdkdynamodbglobalGlobalTableuseast1FC03DD69StreamArn9CE585ED', + }, + }, + }, + }); +}); + +describe('Enforce StreamSpecification', () => { + test('global dynamo should only allow NEW_AND_OLD_IMAGES', () => { + const stack = new Stack(); + + expect(() => { + new GlobalTable(stack, CONSTRUCT_NAME, { + tableName: TABLE_NAME, + stream: StreamViewType.KEYS_ONLY, + partitionKey: TABLE_PARTITION_KEY, + regions: ['us-east-1', 'us-east-2', 'us-west-2'], + }); + }).toThrow(/dynamoProps.stream MUST be set to dynamodb.StreamViewType.NEW_AND_OLD_IMAGES/); + }); +}); + +describe('Check getting tables', () => { + test('global dynamo should only allow NEW_AND_OLD_IMAGES', () => { + const stack = new Stack(); + const regTables = new GlobalTable(stack, CONSTRUCT_NAME, { + tableName: TABLE_NAME, + partitionKey: TABLE_PARTITION_KEY, + regions: ['us-east-1', 'us-east-2', 'us-west-2'], + }); + expect(regTables.regionalTables.length).toEqual(3); + for (const table of regTables.regionalTables) { + expect(table).toBeInstanceOf(Table); + } + }); +}); diff --git a/packages/@aws-cdk/aws-dynamodb-global/test/test.dynamodb.global.ts b/packages/@aws-cdk/aws-dynamodb-global/test/test.dynamodb.global.ts deleted file mode 100644 index 4a4c7596b4dfa..0000000000000 --- a/packages/@aws-cdk/aws-dynamodb-global/test/test.dynamodb.global.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; -import { Attribute, AttributeType, StreamViewType, Table } from '@aws-cdk/aws-dynamodb'; -import { App, CfnOutput, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import { GlobalTable, GlobalTableProps } from '../lib'; - -/* eslint-disable quote-props */ - -// CDK parameters -const CONSTRUCT_NAME = 'aws-cdk-dynamodb-global'; - -// DynamoDB table parameters -const TABLE_NAME = 'GlobalTable'; -const TABLE_PARTITION_KEY: Attribute = { name: 'hashKey', type: AttributeType.STRING }; - -const STACK_PROPS: GlobalTableProps = { - partitionKey: TABLE_PARTITION_KEY, - tableName: TABLE_NAME, - regions: ['us-east-1', 'us-east-2', 'us-west-2'], -}; - -export = { - 'Default Global DynamoDB stack': { - 'global dynamo'(test: Test) { - const stack = new Stack(); - new GlobalTable(stack, CONSTRUCT_NAME, STACK_PROPS); - const topStack = stack.node.findChild(CONSTRUCT_NAME) as Stack; - for ( const reg of STACK_PROPS.regions ) { - const tableStack = topStack.node.findChild(CONSTRUCT_NAME + '-' + reg) as Stack; - expect(tableStack).to(haveResource('AWS::DynamoDB::Table', { - 'KeySchema': [ - { - 'AttributeName': 'hashKey', - 'KeyType': 'HASH', - }, - ], - 'AttributeDefinitions': [ - { - 'AttributeName': 'hashKey', - 'AttributeType': 'S', - }, - ], - 'StreamSpecification': { - 'StreamViewType': 'NEW_AND_OLD_IMAGES', - }, - 'TableName': 'GlobalTable', - })); - } - const customResourceStack = stack.node.findChild(CONSTRUCT_NAME + '-CustomResource') as Stack; - expect(customResourceStack).to(haveResource('AWS::Lambda::Function', { - Description: 'Lambda to make DynamoDB a global table', - Handler: 'index.handler', - Timeout: 300, - })); - expect(customResourceStack).to(haveResource('AWS::CloudFormation::CustomResource', { - Regions: STACK_PROPS.regions, - ResourceType: 'Custom::DynamoGlobalTableCoordinator', - TableName: TABLE_NAME, - })); - test.done(); - }, - }, - - 'GlobalTable generated stacks inherit their account from the parent stack'(test: Test) { - const app = new App({ context: { '@aws-cdk/core:stackRelativeExports': true } }); - const stack = new Stack(app, 'GlobalTableStack', { env: { account: '123456789012', region: 'us-east-1' } }); - - const globalTable = new GlobalTable(stack, CONSTRUCT_NAME, { - tableName: TABLE_NAME, - partitionKey: TABLE_PARTITION_KEY, - regions: ['us-east-1', 'us-west-2'], - stream: StreamViewType.NEW_AND_OLD_IMAGES, - }); - - new CfnOutput(stack, 'DynamoDbOutput', { - // this works, because both `stack` and `regionTables[0]` stack are in the same account & region - value: globalTable.regionalTables[0].tableStreamArn!, - }); - - expect(stack).toMatch({ - 'Outputs': { - 'DynamoDbOutput': { - 'Value': { - 'Fn::ImportValue': 'GlobalTableStackawscdkdynamodbglobalawscdkdynamodbglobaluseast19C1C8A14:ExportsOutputFnGetAttawscdkdynamodbglobalGlobalTableuseast1FC03DD69StreamArn9CE585ED', - }, - }, - }, - }); - - test.done(); - }, - - 'Enforce StreamSpecification': { - 'global dynamo should only allow NEW_AND_OLD_IMAGES'(test: Test) { - const stack = new Stack(); - - test.throws(() => { - new GlobalTable(stack, CONSTRUCT_NAME, { - tableName: TABLE_NAME, - stream: StreamViewType.KEYS_ONLY, - partitionKey: TABLE_PARTITION_KEY, - regions: ['us-east-1', 'us-east-2', 'us-west-2'], - }); - }, /dynamoProps.stream MUST be set to dynamodb.StreamViewType.NEW_AND_OLD_IMAGES/); - - test.done(); - }, - }, - - 'Check getting tables': { - 'global dynamo should only allow NEW_AND_OLD_IMAGES'(test: Test) { - const stack = new Stack(); - const regTables = new GlobalTable(stack, CONSTRUCT_NAME, { - tableName: TABLE_NAME, - partitionKey: TABLE_PARTITION_KEY, - regions: ['us-east-1', 'us-east-2', 'us-west-2'], - }); - test.equal(regTables.regionalTables.length, 3); - for (const table of regTables.regionalTables) { - test.ok(table instanceof Table); - } - test.done(); - }, - }, -}; diff --git a/packages/@aws-cdk/aws-ssm/.gitignore b/packages/@aws-cdk/aws-ssm/.gitignore index 018c65919d67c..7e6fdd4d423db 100644 --- a/packages/@aws-cdk/aws-ssm/.gitignore +++ b/packages/@aws-cdk/aws-ssm/.gitignore @@ -15,4 +15,5 @@ nyc.config.js *.snk !.eslintrc.js -junit.xml \ No newline at end of file +junit.xml +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ssm/.npmignore b/packages/@aws-cdk/aws-ssm/.npmignore index 9a032ae80868c..e8acf10a468a1 100644 --- a/packages/@aws-cdk/aws-ssm/.npmignore +++ b/packages/@aws-cdk/aws-ssm/.npmignore @@ -24,4 +24,5 @@ tsconfig.json **/cdk.out junit.xml test/ -!*.lit.ts \ No newline at end of file +!*.lit.ts +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ssm/jest.config.js b/packages/@aws-cdk/aws-ssm/jest.config.js new file mode 100644 index 0000000000000..f5d5c4c8ad18f --- /dev/null +++ b/packages/@aws-cdk/aws-ssm/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ssm/package.json b/packages/@aws-cdk/aws-ssm/package.json index dcf9ddfe31a17..47f9ff90a4852 100644 --- a/packages/@aws-cdk/aws-ssm/package.json +++ b/packages/@aws-cdk/aws-ssm/package.json @@ -55,6 +55,7 @@ }, "cdk-build": { "cloudformation": "AWS::SSM", + "jest": true, "env": { "AWSLINT_BASE_CONSTRUCT": true } @@ -72,11 +73,11 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.32", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nodeunit": "^0.11.3", + "jest": "^26.6.3", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-ssm/test/parameter-store-string.test.ts b/packages/@aws-cdk/aws-ssm/test/parameter-store-string.test.ts new file mode 100644 index 0000000000000..a8ce58f22fded --- /dev/null +++ b/packages/@aws-cdk/aws-ssm/test/parameter-store-string.test.ts @@ -0,0 +1,65 @@ +import '@aws-cdk/assert-internal/jest'; +import * as cdk from '@aws-cdk/core'; +import * as ssm from '../lib'; + +test('can reference SSMPS string - specific version', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const ref = ssm.StringParameter.fromStringParameterAttributes(stack, 'Ref', { + parameterName: '/some/key', + version: 123, + }); + + // THEN + expect(stack.resolve(ref.stringValue)).toEqual('{{resolve:ssm:/some/key:123}}'); +}); + +test('can reference SSMPS string - latest version', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const ref = ssm.StringParameter.fromStringParameterAttributes(stack, 'Ref', { + parameterName: '/some/key', + }); + + // THEN + expect(stack).toMatchTemplate({ + Parameters: { + RefParameter: { + Type: 'AWS::SSM::Parameter::Value', + Default: '/some/key', + }, + }, + }); + + expect(stack.resolve(ref.stringValue)).toEqual({ Ref: 'RefParameter' }); +}); + +test('can reference SSMPS secure string', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const ref = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'Ref', { + parameterName: '/some/key', + version: 123, + }).stringValue; + + // THEN + expect(stack.resolve(ref)).toEqual('{{resolve:ssm-secure:/some/key:123}}'); +}); + +test('empty parameterName will throw', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + expect(() => { + ssm.StringParameter.fromStringParameterAttributes(stack, 'Ref', { + parameterName: '', + }); + }).toThrow(/parameterName cannot be an empty string/); +}); diff --git a/packages/@aws-cdk/aws-ssm/test/parameter.test.ts b/packages/@aws-cdk/aws-ssm/test/parameter.test.ts new file mode 100644 index 0000000000000..2e40e4626cbd3 --- /dev/null +++ b/packages/@aws-cdk/aws-ssm/test/parameter.test.ts @@ -0,0 +1,733 @@ +/* eslint-disable max-len */ + +import '@aws-cdk/assert-internal/jest'; +import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; +import * as cdk from '@aws-cdk/core'; +import * as ssm from '../lib'; + +test('creating a String SSM Parameter', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ssm.StringParameter(stack, 'Parameter', { + allowedPattern: '.*', + description: 'The value Foo', + parameterName: 'FooParameter', + stringValue: 'Foo', + }); + + // THEN + expect(stack).toHaveResource('AWS::SSM::Parameter', { + AllowedPattern: '.*', + Description: 'The value Foo', + Name: 'FooParameter', + Type: 'String', + Value: 'Foo', + }); +}); + +test('expect String SSM Parameter to have tier properly set', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ssm.StringParameter(stack, 'Parameter', { + allowedPattern: '.*', + description: 'The value Foo', + parameterName: 'FooParameter', + stringValue: 'Foo', + tier: ssm.ParameterTier.ADVANCED, + }); + + // THEN + expect(stack).toHaveResource('AWS::SSM::Parameter', { + Tier: 'Advanced', + }); +}); + +test('String SSM Parameter rejects invalid values', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => new ssm.StringParameter(stack, 'Parameter', { allowedPattern: '^Bar$', stringValue: 'FooBar' })).toThrow( + /does not match the specified allowedPattern/); +}); + +test('String SSM Parameter allows unresolved tokens', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => { + new ssm.StringParameter(stack, 'Parameter', { + allowedPattern: '^Bar$', + stringValue: cdk.Lazy.string({ produce: () => 'Foo!' }), + }); + }).not.toThrow(); +}); + +test('creating a StringList SSM Parameter', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ssm.StringListParameter(stack, 'Parameter', { + allowedPattern: '(Foo|Bar)', + description: 'The values Foo and Bar', + parameterName: 'FooParameter', + stringListValue: ['Foo', 'Bar'], + }); + + // THEN + expect(stack).toHaveResource('AWS::SSM::Parameter', { + AllowedPattern: '(Foo|Bar)', + Description: 'The values Foo and Bar', + Name: 'FooParameter', + Type: 'StringList', + Value: 'Foo,Bar', + }); +}); + +test('String SSM Parameter throws on long descriptions', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => { + new ssm.StringParameter(stack, 'Parameter', { + stringValue: 'Foo', + description: '1024+ character long description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \ + Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, \ + nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat \ + massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, \ + imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. \ + Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, \ + eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus \ + varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. \ + Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing \ + sem neque sed ipsum.', + }); + }).toThrow(/Description cannot be longer than 1024 characters./); +}); + +test('String SSM Parameter throws on long names', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => { + new ssm.StringParameter(stack, 'Parameter', { + stringValue: 'Foo', + parameterName: '2048+ character long name: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \ + Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, \ + nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat \ + massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, \ + imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. \ + Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, \ + eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus \ + varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. \ + Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing \ + sem neque sed ipsum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \ + Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, \ + nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat \ + massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, \ + imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. \ + Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, \ + eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus \ + varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. \ + Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing \ + sem neque sed ipsum.', + }); + }).toThrow(/Name cannot be longer than 2048 characters./); +}); + +test('StringList SSM Parameter throws on long descriptions', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => { + new ssm.StringListParameter(stack, 'Parameter', { + stringListValue: ['Foo', 'Bar'], + description: '1024+ character long description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \ + Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, \ + nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat \ + massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, \ + imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. \ + Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, \ + eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus \ + varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. \ + Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing \ + sem neque sed ipsum.', + }); + }).toThrow(/Description cannot be longer than 1024 characters./); +}); + +test('StringList SSM Parameter throws on long names', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => { + new ssm.StringListParameter(stack, 'Parameter', { + stringListValue: ['Foo', 'Bar'], + parameterName: '2048+ character long name: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \ + Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, \ + nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat \ + massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, \ + imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. \ + Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, \ + eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus \ + varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. \ + Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing \ + sem neque sed ipsum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \ + Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, \ + nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat \ + massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, \ + imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. \ + Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, \ + eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus \ + varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. \ + Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing \ + sem neque sed ipsum.', + }); + }).toThrow(/Name cannot be longer than 2048 characters./); +}); + +test('StringList SSM Parameter values cannot contain commas', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => new ssm.StringListParameter(stack, 'Parameter', { stringListValue: ['Foo,Bar'] })).toThrow( + /cannot contain the ',' character/); +}); + +test('StringList SSM Parameter rejects invalid values', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => new ssm.StringListParameter(stack, 'Parameter', { allowedPattern: '^(Foo|Bar)$', stringListValue: ['Foo', 'FooBar'] })).toThrow( + /does not match the specified allowedPattern/); +}); + +test('StringList SSM Parameter allows unresolved tokens', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => new ssm.StringListParameter(stack, 'Parameter', { + allowedPattern: '^(Foo|Bar)$', + stringListValue: ['Foo', cdk.Lazy.string({ produce: () => 'Baz!' })], + })).not.toThrow(); +}); + +test('parameterArn is crafted correctly', () => { + // GIVEN + const stack = new cdk.Stack(); + const param = new ssm.StringParameter(stack, 'Parameter', { stringValue: 'Foo' }); + + // THEN + expect(stack.resolve(param.parameterArn)).toEqual({ + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ssm:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':parameter/', + { Ref: 'Parameter9E1B4FBA' }, + ]], + }); +}); + +test('parameterName that includes a "/" must be fully qualified (i.e. begin with "/") as well', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => new ssm.StringParameter(stack, 'myParam', { + stringValue: 'myValue', + parameterName: 'path/to/parameter', + })).toThrow(/Parameter names must be fully qualified/); + + expect(() => new ssm.StringListParameter(stack, 'myParam2', { + stringListValue: ['foo', 'bar'], + parameterName: 'path/to/parameter2', + })).toThrow(/Parameter names must be fully qualified \(if they include \"\/\" they must also begin with a \"\/\"\)\: path\/to\/parameter2/); +}); + +test('StringParameter.fromStringParameterName', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const param = ssm.StringParameter.fromStringParameterName(stack, 'MyParamName', 'MyParamName'); + + // THEN + expect(stack.resolve(param.parameterArn)).toEqual({ + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ssm:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':parameter/MyParamName', + ]], + }); + expect(stack.resolve(param.parameterName)).toEqual('MyParamName'); + expect(stack.resolve(param.parameterType)).toEqual('String'); + expect(stack.resolve(param.stringValue)).toEqual({ Ref: 'MyParamNameParameter' }); + expect(stack).toMatchTemplate({ + Parameters: { + MyParamNameParameter: { + Type: 'AWS::SSM::Parameter::Value', + Default: 'MyParamName', + }, + }, + }); +}); + +test('StringParameter.fromStringParameterAttributes', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const param = ssm.StringParameter.fromStringParameterAttributes(stack, 'MyParamName', { + parameterName: 'MyParamName', + version: 2, + }); + + // THEN + expect(stack.resolve(param.parameterArn)).toEqual({ + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ssm:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':parameter/MyParamName', + ]], + }); + expect(stack.resolve(param.parameterName)).toEqual('MyParamName'); + expect(stack.resolve(param.parameterType)).toEqual('String'); + expect(stack.resolve(param.stringValue)).toEqual('{{resolve:ssm:MyParamName:2}}'); +}); + +test('StringParameter.fromStringParameterAttributes with version from token', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const param = ssm.StringParameter.fromStringParameterAttributes(stack, 'MyParamName', { + parameterName: 'MyParamName', + version: cdk.Token.asNumber({ Ref: 'version' }), + }); + + // THEN + expect(stack.resolve(param.parameterArn)).toEqual({ + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ssm:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':parameter/MyParamName', + ]], + }); + expect(stack.resolve(param.parameterName)).toEqual('MyParamName'); + expect(stack.resolve(param.parameterType)).toEqual('String'); + expect(stack.resolve(param.stringValue)).toEqual({ + 'Fn::Join': ['', [ + '{{resolve:ssm:MyParamName:', + { Ref: 'version' }, + '}}', + ]], + }); +}); + +test('StringParameter.fromSecureStringParameterAttributes', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const param = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'MyParamName', { + parameterName: 'MyParamName', + version: 2, + }); + + // THEN + expect(stack.resolve(param.parameterArn)).toEqual({ + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ssm:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':parameter/MyParamName', + ]], + }); + expect(stack.resolve(param.parameterName)).toEqual('MyParamName'); + expect(stack.resolve(param.parameterType)).toEqual('SecureString'); + expect(stack.resolve(param.stringValue)).toEqual('{{resolve:ssm-secure:MyParamName:2}}'); +}); + +test('StringParameter.fromSecureStringParameterAttributes with version from token', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const param = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'MyParamName', { + parameterName: 'MyParamName', + version: cdk.Token.asNumber({ Ref: 'version' }), + }); + + // THEN + expect(stack.resolve(param.parameterArn)).toEqual({ + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ssm:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':parameter/MyParamName', + ]], + }); + expect(stack.resolve(param.parameterName)).toEqual('MyParamName'); + expect(stack.resolve(param.parameterType)).toEqual('SecureString'); + expect(stack.resolve(param.stringValue)).toEqual({ + 'Fn::Join': ['', [ + '{{resolve:ssm-secure:MyParamName:', + { Ref: 'version' }, + '}}', + ]], + }); +}); + +test('StringParameter.fromSecureStringParameterAttributes with encryption key creates the correct policy for grantRead', () => { + // GIVEN + const stack = new cdk.Stack(); + const key = kms.Key.fromKeyArn(stack, 'CustomKey', 'arn:aws:kms:us-east-1:123456789012:key/xyz'); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.AccountRootPrincipal(), + }); + + // WHEN + const param = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'MyParamName', { + parameterName: 'MyParamName', + version: 2, + encryptionKey: key, + }); + param.grantRead(role); + + // THEN + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'kms:Decrypt', + Effect: 'Allow', + Resource: 'arn:aws:kms:us-east-1:123456789012:key/xyz', + }, + { + Action: [ + 'ssm:DescribeParameters', + 'ssm:GetParameters', + 'ssm:GetParameter', + 'ssm:GetParameterHistory', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ssm:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':parameter/MyParamName', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + }); +}); + +test('StringParameter.fromSecureStringParameterAttributes with encryption key creates the correct policy for grantWrite', () => { + // GIVEN + const stack = new cdk.Stack(); + const key = kms.Key.fromKeyArn(stack, 'CustomKey', 'arn:aws:kms:us-east-1:123456789012:key/xyz'); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.AccountRootPrincipal(), + }); + + // WHEN + const param = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'MyParamName', { + parameterName: 'MyParamName', + version: 2, + encryptionKey: key, + }); + param.grantWrite(role); + + // THEN + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', + ], + Effect: 'Allow', + Resource: 'arn:aws:kms:us-east-1:123456789012:key/xyz', + }, + { + Action: 'ssm:PutParameter', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ssm:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':parameter/MyParamName', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + }); +}); + +test('StringListParameter.fromName', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const param = ssm.StringListParameter.fromStringListParameterName(stack, 'MyParamName', 'MyParamName'); + + // THEN + expect(stack.resolve(param.parameterArn)).toEqual({ + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ssm:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':parameter/MyParamName', + ]], + }); + expect(stack.resolve(param.parameterName)).toEqual('MyParamName'); + expect(stack.resolve(param.parameterType)).toEqual('StringList'); + expect(stack.resolve(param.stringListValue)).toEqual({ 'Fn::Split': [',', '{{resolve:ssm:MyParamName}}'] }); +}); + +test('fromLookup will use the SSM context provider to read value during synthesis', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'my-staq', { env: { region: 'us-east-1', account: '12344' } }); + + // WHEN + const value = ssm.StringParameter.valueFromLookup(stack, 'my-param-name'); + + // THEN + expect(value).toEqual('dummy-value-for-my-param-name'); + expect(app.synth().manifest.missing).toEqual([ + { + key: 'ssm:account=12344:parameterName=my-param-name:region=us-east-1', + props: { + account: '12344', + region: 'us-east-1', + parameterName: 'my-param-name', + }, + provider: 'ssm', + }, + ]); +}); + +describe('valueForStringParameter', () => { + test('returns a token that represents the SSM parameter value', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const value = ssm.StringParameter.valueForStringParameter(stack, 'my-param-name'); + + // THEN + expect(stack).toMatchTemplate({ + Parameters: { + SsmParameterValuemyparamnameC96584B6F00A464EAD1953AFF4B05118Parameter: { + Type: 'AWS::SSM::Parameter::Value', + Default: 'my-param-name', + }, + }, + }); + expect(stack.resolve(value)).toEqual({ Ref: 'SsmParameterValuemyparamnameC96584B6F00A464EAD1953AFF4B05118Parameter' }); + }); + + test('de-dup based on parameter name', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + ssm.StringParameter.valueForStringParameter(stack, 'my-param-name'); + ssm.StringParameter.valueForStringParameter(stack, 'my-param-name'); + ssm.StringParameter.valueForStringParameter(stack, 'my-param-name-2'); + ssm.StringParameter.valueForStringParameter(stack, 'my-param-name'); + + // THEN + expect(stack).toMatchTemplate({ + Parameters: { + SsmParameterValuemyparamnameC96584B6F00A464EAD1953AFF4B05118Parameter: { + Type: 'AWS::SSM::Parameter::Value', + Default: 'my-param-name', + }, + SsmParameterValuemyparamname2C96584B6F00A464EAD1953AFF4B05118Parameter: { + Type: 'AWS::SSM::Parameter::Value', + Default: 'my-param-name-2', + }, + }, + }); + }); + + test('can query actual SSM Parameter Names, multiple times', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + ssm.StringParameter.valueForStringParameter(stack, '/my/param/name'); + ssm.StringParameter.valueForStringParameter(stack, '/my/param/name'); + }); +}); + +test('rendering of parameter arns', () => { + const stack = new cdk.Stack(); + const param = new cdk.CfnParameter(stack, 'param'); + const expectedA = { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/bam']] }; + const expectedB = { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'param' }]] }; + const expectedC = { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'param' }]] }; + let i = 0; + + // WHEN + const case1 = ssm.StringParameter.fromStringParameterName(stack, `p${i++}`, 'bam'); + const case2 = ssm.StringParameter.fromStringParameterName(stack, `p${i++}`, '/bam'); + const case4 = ssm.StringParameter.fromStringParameterAttributes(stack, `p${i++}`, { parameterName: 'bam' }); + const case5 = ssm.StringParameter.fromStringParameterAttributes(stack, `p${i++}`, { parameterName: '/bam' }); + const case6 = ssm.StringParameter.fromStringParameterAttributes(stack, `p${i++}`, { parameterName: param.valueAsString, simpleName: true }); + const case7 = ssm.StringParameter.fromSecureStringParameterAttributes(stack, `p${i++}`, { parameterName: 'bam', version: 10 }); + const case8 = ssm.StringParameter.fromSecureStringParameterAttributes(stack, `p${i++}`, { parameterName: '/bam', version: 10 }); + const case9 = ssm.StringParameter.fromSecureStringParameterAttributes(stack, `p${i++}`, { parameterName: param.valueAsString, version: 10, simpleName: false }); + + // auto-generated name is always generated as a "simple name" (not/a/path) + const case10 = new ssm.StringParameter(stack, `p${i++}`, { stringValue: 'value' }); + + // explicitly named physical name gives us a hint on how to render the ARN + const case11 = new ssm.StringParameter(stack, `p${i++}`, { parameterName: '/foo/bar', stringValue: 'hello' }); + const case12 = new ssm.StringParameter(stack, `p${i++}`, { parameterName: 'simple-name', stringValue: 'hello' }); + + const case13 = new ssm.StringListParameter(stack, `p${i++}`, { stringListValue: ['hello', 'world'] }); + const case14 = new ssm.StringListParameter(stack, `p${i++}`, { parameterName: '/not/simple', stringListValue: ['hello', 'world'] }); + const case15 = new ssm.StringListParameter(stack, `p${i++}`, { parameterName: 'simple', stringListValue: ['hello', 'world'] }); + + // THEN + expect(stack.resolve(case1.parameterArn)).toEqual(expectedA); + expect(stack.resolve(case2.parameterArn)).toEqual(expectedA); + expect(stack.resolve(case4.parameterArn)).toEqual(expectedA); + expect(stack.resolve(case5.parameterArn)).toEqual(expectedA); + expect(stack.resolve(case6.parameterArn)).toEqual(expectedB); + expect(stack.resolve(case7.parameterArn)).toEqual(expectedA); + expect(stack.resolve(case8.parameterArn)).toEqual(expectedA); + expect(stack.resolve(case9.parameterArn)).toEqual(expectedC); + + // new ssm.Parameters determine if "/" is needed based on the posture of `parameterName`. + expect(stack.resolve(case10.parameterArn)).toEqual({ 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p81BB0F6FE' }]] }); + expect(stack.resolve(case11.parameterArn)).toEqual({ 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'p97A508212' }]] }); + expect(stack.resolve(case12.parameterArn)).toEqual({ 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p107D6B8AB0' }]] }); + expect(stack.resolve(case13.parameterArn)).toEqual({ 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p118A9CB02C' }]] }); + expect(stack.resolve(case14.parameterArn)).toEqual({ 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'p129BE4CE91' }]] }); + expect(stack.resolve(case15.parameterArn)).toEqual({ 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p1326A2AEC4' }]] }); +}); + +test('if parameterName is a token separator must be specified', () => { + // GIVEN + const stack = new cdk.Stack(); + const param = new cdk.CfnParameter(stack, 'param'); + let i = 0; + + // WHEN + const p1 = new ssm.StringParameter(stack, `p${i++}`, { parameterName: param.valueAsString, stringValue: 'foo', simpleName: true }); + const p2 = new ssm.StringParameter(stack, `p${i++}`, { parameterName: param.valueAsString, stringValue: 'foo', simpleName: false }); + const p3 = new ssm.StringListParameter(stack, `p${i++}`, { parameterName: param.valueAsString, stringListValue: ['foo'], simpleName: false }); + + // THEN + expect(stack.resolve(p1.parameterArn)).toEqual({ 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p0B02A8F65' }]] }); + expect(stack.resolve(p2.parameterArn)).toEqual({ 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'p1E43AD5AC' }]] }); + expect(stack.resolve(p3.parameterArn)).toEqual({ 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'p2C1903AEB' }]] }); +}); + +test('fails if name is a token and no explicit separator', () => { + // GIVEN + const stack = new cdk.Stack(); + const param = new cdk.CfnParameter(stack, 'param'); + let i = 0; + + // THEN + const expected = /Unable to determine ARN separator for SSM parameter since the parameter name is an unresolved token. Use "fromAttributes" and specify "simpleName" explicitly/; + expect(() => ssm.StringParameter.fromStringParameterName(stack, `p${i++}`, param.valueAsString)).toThrow(expected); + expect(() => ssm.StringParameter.fromSecureStringParameterAttributes(stack, `p${i++}`, { parameterName: param.valueAsString, version: 1 })).toThrow(expected); + expect(() => new ssm.StringParameter(stack, `p${i++}`, { parameterName: param.valueAsString, stringValue: 'foo' })).toThrow(expected); + expect(() => new ssm.StringParameter(stack, `p${i++}`, { parameterName: param.valueAsString, stringValue: 'foo' })).toThrow(expected); +}); + +test('fails if simpleName is wrong based on a concrete physical name', () => { + // GIVEN + const stack = new cdk.Stack(); + let i = 0; + + // THEN + expect(() => ssm.StringParameter.fromStringParameterAttributes(stack, `p${i++}`, { parameterName: 'simple', simpleName: false })).toThrow(/Parameter name "simple" is a simple name, but "simpleName" was explicitly set to false. Either omit it or set it to true/); + expect(() => ssm.StringParameter.fromStringParameterAttributes(stack, `p${i++}`, { parameterName: '/foo/bar', simpleName: true })).toThrow(/Parameter name "\/foo\/bar" is not a simple name, but "simpleName" was explicitly set to true. Either omit it or set it to false/); +}); + +test('fails if parameterName is undefined and simpleName is "false"', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => new ssm.StringParameter(stack, 'p', { simpleName: false, stringValue: 'foo' })).toThrow(/If "parameterName" is not explicitly defined, "simpleName" must be "true" or undefined since auto-generated parameter names always have simple names/); +}); diff --git a/packages/@aws-cdk/aws-ssm/test/ssm-document.test.ts b/packages/@aws-cdk/aws-ssm/test/ssm-document.test.ts new file mode 100644 index 0000000000000..36f6f54c566e6 --- /dev/null +++ b/packages/@aws-cdk/aws-ssm/test/ssm-document.test.ts @@ -0,0 +1,26 @@ +import { expect, haveResource } from '@aws-cdk/assert-internal'; +import * as cdk from '@aws-cdk/core'; +import * as ssm from '../lib'; + +test('association name is rendered properly in L1 construct', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ssm.CfnAssociation(stack, 'Assoc', { + name: 'document', + parameters: { + a: ['a'], + B: [], + }, + }); + + // THEN + expect(stack).to(haveResource('AWS::SSM::Association', { + Name: 'document', + Parameters: { + a: ['a'], + B: [], + }, + })); +}); diff --git a/packages/@aws-cdk/aws-ssm/test/test.parameter-store-string.ts b/packages/@aws-cdk/aws-ssm/test/test.parameter-store-string.ts deleted file mode 100644 index 215a91090bc5c..0000000000000 --- a/packages/@aws-cdk/aws-ssm/test/test.parameter-store-string.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { expect } from '@aws-cdk/assert-internal'; -import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import * as ssm from '../lib'; - -export = { - 'can reference SSMPS string - specific version'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const ref = ssm.StringParameter.fromStringParameterAttributes(stack, 'Ref', { - parameterName: '/some/key', - version: 123, - }); - - // THEN - test.equal(stack.resolve(ref.stringValue), '{{resolve:ssm:/some/key:123}}'); - - test.done(); - }, - - 'can reference SSMPS string - latest version'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const ref = ssm.StringParameter.fromStringParameterAttributes(stack, 'Ref', { - parameterName: '/some/key', - }); - - // THEN - expect(stack).toMatch({ - Parameters: { - RefParameter: { - Type: 'AWS::SSM::Parameter::Value', - Default: '/some/key', - }, - }, - }); - - test.deepEqual(stack.resolve(ref.stringValue), { Ref: 'RefParameter' }); - - test.done(); - }, - - 'can reference SSMPS secure string'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const ref = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'Ref', { - parameterName: '/some/key', - version: 123, - }).stringValue; - - // THEN - test.equal(stack.resolve(ref), '{{resolve:ssm-secure:/some/key:123}}'); - - test.done(); - }, - - 'empty parameterName will throw'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - test.throws(() => { - ssm.StringParameter.fromStringParameterAttributes(stack, 'Ref', { - parameterName: '', - }); - }, /parameterName cannot be an empty string/); - - test.done(); - }, -}; diff --git a/packages/@aws-cdk/aws-ssm/test/test.parameter.ts b/packages/@aws-cdk/aws-ssm/test/test.parameter.ts deleted file mode 100644 index 83b68eda4a508..0000000000000 --- a/packages/@aws-cdk/aws-ssm/test/test.parameter.ts +++ /dev/null @@ -1,778 +0,0 @@ -/* eslint-disable max-len */ - -import { expect, haveResource } from '@aws-cdk/assert-internal'; -import * as iam from '@aws-cdk/aws-iam'; -import * as kms from '@aws-cdk/aws-kms'; -import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import * as ssm from '../lib'; - -export = { - 'creating a String SSM Parameter'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - new ssm.StringParameter(stack, 'Parameter', { - allowedPattern: '.*', - description: 'The value Foo', - parameterName: 'FooParameter', - stringValue: 'Foo', - }); - - // THEN - expect(stack).to(haveResource('AWS::SSM::Parameter', { - AllowedPattern: '.*', - Description: 'The value Foo', - Name: 'FooParameter', - Type: 'String', - Value: 'Foo', - })); - test.done(); - }, - - 'expect String SSM Parameter to have tier properly set'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - new ssm.StringParameter(stack, 'Parameter', { - allowedPattern: '.*', - description: 'The value Foo', - parameterName: 'FooParameter', - stringValue: 'Foo', - tier: ssm.ParameterTier.ADVANCED, - }); - - // THEN - expect(stack).to(haveResource('AWS::SSM::Parameter', { - Tier: 'Advanced', - })); - test.done(); - }, - - 'String SSM Parameter rejects invalid values'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // THEN - test.throws(() => new ssm.StringParameter(stack, 'Parameter', { allowedPattern: '^Bar$', stringValue: 'FooBar' }), - /does not match the specified allowedPattern/); - test.done(); - }, - - 'String SSM Parameter allows unresolved tokens'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // THEN - test.doesNotThrow(() => { - new ssm.StringParameter(stack, 'Parameter', { - allowedPattern: '^Bar$', - stringValue: cdk.Lazy.string({ produce: () => 'Foo!' }), - }); - }); - test.done(); - }, - - 'creating a StringList SSM Parameter'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - new ssm.StringListParameter(stack, 'Parameter', { - allowedPattern: '(Foo|Bar)', - description: 'The values Foo and Bar', - parameterName: 'FooParameter', - stringListValue: ['Foo', 'Bar'], - }); - - // THEN - expect(stack).to(haveResource('AWS::SSM::Parameter', { - AllowedPattern: '(Foo|Bar)', - Description: 'The values Foo and Bar', - Name: 'FooParameter', - Type: 'StringList', - Value: 'Foo,Bar', - })); - test.done(); - }, - - 'String SSM Parameter throws on long descriptions'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // THEN - test.throws(() => { - new ssm.StringParameter(stack, 'Parameter', { - stringValue: 'Foo', - description: '1024+ character long description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \ - Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, \ - nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat \ - massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, \ - imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. \ - Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, \ - eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus \ - varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. \ - Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing \ - sem neque sed ipsum.', - }); - }, /Description cannot be longer than 1024 characters./); - - test.done(); - }, - - 'String SSM Parameter throws on long names'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // THEN - test.throws(() => { - new ssm.StringParameter(stack, 'Parameter', { - stringValue: 'Foo', - parameterName: '2048+ character long name: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \ - Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, \ - nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat \ - massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, \ - imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. \ - Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, \ - eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus \ - varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. \ - Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing \ - sem neque sed ipsum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \ - Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, \ - nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat \ - massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, \ - imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. \ - Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, \ - eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus \ - varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. \ - Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing \ - sem neque sed ipsum.', - }); - }, /Name cannot be longer than 2048 characters./); - - test.done(); - }, - - 'StringList SSM Parameter throws on long descriptions'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // THEN - test.throws(() => { - new ssm.StringListParameter(stack, 'Parameter', { - stringListValue: ['Foo', 'Bar'], - description: '1024+ character long description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \ - Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, \ - nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat \ - massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, \ - imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. \ - Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, \ - eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus \ - varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. \ - Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing \ - sem neque sed ipsum.', - }); - }, /Description cannot be longer than 1024 characters./); - - test.done(); - }, - - 'StringList SSM Parameter throws on long names'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // THEN - test.throws(() => { - new ssm.StringListParameter(stack, 'Parameter', { - stringListValue: ['Foo', 'Bar'], - parameterName: '2048+ character long name: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \ - Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, \ - nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat \ - massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, \ - imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. \ - Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, \ - eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus \ - varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. \ - Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing \ - sem neque sed ipsum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \ - Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, \ - nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat \ - massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, \ - imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. \ - Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, \ - eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus \ - varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. \ - Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing \ - sem neque sed ipsum.', - }); - }, /Name cannot be longer than 2048 characters./); - - test.done(); - }, - - 'StringList SSM Parameter values cannot contain commas'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // THEN - test.throws(() => new ssm.StringListParameter(stack, 'Parameter', { stringListValue: ['Foo,Bar'] }), - /cannot contain the ',' character/); - test.done(); - }, - - 'StringList SSM Parameter rejects invalid values'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // THEN - test.throws(() => new ssm.StringListParameter(stack, 'Parameter', { allowedPattern: '^(Foo|Bar)$', stringListValue: ['Foo', 'FooBar'] }), - /does not match the specified allowedPattern/); - test.done(); - }, - - 'StringList SSM Parameter allows unresolved tokens'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // THEN - test.doesNotThrow(() => new ssm.StringListParameter(stack, 'Parameter', { - allowedPattern: '^(Foo|Bar)$', - stringListValue: ['Foo', cdk.Lazy.string({ produce: () => 'Baz!' })], - })); - test.done(); - }, - - 'parameterArn is crafted correctly'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const param = new ssm.StringParameter(stack, 'Parameter', { stringValue: 'Foo' }); - - // THEN - test.deepEqual(stack.resolve(param.parameterArn), { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':ssm:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':parameter/', - { Ref: 'Parameter9E1B4FBA' }, - ]], - }); - test.done(); - }, - - 'parameterName that includes a "/" must be fully qualified (i.e. begin with "/") as well'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // THEN - test.throws(() => new ssm.StringParameter(stack, 'myParam', { - stringValue: 'myValue', - parameterName: 'path/to/parameter', - }), /Parameter names must be fully qualified/); - - test.throws(() => new ssm.StringListParameter(stack, 'myParam2', { - stringListValue: ['foo', 'bar'], - parameterName: 'path/to/parameter2', - }), /Parameter names must be fully qualified \(if they include \"\/\" they must also begin with a \"\/\"\)\: path\/to\/parameter2/); - - test.done(); - }, - - 'StringParameter.fromStringParameterName'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const param = ssm.StringParameter.fromStringParameterName(stack, 'MyParamName', 'MyParamName'); - - // THEN - test.deepEqual(stack.resolve(param.parameterArn), { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':ssm:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':parameter/MyParamName', - ]], - }); - test.deepEqual(stack.resolve(param.parameterName), 'MyParamName'); - test.deepEqual(stack.resolve(param.parameterType), 'String'); - test.deepEqual(stack.resolve(param.stringValue), { Ref: 'MyParamNameParameter' }); - expect(stack).toMatch({ - Parameters: { - MyParamNameParameter: { - Type: 'AWS::SSM::Parameter::Value', - Default: 'MyParamName', - }, - }, - }); - test.done(); - }, - - 'StringParameter.fromStringParameterAttributes'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const param = ssm.StringParameter.fromStringParameterAttributes(stack, 'MyParamName', { - parameterName: 'MyParamName', - version: 2, - }); - - // THEN - test.deepEqual(stack.resolve(param.parameterArn), { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':ssm:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':parameter/MyParamName', - ]], - }); - test.deepEqual(stack.resolve(param.parameterName), 'MyParamName'); - test.deepEqual(stack.resolve(param.parameterType), 'String'); - test.deepEqual(stack.resolve(param.stringValue), '{{resolve:ssm:MyParamName:2}}'); - test.done(); - }, - - 'StringParameter.fromStringParameterAttributes with version from token'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const param = ssm.StringParameter.fromStringParameterAttributes(stack, 'MyParamName', { - parameterName: 'MyParamName', - version: cdk.Token.asNumber({ Ref: 'version' }), - }); - - // THEN - test.deepEqual(stack.resolve(param.parameterArn), { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':ssm:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':parameter/MyParamName', - ]], - }); - test.deepEqual(stack.resolve(param.parameterName), 'MyParamName'); - test.deepEqual(stack.resolve(param.parameterType), 'String'); - test.deepEqual(stack.resolve(param.stringValue), { - 'Fn::Join': ['', [ - '{{resolve:ssm:MyParamName:', - { Ref: 'version' }, - '}}', - ]], - }); - test.done(); - }, - - 'StringParameter.fromSecureStringParameterAttributes'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const param = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'MyParamName', { - parameterName: 'MyParamName', - version: 2, - }); - - // THEN - test.deepEqual(stack.resolve(param.parameterArn), { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':ssm:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':parameter/MyParamName', - ]], - }); - test.deepEqual(stack.resolve(param.parameterName), 'MyParamName'); - test.deepEqual(stack.resolve(param.parameterType), 'SecureString'); - test.deepEqual(stack.resolve(param.stringValue), '{{resolve:ssm-secure:MyParamName:2}}'); - test.done(); - }, - - 'StringParameter.fromSecureStringParameterAttributes with version from token'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const param = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'MyParamName', { - parameterName: 'MyParamName', - version: cdk.Token.asNumber({ Ref: 'version' }), - }); - - // THEN - test.deepEqual(stack.resolve(param.parameterArn), { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':ssm:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':parameter/MyParamName', - ]], - }); - test.deepEqual(stack.resolve(param.parameterName), 'MyParamName'); - test.deepEqual(stack.resolve(param.parameterType), 'SecureString'); - test.deepEqual(stack.resolve(param.stringValue), { - 'Fn::Join': ['', [ - '{{resolve:ssm-secure:MyParamName:', - { Ref: 'version' }, - '}}', - ]], - }); - test.done(); - }, - - 'StringParameter.fromSecureStringParameterAttributes with encryption key creates the correct policy for grantRead'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const key = kms.Key.fromKeyArn(stack, 'CustomKey', 'arn:aws:kms:us-east-1:123456789012:key/xyz'); - const role = new iam.Role(stack, 'Role', { - assumedBy: new iam.AccountRootPrincipal(), - }); - - // WHEN - const param = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'MyParamName', { - parameterName: 'MyParamName', - version: 2, - encryptionKey: key, - }); - param.grantRead(role); - - // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { - PolicyDocument: { - Statement: [ - { - Action: 'kms:Decrypt', - Effect: 'Allow', - Resource: 'arn:aws:kms:us-east-1:123456789012:key/xyz', - }, - { - Action: [ - 'ssm:DescribeParameters', - 'ssm:GetParameters', - 'ssm:GetParameter', - 'ssm:GetParameterHistory', - ], - Effect: 'Allow', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':ssm:', - { - Ref: 'AWS::Region', - }, - ':', - { - Ref: 'AWS::AccountId', - }, - ':parameter/MyParamName', - ], - ], - }, - }, - ], - Version: '2012-10-17', - }, - })); - - test.done(); - }, - - 'StringParameter.fromSecureStringParameterAttributes with encryption key creates the correct policy for grantWrite'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const key = kms.Key.fromKeyArn(stack, 'CustomKey', 'arn:aws:kms:us-east-1:123456789012:key/xyz'); - const role = new iam.Role(stack, 'Role', { - assumedBy: new iam.AccountRootPrincipal(), - }); - - // WHEN - const param = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'MyParamName', { - parameterName: 'MyParamName', - version: 2, - encryptionKey: key, - }); - param.grantWrite(role); - - // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { - PolicyDocument: { - Statement: [ - { - Action: [ - 'kms:Encrypt', - 'kms:ReEncrypt*', - 'kms:GenerateDataKey*', - ], - Effect: 'Allow', - Resource: 'arn:aws:kms:us-east-1:123456789012:key/xyz', - }, - { - Action: 'ssm:PutParameter', - Effect: 'Allow', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':ssm:', - { - Ref: 'AWS::Region', - }, - ':', - { - Ref: 'AWS::AccountId', - }, - ':parameter/MyParamName', - ], - ], - }, - }, - ], - Version: '2012-10-17', - }, - })); - - test.done(); - }, - - 'StringListParameter.fromName'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const param = ssm.StringListParameter.fromStringListParameterName(stack, 'MyParamName', 'MyParamName'); - - // THEN - test.deepEqual(stack.resolve(param.parameterArn), { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':ssm:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':parameter/MyParamName', - ]], - }); - test.deepEqual(stack.resolve(param.parameterName), 'MyParamName'); - test.deepEqual(stack.resolve(param.parameterType), 'StringList'); - test.deepEqual(stack.resolve(param.stringListValue), { 'Fn::Split': [',', '{{resolve:ssm:MyParamName}}'] }); - test.done(); - }, - - 'fromLookup will use the SSM context provider to read value during synthesis'(test: Test) { - // GIVEN - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'my-staq', { env: { region: 'us-east-1', account: '12344' } }); - - // WHEN - const value = ssm.StringParameter.valueFromLookup(stack, 'my-param-name'); - - // THEN - test.deepEqual(value, 'dummy-value-for-my-param-name'); - test.deepEqual(app.synth().manifest.missing, [ - { - key: 'ssm:account=12344:parameterName=my-param-name:region=us-east-1', - props: { - account: '12344', - region: 'us-east-1', - parameterName: 'my-param-name', - }, - provider: 'ssm', - }, - ]); - test.done(); - }, - - valueForStringParameter: { - - 'returns a token that represents the SSM parameter value'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const value = ssm.StringParameter.valueForStringParameter(stack, 'my-param-name'); - - // THEN - expect(stack).toMatch({ - Parameters: { - SsmParameterValuemyparamnameC96584B6F00A464EAD1953AFF4B05118Parameter: { - Type: 'AWS::SSM::Parameter::Value', - Default: 'my-param-name', - }, - }, - }); - test.deepEqual(stack.resolve(value), { Ref: 'SsmParameterValuemyparamnameC96584B6F00A464EAD1953AFF4B05118Parameter' }); - test.done(); - }, - - 'de-dup based on parameter name'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - ssm.StringParameter.valueForStringParameter(stack, 'my-param-name'); - ssm.StringParameter.valueForStringParameter(stack, 'my-param-name'); - ssm.StringParameter.valueForStringParameter(stack, 'my-param-name-2'); - ssm.StringParameter.valueForStringParameter(stack, 'my-param-name'); - - // THEN - expect(stack).toMatch({ - Parameters: { - SsmParameterValuemyparamnameC96584B6F00A464EAD1953AFF4B05118Parameter: { - Type: 'AWS::SSM::Parameter::Value', - Default: 'my-param-name', - }, - SsmParameterValuemyparamname2C96584B6F00A464EAD1953AFF4B05118Parameter: { - Type: 'AWS::SSM::Parameter::Value', - Default: 'my-param-name-2', - }, - }, - }); - test.done(); - }, - - 'can query actual SSM Parameter Names, multiple times'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - ssm.StringParameter.valueForStringParameter(stack, '/my/param/name'); - ssm.StringParameter.valueForStringParameter(stack, '/my/param/name'); - - test.done(); - }, - }, - - 'rendering of parameter arns'(test: Test) { - const stack = new cdk.Stack(); - const param = new cdk.CfnParameter(stack, 'param'); - const expectedA = { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/bam']] }; - const expectedB = { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'param' }]] }; - const expectedC = { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'param' }]] }; - let i = 0; - - // WHEN - const case1 = ssm.StringParameter.fromStringParameterName(stack, `p${i++}`, 'bam'); - const case2 = ssm.StringParameter.fromStringParameterName(stack, `p${i++}`, '/bam'); - const case4 = ssm.StringParameter.fromStringParameterAttributes(stack, `p${i++}`, { parameterName: 'bam' }); - const case5 = ssm.StringParameter.fromStringParameterAttributes(stack, `p${i++}`, { parameterName: '/bam' }); - const case6 = ssm.StringParameter.fromStringParameterAttributes(stack, `p${i++}`, { parameterName: param.valueAsString, simpleName: true }); - const case7 = ssm.StringParameter.fromSecureStringParameterAttributes(stack, `p${i++}`, { parameterName: 'bam', version: 10 }); - const case8 = ssm.StringParameter.fromSecureStringParameterAttributes(stack, `p${i++}`, { parameterName: '/bam', version: 10 }); - const case9 = ssm.StringParameter.fromSecureStringParameterAttributes(stack, `p${i++}`, { parameterName: param.valueAsString, version: 10, simpleName: false }); - - // auto-generated name is always generated as a "simple name" (not/a/path) - const case10 = new ssm.StringParameter(stack, `p${i++}`, { stringValue: 'value' }); - - // explicitly named physical name gives us a hint on how to render the ARN - const case11 = new ssm.StringParameter(stack, `p${i++}`, { parameterName: '/foo/bar', stringValue: 'hello' }); - const case12 = new ssm.StringParameter(stack, `p${i++}`, { parameterName: 'simple-name', stringValue: 'hello' }); - - const case13 = new ssm.StringListParameter(stack, `p${i++}`, { stringListValue: ['hello', 'world'] }); - const case14 = new ssm.StringListParameter(stack, `p${i++}`, { parameterName: '/not/simple', stringListValue: ['hello', 'world'] }); - const case15 = new ssm.StringListParameter(stack, `p${i++}`, { parameterName: 'simple', stringListValue: ['hello', 'world'] }); - - // THEN - test.deepEqual(stack.resolve(case1.parameterArn), expectedA); - test.deepEqual(stack.resolve(case2.parameterArn), expectedA); - test.deepEqual(stack.resolve(case4.parameterArn), expectedA); - test.deepEqual(stack.resolve(case5.parameterArn), expectedA); - test.deepEqual(stack.resolve(case6.parameterArn), expectedB); - test.deepEqual(stack.resolve(case7.parameterArn), expectedA); - test.deepEqual(stack.resolve(case8.parameterArn), expectedA); - test.deepEqual(stack.resolve(case9.parameterArn), expectedC); - - // new ssm.Parameters determine if "/" is needed based on the posture of `parameterName`. - test.deepEqual(stack.resolve(case10.parameterArn), { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p81BB0F6FE' }]] }); - test.deepEqual(stack.resolve(case11.parameterArn), { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'p97A508212' }]] }); - test.deepEqual(stack.resolve(case12.parameterArn), { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p107D6B8AB0' }]] }); - test.deepEqual(stack.resolve(case13.parameterArn), { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p118A9CB02C' }]] }); - test.deepEqual(stack.resolve(case14.parameterArn), { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'p129BE4CE91' }]] }); - test.deepEqual(stack.resolve(case15.parameterArn), { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p1326A2AEC4' }]] }); - - test.done(); - }, - - 'if parameterName is a token separator must be specified'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const param = new cdk.CfnParameter(stack, 'param'); - let i = 0; - - // WHEN - const p1 = new ssm.StringParameter(stack, `p${i++}`, { parameterName: param.valueAsString, stringValue: 'foo', simpleName: true }); - const p2 = new ssm.StringParameter(stack, `p${i++}`, { parameterName: param.valueAsString, stringValue: 'foo', simpleName: false }); - const p3 = new ssm.StringListParameter(stack, `p${i++}`, { parameterName: param.valueAsString, stringListValue: ['foo'], simpleName: false }); - - // THEN - test.deepEqual(stack.resolve(p1.parameterArn), { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p0B02A8F65' }]] }); - test.deepEqual(stack.resolve(p2.parameterArn), { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'p1E43AD5AC' }]] }); - test.deepEqual(stack.resolve(p3.parameterArn), { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'p2C1903AEB' }]] }); - - test.done(); - }, - - 'fails if name is a token and no explicit separator'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const param = new cdk.CfnParameter(stack, 'param'); - let i = 0; - - // THEN - const expected = /Unable to determine ARN separator for SSM parameter since the parameter name is an unresolved token. Use "fromAttributes" and specify "simpleName" explicitly/; - test.throws(() => ssm.StringParameter.fromStringParameterName(stack, `p${i++}`, param.valueAsString), expected); - test.throws(() => ssm.StringParameter.fromSecureStringParameterAttributes(stack, `p${i++}`, { parameterName: param.valueAsString, version: 1 }), expected); - test.throws(() => new ssm.StringParameter(stack, `p${i++}`, { parameterName: param.valueAsString, stringValue: 'foo' }), expected); - test.throws(() => new ssm.StringParameter(stack, `p${i++}`, { parameterName: param.valueAsString, stringValue: 'foo' }), expected); - test.done(); - }, - - 'fails if simpleName is wrong based on a concrete physical name'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - let i = 0; - - // THEN - test.throws(() => ssm.StringParameter.fromStringParameterAttributes(stack, `p${i++}`, { parameterName: 'simple', simpleName: false }), /Parameter name "simple" is a simple name, but "simpleName" was explicitly set to false. Either omit it or set it to true/); - test.throws(() => ssm.StringParameter.fromStringParameterAttributes(stack, `p${i++}`, { parameterName: '/foo/bar', simpleName: true }), /Parameter name "\/foo\/bar" is not a simple name, but "simpleName" was explicitly set to true. Either omit it or set it to false/); - test.done(); - }, - - 'fails if parameterName is undefined and simpleName is "false"'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // THEN - test.throws(() => new ssm.StringParameter(stack, 'p', { simpleName: false, stringValue: 'foo' }), /If "parameterName" is not explicitly defined, "simpleName" must be "true" or undefined since auto-generated parameter names always have simple names/); - test.done(); - }, -}; diff --git a/packages/@aws-cdk/aws-ssm/test/test.ssm-document.ts b/packages/@aws-cdk/aws-ssm/test/test.ssm-document.ts deleted file mode 100644 index 7e6fbae411a59..0000000000000 --- a/packages/@aws-cdk/aws-ssm/test/test.ssm-document.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; -import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import * as ssm from '../lib'; - -export = { - 'association name is rendered properly in L1 construct'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - new ssm.CfnAssociation(stack, 'Assoc', { - name: 'document', - parameters: { - a: ['a'], - B: [], - }, - }); - - // THEN - expect(stack).to(haveResource('AWS::SSM::Association', { - Name: 'document', - Parameters: { - a: ['a'], - B: [], - }, - })); - test.done(); - }, -}; diff --git a/packages/@aws-cdk/aws-ssm/test/test.util.ts b/packages/@aws-cdk/aws-ssm/test/test.util.ts deleted file mode 100644 index 884f05b26d7e0..0000000000000 --- a/packages/@aws-cdk/aws-ssm/test/test.util.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* eslint-disable max-len */ - -import { Stack, Token } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import { arnForParameterName } from '../lib/util'; - -export = { - arnForParameterName: { - - 'simple names': { - - 'concrete parameterName and no physical name (sep is "/")'(test: Test) { - const stack = new Stack(); - test.deepEqual(stack.resolve(arnForParameterName(stack, 'myParam', undefined)), { - 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/myParam']], - }); - test.done(); - }, - - 'token parameterName and concrete physical name (no additional "/")'(test: Test) { - const stack = new Stack(); - test.deepEqual(stack.resolve(arnForParameterName(stack, Token.asString({ Ref: 'Boom' }), { physicalName: 'myParam' })), { - 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'Boom' }]], - }); - test.done(); - }, - - 'token parameterName, explicit "/" separator'(test: Test) { - const stack = new Stack(); - test.deepEqual(stack.resolve(arnForParameterName(stack, Token.asString({ Ref: 'Boom' }), { simpleName: true })), { - 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'Boom' }]], - }); - test.done(); - }, - - }, - - 'path names': { - - 'concrete parameterName and no physical name (sep is "/")'(test: Test) { - const stack = new Stack(); - test.deepEqual(stack.resolve(arnForParameterName(stack, '/foo/bar', undefined)), { - 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/foo/bar']], - }); - test.done(); - }, - - 'token parameterName and concrete physical name (no sep)'(test: Test) { - const stack = new Stack(); - test.deepEqual(stack.resolve(arnForParameterName(stack, Token.asString({ Ref: 'Boom' }), { physicalName: '/foo/bar' })), { - 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'Boom' }]], - }); - test.done(); - }, - - 'token parameterName, explicit "" separator'(test: Test) { - const stack = new Stack(); - test.deepEqual(stack.resolve(arnForParameterName(stack, Token.asString({ Ref: 'Boom' }), { simpleName: false })), { - 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'Boom' }]], - }); - test.done(); - }, - - }, - - 'fails if explicit separator is not defined and parameterName is a token'(test: Test) { - const stack = new Stack(); - test.throws(() => arnForParameterName(stack, Token.asString({ Ref: 'Boom' })), /Unable to determine ARN separator for SSM parameter since the parameter name is an unresolved token. Use "fromAttributes" and specify "simpleName" explicitly/); - test.done(); - }, - - }, -}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ssm/test/util.test.ts b/packages/@aws-cdk/aws-ssm/test/util.test.ts new file mode 100644 index 0000000000000..c467960421538 --- /dev/null +++ b/packages/@aws-cdk/aws-ssm/test/util.test.ts @@ -0,0 +1,57 @@ +/* eslint-disable max-len */ + +import { Stack, Token } from '@aws-cdk/core'; +import { arnForParameterName } from '../lib/util'; + +describe('arnForParameterName', () => { + describe('simple names', () => { + test('concrete parameterName and no physical name (sep is "/")', () => { + const stack = new Stack(); + expect(stack.resolve(arnForParameterName(stack, 'myParam', undefined))).toEqual({ + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/myParam']], + }); + }); + + test('token parameterName and concrete physical name (no additional "/")', () => { + const stack = new Stack(); + expect(stack.resolve(arnForParameterName(stack, Token.asString({ Ref: 'Boom' }), { physicalName: 'myParam' }))).toEqual({ + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'Boom' }]], + }); + }); + + test('token parameterName, explicit "/" separator', () => { + const stack = new Stack(); + expect(stack.resolve(arnForParameterName(stack, Token.asString({ Ref: 'Boom' }), { simpleName: true }))).toEqual({ + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'Boom' }]], + }); + }); + }); + + describe('path names', () => { + test('concrete parameterName and no physical name (sep is "/")', () => { + const stack = new Stack(); + expect(stack.resolve(arnForParameterName(stack, '/foo/bar', undefined))).toEqual({ + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/foo/bar']], + }); + }); + + test('token parameterName and concrete physical name (no sep)', () => { + const stack = new Stack(); + expect(stack.resolve(arnForParameterName(stack, Token.asString({ Ref: 'Boom' }), { physicalName: '/foo/bar' }))).toEqual({ + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'Boom' }]], + }); + }); + + test('token parameterName, explicit "" separator', () => { + const stack = new Stack(); + expect(stack.resolve(arnForParameterName(stack, Token.asString({ Ref: 'Boom' }), { simpleName: false }))).toEqual({ + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'Boom' }]], + }); + }); + }); + + test('fails if explicit separator is not defined and parameterName is a token', () => { + const stack = new Stack(); + expect(() => arnForParameterName(stack, Token.asString({ Ref: 'Boom' }))).toThrow(/Unable to determine ARN separator for SSM parameter since the parameter name is an unresolved token. Use "fromAttributes" and specify "simpleName" explicitly/); + }); +}); \ No newline at end of file From 124a7a1c20981c72bfdce0c857c87c46c6cb5f51 Mon Sep 17 00:00:00 2001 From: AWS CDK Automation <43080478+aws-cdk-automation@users.noreply.github.com> Date: Fri, 17 Sep 2021 10:54:21 +0100 Subject: [PATCH 4/5] feat(cfnspec): cloudformation spec v41.1.0 (#16524) Co-authored-by: AWS CDK Team --- packages/monocdk/rosetta/cluster.ts-fixture | 20 +++++++++++++++++++ .../rosetta/migrate-opensearch.ts-fixture | 16 +++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 packages/monocdk/rosetta/cluster.ts-fixture create mode 100644 packages/monocdk/rosetta/migrate-opensearch.ts-fixture diff --git a/packages/monocdk/rosetta/cluster.ts-fixture b/packages/monocdk/rosetta/cluster.ts-fixture new file mode 100644 index 0000000000000..82d98ca3e381e --- /dev/null +++ b/packages/monocdk/rosetta/cluster.ts-fixture @@ -0,0 +1,20 @@ +// Fixture with cluster already created +import { Construct, SecretValue, Stack } from '@aws-cdk/core'; +import { Vpc } from '@aws-cdk/aws-ec2'; +import { Cluster, Table, TableAction, User } from '@aws-cdk/aws-redshift'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const vpc = new Vpc(this, 'Vpc'); + const cluster = new Cluster(this, 'Cluster', { + vpc, + masterUser: { + masterUsername: 'admin', + }, + }); + + /// here + } +} diff --git a/packages/monocdk/rosetta/migrate-opensearch.ts-fixture b/packages/monocdk/rosetta/migrate-opensearch.ts-fixture new file mode 100644 index 0000000000000..bb93c1d40f369 --- /dev/null +++ b/packages/monocdk/rosetta/migrate-opensearch.ts-fixture @@ -0,0 +1,16 @@ +import * as cdk from '@aws-cdk/core'; +import * as es from '@aws-cdk/aws-elasticsearch'; +import * as iam from '@aws-cdk/aws-iam'; +import * as opensearch from '@aws-cdk/aws-opensearchservice'; + +declare const role: iam.IRole; +declare const elasticsearchVersion: es.ElasticsearchVersion; +declare const openSearchVersion: opensearch.EngineVersion; + +class Fixture extends cdk.Construct { + constructor(scope: cdk.Construct, id: string) { + super(scope, id); + + /// here + } +} From c6c594159c7fbda66f40fe8666f70b6806bb2d5e Mon Sep 17 00:00:00 2001 From: Unnati Parekh <80710604+upparekh@users.noreply.github.com> Date: Fri, 17 Sep 2021 10:12:00 -0700 Subject: [PATCH 5/5] feat(ecs-service-extensions): Publish Extension (#16326) ---- This PR adds a new service extension, `PublisherExtension`. This extension can be added to a service to allow it to publish events to SNS Topics. (This PR when paired with #16049 can be used to set up the pub/ sub architecture pattern) It sets up publish permissions for the service to be able to publish events to the topics provided. The user can also provide a list of accounts that will be given permissions to subscribe to the given topics. *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../ecs-service-extensions/README.md | 78 +- .../lib/extensions/index.ts | 3 +- .../lib/extensions/injecter.ts | 154 +++ .../test/injecter.test.ts | 114 ++ .../integ.publish-subscribe.expected.json | 996 ++++++++++++++++++ .../test/integ.publish-subscribe.ts | 69 ++ 6 files changed, 1411 insertions(+), 3 deletions(-) create mode 100644 packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/injecter.ts create mode 100644 packages/@aws-cdk-containers/ecs-service-extensions/test/injecter.test.ts create mode 100644 packages/@aws-cdk-containers/ecs-service-extensions/test/integ.publish-subscribe.expected.json create mode 100644 packages/@aws-cdk-containers/ecs-service-extensions/test/integ.publish-subscribe.ts diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/README.md b/packages/@aws-cdk-containers/ecs-service-extensions/README.md index 53e4d2b6f3c56..0556debfc705f 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/README.md +++ b/packages/@aws-cdk-containers/ecs-service-extensions/README.md @@ -19,7 +19,8 @@ The `Service` construct provided by this module can be extended with optional `S - [AWS AppMesh](https://aws.amazon.com/app-mesh/) for adding your application to a service mesh - [Application Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html), for exposing your service to the public - [AWS FireLens](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/using_firelens.html), for filtering and routing application logs -- Queue to allow your service to consume messages from an SQS Queue which is populated by one or more SNS Topics that it is subscribed to +- [Injecter Extension](#injecter-extension), for allowing your service connect to other AWS services by granting permission and injecting environment variables +- [Queue Extension](#queue-extension), for allowing your service to consume messages from an SQS Queue which can be populated by one or more SNS Topics that it is subscribed to - [Community Extensions](#community-extensions), providing support for advanced use cases The `ServiceExtension` class is an abstract class which you can also implement in @@ -322,9 +323,28 @@ const environment = Environment.fromEnvironmentAttributes(stack, 'Environment', ``` +## Injecter Extension + +This service extension accepts a list of `Injectable` resources. It grants access to these resources and adds the necessary environment variables to the tasks that are part of the service. + +For example, an `InjectableTopic` is an SNS Topic that grants permission to the task role and adds the topic ARN as an environment variable to the task definition. + +### Publishing to SNS Topics + +You can use this extension to set up publishing permissions for SNS Topics. + +```ts +nameDescription.add(new InjecterExtension({ + injectables: [new InjectableTopic({ + // SNS Topic the service will publish to + topic: new sns.Topic(stack, 'my-topic'), + })], +})); +``` + ## Queue Extension -This service extension creates a default SQS Queue `eventsQueue` for the service (if not provided) and accepts a list of `ISubscribable` objects that the `eventsQueue` can subscribe to. The service extension creates the subscriptions and sets up permissions for the service to consume messages from the SQS Queue. +This service extension creates a default SQS Queue `eventsQueue` for the service (if not provided) and optionally also accepts list of `ISubscribable` objects that the `eventsQueue` can subscribe to. The service extension creates the subscriptions and sets up permissions for the service to consume messages from the SQS Queue. ### Setting up SNS Topic Subscriptions for SQS Queues @@ -356,6 +376,60 @@ nameDescription.add(new QueueExtension({ })); ``` +## Publish/Subscribe Service Pattern + +The [Publish/Subscribe Service Pattern](https://aws.amazon.com/pub-sub-messaging/) is used for implementing asynchronous communication between services. It involves 'publisher' services emitting events to SNS Topics, which are passed to subscribed SQS queues and then consumed by 'worker' services. + +The following example adds the `InjecterExtension` to a `Publisher` Service which can publish events to an SNS Topic and adds the `QueueExtension` to a `Worker` Service which can poll its `eventsQueue` to consume messages populated by the topic. + +```ts +const environment = new Environment(stack, 'production'); + +const pubServiceDescription = new ServiceDescription(); +pubServiceDescription.add(new Container({ + cpu: 256, + memoryMiB: 512, + trafficPort: 80, + image: ecs.ContainerImage.fromRegistry('sns-publish'), +})); + +const myTopic = new sns.Topic(stack, 'myTopic'); + +// Add the `InjecterExtension` to the service description to allow publishing events to `myTopic` +pubServiceDescription.add(new InjecterExtension({ + injectables: [new InjectableTopic({ + topic: myTopic, + }], +})); + +// Create the `Publisher` Service +new Service(stack, 'Publisher', { + environment: environment, + serviceDescription: pubServiceDescription, +}); + +const subServiceDescription = new ServiceDescription(); +subServiceDescription.add(new Container({ + cpu: 256, + memoryMiB: 512, + trafficPort: 80, + image: ecs.ContainerImage.fromRegistry('sqs-reader'), +})); + +// Add the `QueueExtension` to the service description to subscribe to `myTopic` +subServiceDescription.add(new QueueExtension({ + subscriptions: [new TopicSubscription({ + topic: myTopic, + }], +})); + +// Create the `Worker` Service +new Service(stack, 'Worker', { + environment: environment, + serviceDescription: subServiceDescription, +}); +``` + ## Community Extensions We encourage the development of Community Service Extensions that support diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/index.ts b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/index.ts index 78c138aba0102..2191def8aaa15 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/index.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/index.ts @@ -6,4 +6,5 @@ export * from './cloudwatch-agent'; export * from './scale-on-cpu-utilization'; export * from './xray'; export * from './assign-public-ip'; -export * from './queue'; \ No newline at end of file +export * from './queue'; +export * from './injecter'; diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/injecter.ts b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/injecter.ts new file mode 100644 index 0000000000000..b7c224f52d9f7 --- /dev/null +++ b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/injecter.ts @@ -0,0 +1,154 @@ +import * as ecs from '@aws-cdk/aws-ecs'; +import * as sns from '@aws-cdk/aws-sns'; +import { Service } from '../service'; +import { Container } from './container'; +import { ContainerMutatingHook, ServiceExtension } from './extension-interfaces'; + +// Keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +/** + * An interface that will be implemented by all the resources that can be published events or written data to. + */ +export interface Injectable { + environmentVariables(): { [key: string]: string }; +} + +/** + * An interface that will be implemented by all the injectable resources that need to grant permissions to the task role. + */ +export interface GrantInjectable extends Injectable { + grant(taskDefinition: ecs.TaskDefinition): void; +} + +/** + * The settings for the `InjectableTopic` class. + */ +export interface InjectableTopicProps { + /** + * The SNS Topic to publish events to. + */ + readonly topic: sns.ITopic; +} + +/** + * The `InjectableTopic` class represents SNS Topic resource that can be published events to by the parent service. + */ + +export class InjectableTopic implements GrantInjectable { + public readonly topic: sns.ITopic; + + constructor(props: InjectableTopicProps) { + this.topic = props.topic; + } + + public grant(taskDefinition: ecs.TaskDefinition) { + this.topic.grantPublish(taskDefinition.taskRole); + } + + public environmentVariables(): { [key: string]: string } { + let environment: { [key: string]: string } = {}; + environment[`${this.topic.node.id.toUpperCase()}_TOPIC_ARN`] = this.topic.topicArn; + return environment; + } +} + +/** + * The settings for the Injecter extension. + */ +export interface InjecterExtensionProps { + /** + * The list of injectable resources for this service. + */ + readonly injectables: Injectable[]; +} + +/** + * Settings for the hook which mutates the application container + * to add the injectable resource environment variables. + */ +interface ContainerMutatingProps { + /** + * The resource environment variables to be added to the container environment. + */ + readonly environment: { [key: string]: string }; +} + +/** + * This hook modifies the application container's environment to + * add the injectable resource environment variables. + */ +class InjecterExtensionMutatingHook extends ContainerMutatingHook { + private environment: { [key: string]: string }; + + constructor(props: ContainerMutatingProps) { + super(); + this.environment = props.environment; + } + + public mutateContainerDefinition(props: ecs.ContainerDefinitionOptions): ecs.ContainerDefinitionOptions { + return { + ...props, + + environment: { ...(props.environment || {}), ...this.environment }, + } as ecs.ContainerDefinitionOptions; + } +} + +/** + * This extension accepts a list of `Injectable` resources that the parent service can publish events or write data to. + * It sets up the corresponding permissions for the task role of the parent service. + */ +export class InjecterExtension extends ServiceExtension { + private props: InjecterExtensionProps; + + private environment: { [key: string]: string } = {}; + + constructor(props: InjecterExtensionProps) { + super('injecter'); + + this.props = props; + } + + // @ts-ignore - Ignore unused params that are required for abstract class extend + public prehook(service: Service, scope: Construct) { + this.parentService = service; + + for (const injectable of this.props.injectables) { + for (const [key, val] of Object.entries(injectable.environmentVariables())) { + this.environment[key] = val; + } + } + } + + /** + * Add hooks to the main application extension so that it is modified to + * add the injectable resource environment variables to the container environment. + */ + public addHooks() { + const container = this.parentService.serviceDescription.get('service-container') as Container; + + if (!container) { + throw new Error('Injecter Extension requires an application extension'); + } + + container.addContainerMutatingHook(new InjecterExtensionMutatingHook({ + environment: this.environment, + })); + } + + /** + * After the task definition has been created, this hook grants the required permissions to the task role for the + * parent service. + * + * @param taskDefinition The created task definition + */ + public useTaskDefinition(taskDefinition: ecs.TaskDefinition) { + for (const injectable of this.props.injectables) { + if ((injectable as GrantInjectable).grant !== undefined) { + (injectable as GrantInjectable).grant(taskDefinition); + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/injecter.test.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/injecter.test.ts new file mode 100644 index 0000000000000..3cfd8f4918f48 --- /dev/null +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/injecter.test.ts @@ -0,0 +1,114 @@ +import { countResources, expect, haveResource } from '@aws-cdk/assert-internal'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as sns from '@aws-cdk/aws-sns'; +import * as cdk from '@aws-cdk/core'; +import { Container, Environment, InjecterExtension, InjectableTopic, Service, ServiceDescription } from '../lib'; + +describe('injecter', () => { + test('correctly sets publish permissions for given topics', () => { + // GIVEN + const stack = new cdk.Stack(); + + const environment = new Environment(stack, 'production'); + const serviceDescription = new ServiceDescription(); + + serviceDescription.add(new Container({ + cpu: 256, + memoryMiB: 512, + trafficPort: 80, + image: ecs.ContainerImage.fromRegistry('nathanpeck/name'), + environment: { + PORT: '80', + }, + })); + + // WHEN + const topic1 = new InjectableTopic({ + topic: new sns.Topic(stack, 'topic1'), + }); + + const topic2 = new InjectableTopic({ + topic: new sns.Topic(stack, 'topic2'), + }); + + serviceDescription.add(new InjecterExtension({ + injectables: [topic1, topic2], + })); + + new Service(stack, 'my-service', { + environment, + serviceDescription, + }); + + // THEN + // Ensure creation of provided topics + expect(stack).to(countResources('AWS::SNS::Topic', 2)); + + // Ensure the task role is given permissions to publish events to topics + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'sns:Publish', + Effect: 'Allow', + Resource: { + Ref: 'topic152D84A37', + }, + }, + { + Action: 'sns:Publish', + Effect: 'Allow', + Resource: { + Ref: 'topic2A4FB547F', + }, + }, + ], + Version: '2012-10-17', + }, + })); + + // Ensure that the topic ARNs have been correctly appended to the environment variables + expect(stack).to(haveResource('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Cpu: 256, + Environment: [ + { + Name: 'PORT', + Value: '80', + }, + { + Name: 'TOPIC1_TOPIC_ARN', + Value: { + Ref: 'topic152D84A37', + }, + }, + { + Name: 'TOPIC2_TOPIC_ARN', + Value: { + Ref: 'topic2A4FB547F', + }, + }, + ], + Image: 'nathanpeck/name', + Essential: true, + Memory: 512, + Name: 'app', + PortMappings: [ + { + ContainerPort: 80, + Protocol: 'tcp', + }, + ], + Ulimits: [ + { + HardLimit: 1024000, + Name: 'nofile', + SoftLimit: 1024000, + }, + ], + }, + ], + })); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.publish-subscribe.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.publish-subscribe.expected.json new file mode 100644 index 0000000000000..4fa8a31db3ba9 --- /dev/null +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.publish-subscribe.expected.json @@ -0,0 +1,996 @@ +{ + "Resources": { + "productionenvironmentvpcAEB47DF7": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/production-environment-vpc" + } + ] + } + }, + "productionenvironmentvpcPublicSubnet1Subnet8D92C089": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "productionenvironmentvpcAEB47DF7" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/production-environment-vpc/PublicSubnet1" + } + ] + } + }, + "productionenvironmentvpcPublicSubnet1RouteTable6E9ABC21": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "productionenvironmentvpcAEB47DF7" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/production-environment-vpc/PublicSubnet1" + } + ] + } + }, + "productionenvironmentvpcPublicSubnet1RouteTableAssociationA8117374": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "productionenvironmentvpcPublicSubnet1RouteTable6E9ABC21" + }, + "SubnetId": { + "Ref": "productionenvironmentvpcPublicSubnet1Subnet8D92C089" + } + } + }, + "productionenvironmentvpcPublicSubnet1DefaultRoute524C894D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "productionenvironmentvpcPublicSubnet1RouteTable6E9ABC21" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "productionenvironmentvpcIGWE7C39890" + } + }, + "DependsOn": [ + "productionenvironmentvpcVPCGW1B428D07" + ] + }, + "productionenvironmentvpcPublicSubnet1EIP54BA88DB": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/production-environment-vpc/PublicSubnet1" + } + ] + } + }, + "productionenvironmentvpcPublicSubnet1NATGateway6075E4CA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "productionenvironmentvpcPublicSubnet1Subnet8D92C089" + }, + "AllocationId": { + "Fn::GetAtt": [ + "productionenvironmentvpcPublicSubnet1EIP54BA88DB", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/production-environment-vpc/PublicSubnet1" + } + ] + } + }, + "productionenvironmentvpcPublicSubnet2Subnet298E6C31": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "productionenvironmentvpcAEB47DF7" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/production-environment-vpc/PublicSubnet2" + } + ] + } + }, + "productionenvironmentvpcPublicSubnet2RouteTable842A68D7": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "productionenvironmentvpcAEB47DF7" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/production-environment-vpc/PublicSubnet2" + } + ] + } + }, + "productionenvironmentvpcPublicSubnet2RouteTableAssociation0A7549F3": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "productionenvironmentvpcPublicSubnet2RouteTable842A68D7" + }, + "SubnetId": { + "Ref": "productionenvironmentvpcPublicSubnet2Subnet298E6C31" + } + } + }, + "productionenvironmentvpcPublicSubnet2DefaultRoute92CD697D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "productionenvironmentvpcPublicSubnet2RouteTable842A68D7" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "productionenvironmentvpcIGWE7C39890" + } + }, + "DependsOn": [ + "productionenvironmentvpcVPCGW1B428D07" + ] + }, + "productionenvironmentvpcPublicSubnet2EIP14CA46AA": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/production-environment-vpc/PublicSubnet2" + } + ] + } + }, + "productionenvironmentvpcPublicSubnet2NATGatewayE1850FCC": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "productionenvironmentvpcPublicSubnet2Subnet298E6C31" + }, + "AllocationId": { + "Fn::GetAtt": [ + "productionenvironmentvpcPublicSubnet2EIP14CA46AA", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/production-environment-vpc/PublicSubnet2" + } + ] + } + }, + "productionenvironmentvpcPublicSubnet3SubnetC7B5665D": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "productionenvironmentvpcAEB47DF7" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/production-environment-vpc/PublicSubnet3" + } + ] + } + }, + "productionenvironmentvpcPublicSubnet3RouteTable00E3BF60": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "productionenvironmentvpcAEB47DF7" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/production-environment-vpc/PublicSubnet3" + } + ] + } + }, + "productionenvironmentvpcPublicSubnet3RouteTableAssociationFA34D6E7": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "productionenvironmentvpcPublicSubnet3RouteTable00E3BF60" + }, + "SubnetId": { + "Ref": "productionenvironmentvpcPublicSubnet3SubnetC7B5665D" + } + } + }, + "productionenvironmentvpcPublicSubnet3DefaultRouteE1ADEA6C": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "productionenvironmentvpcPublicSubnet3RouteTable00E3BF60" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "productionenvironmentvpcIGWE7C39890" + } + }, + "DependsOn": [ + "productionenvironmentvpcVPCGW1B428D07" + ] + }, + "productionenvironmentvpcPublicSubnet3EIP53405AED": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/production-environment-vpc/PublicSubnet3" + } + ] + } + }, + "productionenvironmentvpcPublicSubnet3NATGateway94604057": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "productionenvironmentvpcPublicSubnet3SubnetC7B5665D" + }, + "AllocationId": { + "Fn::GetAtt": [ + "productionenvironmentvpcPublicSubnet3EIP53405AED", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/production-environment-vpc/PublicSubnet3" + } + ] + } + }, + "productionenvironmentvpcPrivateSubnet1Subnet53F632E6": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "productionenvironmentvpcAEB47DF7" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/production-environment-vpc/PrivateSubnet1" + } + ] + } + }, + "productionenvironmentvpcPrivateSubnet1RouteTable2C6DFF0C": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "productionenvironmentvpcAEB47DF7" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/production-environment-vpc/PrivateSubnet1" + } + ] + } + }, + "productionenvironmentvpcPrivateSubnet1RouteTableAssociation8BA32463": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "productionenvironmentvpcPrivateSubnet1RouteTable2C6DFF0C" + }, + "SubnetId": { + "Ref": "productionenvironmentvpcPrivateSubnet1Subnet53F632E6" + } + } + }, + "productionenvironmentvpcPrivateSubnet1DefaultRouteFBB3DE6C": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "productionenvironmentvpcPrivateSubnet1RouteTable2C6DFF0C" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "productionenvironmentvpcPublicSubnet1NATGateway6075E4CA" + } + } + }, + "productionenvironmentvpcPrivateSubnet2Subnet756FB93C": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "productionenvironmentvpcAEB47DF7" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/production-environment-vpc/PrivateSubnet2" + } + ] + } + }, + "productionenvironmentvpcPrivateSubnet2RouteTable2F77D0D2": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "productionenvironmentvpcAEB47DF7" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/production-environment-vpc/PrivateSubnet2" + } + ] + } + }, + "productionenvironmentvpcPrivateSubnet2RouteTableAssociation09188261": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "productionenvironmentvpcPrivateSubnet2RouteTable2F77D0D2" + }, + "SubnetId": { + "Ref": "productionenvironmentvpcPrivateSubnet2Subnet756FB93C" + } + } + }, + "productionenvironmentvpcPrivateSubnet2DefaultRoute5F9AB6C1": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "productionenvironmentvpcPrivateSubnet2RouteTable2F77D0D2" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "productionenvironmentvpcPublicSubnet2NATGatewayE1850FCC" + } + } + }, + "productionenvironmentvpcPrivateSubnet3Subnet3BD4064E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "productionenvironmentvpcAEB47DF7" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/production-environment-vpc/PrivateSubnet3" + } + ] + } + }, + "productionenvironmentvpcPrivateSubnet3RouteTable1A244D34": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "productionenvironmentvpcAEB47DF7" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/production-environment-vpc/PrivateSubnet3" + } + ] + } + }, + "productionenvironmentvpcPrivateSubnet3RouteTableAssociation65F18B9C": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "productionenvironmentvpcPrivateSubnet3RouteTable1A244D34" + }, + "SubnetId": { + "Ref": "productionenvironmentvpcPrivateSubnet3Subnet3BD4064E" + } + } + }, + "productionenvironmentvpcPrivateSubnet3DefaultRoute2438918B": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "productionenvironmentvpcPrivateSubnet3RouteTable1A244D34" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "productionenvironmentvpcPublicSubnet3NATGateway94604057" + } + } + }, + "productionenvironmentvpcIGWE7C39890": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/production-environment-vpc" + } + ] + } + }, + "productionenvironmentvpcVPCGW1B428D07": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "productionenvironmentvpcAEB47DF7" + }, + "InternetGatewayId": { + "Ref": "productionenvironmentvpcIGWE7C39890" + } + } + }, + "productionenvironmentclusterC6599D2D": { + "Type": "AWS::ECS::Cluster" + }, + "signupD2AAA171": { + "Type": "AWS::SNS::Topic" + }, + "delete1CCE71FF": { + "Type": "AWS::SNS::Topic" + }, + "PublishertaskdefinitionTaskRoleE8655AA5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PublishertaskdefinitionTaskRoleDefaultPolicyD6E49F15": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "signupD2AAA171" + } + }, + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "delete1CCE71FF" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PublishertaskdefinitionTaskRoleDefaultPolicyD6E49F15", + "Roles": [ + { + "Ref": "PublishertaskdefinitionTaskRoleE8655AA5" + } + ] + } + }, + "PublishertaskdefinitionA4324C64": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Cpu": 256, + "Environment": [ + { + "Name": "PORT", + "Value": "80" + }, + { + "Name": "SIGN-UP_TOPIC_ARN", + "Value": { + "Ref": "signupD2AAA171" + } + }, + { + "Name": "DELETE_TOPIC_ARN", + "Value": { + "Ref": "delete1CCE71FF" + } + } + ], + "Essential": true, + "Image": "nathanpeck/name", + "Memory": 512, + "Name": "app", + "PortMappings": [ + { + "ContainerPort": 80, + "Protocol": "tcp" + } + ], + "Ulimits": [ + { + "HardLimit": 1024000, + "Name": "nofile", + "SoftLimit": 1024000 + } + ] + } + ], + "Cpu": "256", + "Family": "awsecsintegPublishertaskdefinitionD50610D0", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "EC2", + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "PublishertaskdefinitionTaskRoleE8655AA5", + "Arn" + ] + } + } + }, + "PublisherserviceService9EB00F60": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Ref": "productionenvironmentclusterC6599D2D" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 100 + }, + "DesiredCount": 1, + "EnableECSManagedTags": false, + "LaunchType": "FARGATE", + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "PublisherserviceSecurityGroupC7B0C0D0", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "productionenvironmentvpcPrivateSubnet1Subnet53F632E6" + }, + { + "Ref": "productionenvironmentvpcPrivateSubnet2Subnet756FB93C" + }, + { + "Ref": "productionenvironmentvpcPrivateSubnet3Subnet3BD4064E" + } + ] + } + }, + "TaskDefinition": { + "Ref": "PublishertaskdefinitionA4324C64" + } + } + }, + "PublisherserviceSecurityGroupC7B0C0D0": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ/Publisher-service/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "productionenvironmentvpcAEB47DF7" + } + } + }, + "signupqueue33AFF2E6": { + "Type": "AWS::SQS::Queue", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "signupqueuePolicy185ADC00": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Ref": "signupD2AAA171" + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "sns.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "signupqueue33AFF2E6", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "signupqueue33AFF2E6" + } + ] + } + }, + "signupqueueawsecsintegsignup8DE00B29CE828029": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "sqs", + "TopicArn": { + "Ref": "signupD2AAA171" + }, + "Endpoint": { + "Fn::GetAtt": [ + "signupqueue33AFF2E6", + "Arn" + ] + } + } + }, + "EventsDeadLetterQueue404572C7": { + "Type": "AWS::SQS::Queue", + "Properties": { + "MessageRetentionPeriod": 1209600 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "EventsQueueB96EB0D2": { + "Type": "AWS::SQS::Queue", + "Properties": { + "RedrivePolicy": { + "deadLetterTargetArn": { + "Fn::GetAtt": [ + "EventsDeadLetterQueue404572C7", + "Arn" + ] + }, + "maxReceiveCount": 3 + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "EventsQueuePolicyF3E925EC": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Ref": "delete1CCE71FF" + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "sns.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "EventsQueueB96EB0D2", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "EventsQueueB96EB0D2" + } + ] + } + }, + "EventsQueueawsecsintegdeleteF56807768162F4C0": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "sqs", + "TopicArn": { + "Ref": "delete1CCE71FF" + }, + "Endpoint": { + "Fn::GetAtt": [ + "EventsQueueB96EB0D2", + "Arn" + ] + } + } + }, + "WorkertaskdefinitionTaskRole1EBF20D6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "WorkertaskdefinitionTaskRoleDefaultPolicy45EAFD8C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "sqs:ReceiveMessage", + "sqs:ChangeMessageVisibility", + "sqs:GetQueueUrl", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EventsQueueB96EB0D2", + "Arn" + ] + } + }, + { + "Action": [ + "sqs:ReceiveMessage", + "sqs:ChangeMessageVisibility", + "sqs:GetQueueUrl", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "signupqueue33AFF2E6", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "WorkertaskdefinitionTaskRoleDefaultPolicy45EAFD8C", + "Roles": [ + { + "Ref": "WorkertaskdefinitionTaskRole1EBF20D6" + } + ] + } + }, + "WorkertaskdefinitionBF93A675": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Cpu": 256, + "Environment": [ + { + "Name": "PORT", + "Value": "80" + }, + { + "Name": "WORKER_QUEUE_URI", + "Value": { + "Ref": "EventsQueueB96EB0D2" + } + } + ], + "Essential": true, + "Image": "nathanpeck/name", + "Memory": 512, + "Name": "app", + "PortMappings": [ + { + "ContainerPort": 80, + "Protocol": "tcp" + } + ], + "Ulimits": [ + { + "HardLimit": 1024000, + "Name": "nofile", + "SoftLimit": 1024000 + } + ] + } + ], + "Cpu": "256", + "Family": "awsecsintegWorkertaskdefinition32B60762", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "EC2", + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "WorkertaskdefinitionTaskRole1EBF20D6", + "Arn" + ] + } + } + }, + "WorkerserviceService68C5A5C3": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Ref": "productionenvironmentclusterC6599D2D" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 100 + }, + "DesiredCount": 1, + "EnableECSManagedTags": false, + "LaunchType": "FARGATE", + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "WorkerserviceSecurityGroup1CDDB904", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "productionenvironmentvpcPrivateSubnet1Subnet53F632E6" + }, + { + "Ref": "productionenvironmentvpcPrivateSubnet2Subnet756FB93C" + }, + { + "Ref": "productionenvironmentvpcPrivateSubnet3Subnet3BD4064E" + } + ] + } + }, + "TaskDefinition": { + "Ref": "WorkertaskdefinitionBF93A675" + } + } + }, + "WorkerserviceSecurityGroup1CDDB904": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ/Worker-service/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "productionenvironmentvpcAEB47DF7" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.publish-subscribe.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.publish-subscribe.ts new file mode 100644 index 0000000000000..acfdae2437646 --- /dev/null +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.publish-subscribe.ts @@ -0,0 +1,69 @@ +import * as ecs from '@aws-cdk/aws-ecs'; +import * as sns from '@aws-cdk/aws-sns'; +import * as sqs from '@aws-cdk/aws-sqs'; +import * as cdk from '@aws-cdk/core'; +import { Container, Environment, InjecterExtension, InjectableTopic, QueueExtension, Service, ServiceDescription, TopicSubscription } from '../lib'; + + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-ecs-integ'); + +const environment = new Environment(stack, 'production'); + +const pubServiceDescription = new ServiceDescription(); + +pubServiceDescription.add(new Container({ + cpu: 256, + memoryMiB: 512, + trafficPort: 80, + image: ecs.ContainerImage.fromRegistry('nathanpeck/name'), + environment: { + PORT: '80', + }, +})); + +const topic1 = new InjectableTopic({ + topic: new sns.Topic(stack, 'sign-up'), +}); + +const topic2 = new InjectableTopic({ + topic: new sns.Topic(stack, 'delete'), +}); + +pubServiceDescription.add(new InjecterExtension({ + injectables: [topic1, topic2], +})); + +new Service(stack, 'Publisher', { + environment: environment, + serviceDescription: pubServiceDescription, +}); + +const subServiceDescription = new ServiceDescription(); + +subServiceDescription.add(new Container({ + cpu: 256, + memoryMiB: 512, + trafficPort: 80, + image: ecs.ContainerImage.fromRegistry('nathanpeck/name'), + environment: { + PORT: '80', + }, +})); + +const topicSubscription1 = new TopicSubscription({ + topic: topic1.topic, + queue: new sqs.Queue(stack, 'sign-up-queue'), +}); +const topicSubscription2 = new TopicSubscription({ + topic: topic2.topic, +}); + +subServiceDescription.add(new QueueExtension({ + subscriptions: [topicSubscription1, topicSubscription2], +})); + +new Service(stack, 'Worker', { + environment: environment, + serviceDescription: subServiceDescription, +}); \ No newline at end of file